diff --git a/xds/build.gradle b/xds/build.gradle index d69fdc9da6..322529b8ef 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -42,6 +42,7 @@ dependencies { project(':grpc-testing-proto'), libraries.guava_testlib signature "org.codehaus.mojo.signature:java17:1.0@signature" + testRuntime libraries.netty_tcnative } sourceSets { diff --git a/xds/src/main/java/io/grpc/xds/sds/SecretProvider.java b/xds/src/main/java/io/grpc/xds/sds/SecretProvider.java index a3418521d6..f7f5dd9094 100644 --- a/xds/src/main/java/io/grpc/xds/sds/SecretProvider.java +++ b/xds/src/main/java/io/grpc/xds/sds/SecretProvider.java @@ -29,12 +29,16 @@ import java.util.concurrent.Executor; public interface SecretProvider { interface Callback { + /** Informs callee of new/updated secret. */ void updateSecret(T secret); + + /** Informs callee of an exception that was generated. */ + void onException(Throwable throwable); } /** - * Registers a callback on the given executor. The callback will run when secret becomes - * available or immediately if the result is already available. + * Registers a callback on the given executor. The callback will run when secret becomes available + * or immediately if the result is already available. */ void addCallback(Callback callback, Executor executor); } diff --git a/xds/src/main/java/io/grpc/xds/sds/SslContextSecretVolumeSecretProvider.java b/xds/src/main/java/io/grpc/xds/sds/SslContextSecretVolumeSecretProvider.java new file mode 100644 index 0000000000..5a7dc2e244 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/sds/SslContextSecretVolumeSecretProvider.java @@ -0,0 +1,191 @@ +/* + * Copyright 2019 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.sds; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import io.envoyproxy.envoy.api.v2.auth.CertificateValidationContext; +import io.envoyproxy.envoy.api.v2.auth.TlsCertificate; +import io.envoyproxy.envoy.api.v2.core.DataSource.SpecifierCase; +import io.grpc.netty.GrpcSslContexts; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import java.io.File; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.net.ssl.SSLException; + +/** + * An SslContext provider that uses file-based secrets (secret volume). Used for both server and + * client SslContexts + */ +final class SslContextSecretVolumeSecretProvider implements SecretProvider { + + private static final Logger logger = + Logger.getLogger(SslContextSecretVolumeSecretProvider.class.getName()); + + private final boolean server; + @Nullable private final String privateKey; + @Nullable private final String privateKeyPassword; + @Nullable private final String certificateChain; + @Nullable private final String trustedCa; + + private SslContextSecretVolumeSecretProvider( + @Nullable String privateKey, + @Nullable String privateKeyPassword, + @Nullable String certificateChain, + @Nullable String trustedCa, + boolean server) { + this.privateKey = privateKey; + this.privateKeyPassword = privateKeyPassword; + this.certificateChain = certificateChain; + this.trustedCa = trustedCa; + this.server = server; + } + + @VisibleForTesting + @Nullable + static CertificateValidationContext validateCertificateContext( + @Nullable CertificateValidationContext certContext, boolean optional) { + if (certContext == null || !certContext.hasTrustedCa()) { + checkArgument(optional, "certContext is required"); + return null; + } + checkArgument( + certContext.getTrustedCa().getSpecifierCase() == SpecifierCase.FILENAME, + "filename expected"); + return certContext; + } + + @VisibleForTesting + @Nullable + static TlsCertificate validateTlsCertificate( + @Nullable TlsCertificate tlsCertificate, boolean optional) { + if (tlsCertificate == null) { + checkArgument(optional, "tlsCertificate is required"); + return null; + } + if (optional + && (tlsCertificate.getPrivateKey().getSpecifierCase() == SpecifierCase.SPECIFIER_NOT_SET) + && (tlsCertificate.getCertificateChain().getSpecifierCase() + == SpecifierCase.SPECIFIER_NOT_SET)) { + return null; + } + checkArgument( + tlsCertificate.getPrivateKey().getSpecifierCase() == SpecifierCase.FILENAME, + "filename expected"); + checkArgument( + tlsCertificate.getCertificateChain().getSpecifierCase() == SpecifierCase.FILENAME, + "filename expected"); + return tlsCertificate; + } + + static SslContextSecretVolumeSecretProvider getProviderForServer( + TlsCertificate tlsCertificate, @Nullable CertificateValidationContext certContext) { + // first validate + validateTlsCertificate(tlsCertificate, /* optional= */ false); + // certContext exists in case of mTLS, else null for a server + if (certContext != null) { + certContext = validateCertificateContext(certContext, /* optional= */ true); + } + String privateKeyPassword = + tlsCertificate.hasPassword() ? tlsCertificate.getPassword().getInlineString() : null; + String trustedCa = certContext != null ? certContext.getTrustedCa().getFilename() : null; + return new SslContextSecretVolumeSecretProvider( + tlsCertificate.getPrivateKey().getFilename(), + privateKeyPassword, + tlsCertificate.getCertificateChain().getFilename(), + trustedCa, + /* server= */ true); + } + + static SslContextSecretVolumeSecretProvider getProviderForClient( + @Nullable TlsCertificate tlsCertificate, CertificateValidationContext certContext) { + // first validate + validateCertificateContext(certContext, /* optional= */ false); + // tlsCertificate exists in case of mTLS, else null for a client + if (tlsCertificate != null) { + tlsCertificate = validateTlsCertificate(tlsCertificate, /* optional= */ true); + } + String privateKey = null; + String privateKeyPassword = null; + String certificateChain = null; + if (tlsCertificate != null) { + privateKey = tlsCertificate.getPrivateKey().getFilename(); + if (tlsCertificate.hasPassword()) { + privateKeyPassword = tlsCertificate.getPassword().getInlineString(); + } + certificateChain = tlsCertificate.getCertificateChain().getFilename(); + } + return new SslContextSecretVolumeSecretProvider( + privateKey, + privateKeyPassword, + certificateChain, + certContext.getTrustedCa().getFilename(), + /* server= */ false); + } + + @Override + public void addCallback(final Callback callback, Executor executor) { + checkNotNull(callback, "callback"); + checkNotNull(executor, "executor"); + executor.execute( + new Runnable() { + @Override + public void run() { + // as per the contract we will read the current secrets on disk + // this involves I/O which can potentially block the executor or event loop + SslContext sslContext = null; + try { + sslContext = buildSslContextFromSecrets(); + try { + callback.updateSecret(sslContext); + } catch (Throwable t) { + logger.log(Level.SEVERE, "Exception from callback.updateSecret", t); + } + } catch (Throwable e) { + logger.log(Level.SEVERE, "Exception from buildSslContextFromSecrets", e); + callback.onException(e); + } + } + }); + } + + @VisibleForTesting + SslContext buildSslContextFromSecrets() throws SSLException { + SslContextBuilder sslContextBuilder; + if (server) { + sslContextBuilder = + GrpcSslContexts.forServer( + new File(certificateChain), new File(privateKey), privateKeyPassword); + if (trustedCa != null) { + sslContextBuilder.trustManager(new File(trustedCa)); + } + } else { + sslContextBuilder = GrpcSslContexts.forClient().trustManager(new File(trustedCa)); + if (privateKey != null && certificateChain != null) { + sslContextBuilder.keyManager( + new File(certificateChain), new File(privateKey), privateKeyPassword); + } + } + return sslContextBuilder.build(); + } +} diff --git a/xds/src/test/java/io/grpc/xds/sds/SslContextSecretVolumeSecretProviderTest.java b/xds/src/test/java/io/grpc/xds/sds/SslContextSecretVolumeSecretProviderTest.java new file mode 100644 index 0000000000..fe7b812960 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/sds/SslContextSecretVolumeSecretProviderTest.java @@ -0,0 +1,523 @@ +/* + * Copyright 2019 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.sds; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.util.concurrent.MoreExecutors; +import io.envoyproxy.envoy.api.v2.auth.CertificateValidationContext; +import io.envoyproxy.envoy.api.v2.auth.TlsCertificate; +import io.envoyproxy.envoy.api.v2.core.DataSource; +import io.grpc.internal.testing.TestUtils; +import io.netty.handler.ssl.SslContext; +import java.io.IOException; +import java.util.List; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link SslContextSecretVolumeSecretProvider}. */ +@RunWith(JUnit4.class) +public class SslContextSecretVolumeSecretProviderTest { + + private static final String SERVER_1_PEM_FILE = "server1.pem"; + private static final String SERVER_1_KEY_FILE = "server1.key"; + private static final String CLIENT_PEM_FILE = "client.pem"; + private static final String CLIENT_KEY_FILE = "client.key"; + private static final String CA_PEM_FILE = "ca.pem"; + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void validateCertificateContext_nullAndNotOptional_throwsException() { + // expect exception when certContext is null and not optional + try { + SslContextSecretVolumeSecretProvider.validateCertificateContext( + /* certContext= */ null, /* optional= */ false); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("certContext is required"); + } + } + + @Test + public void validateCertificateContext_missingTrustCa_throwsException() { + // expect exception when certContext has no CA and not optional + CertificateValidationContext certContext = CertificateValidationContext.getDefaultInstance(); + try { + SslContextSecretVolumeSecretProvider.validateCertificateContext( + certContext, /* optional= */ false); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("certContext is required"); + } + } + + @Test + public void validateCertificateContext_nullAndOptional() { + // certContext argument can be null when optional + CertificateValidationContext certContext = + SslContextSecretVolumeSecretProvider.validateCertificateContext( + /* certContext= */ null, /* optional= */ true); + assertThat(certContext).isNull(); + } + + @Test + public void validateCertificateContext_missingTrustCaOptional() { + // certContext argument can have missing CA when optional + CertificateValidationContext certContext = CertificateValidationContext.getDefaultInstance(); + assertThat( + SslContextSecretVolumeSecretProvider.validateCertificateContext( + certContext, /* optional= */ true)) + .isNull(); + } + + @Test + public void validateCertificateContext_inlineString_throwsException() { + // expect exception when certContext doesn't use filename (inline string) + CertificateValidationContext certContext = + CertificateValidationContext.newBuilder() + .setTrustedCa(DataSource.newBuilder().setInlineString("foo")) + .build(); + try { + SslContextSecretVolumeSecretProvider.validateCertificateContext( + certContext, /* optional= */ false); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("filename expected"); + } + } + + @Test + public void validateCertificateContext_filename() { + // validation succeeds and returns same instance when filename provided + CertificateValidationContext certContext = + CertificateValidationContext.newBuilder() + .setTrustedCa(DataSource.newBuilder().setFilename("bar")) + .build(); + assertThat( + SslContextSecretVolumeSecretProvider.validateCertificateContext( + certContext, /* optional= */ false)) + .isSameInstanceAs(certContext); + } + + @Test + public void validateTlsCertificate_nullAndNotOptional_throwsException() { + // expect exception when tlsCertificate is null and not optional + try { + SslContextSecretVolumeSecretProvider.validateTlsCertificate( + /* tlsCertificate= */ null, /* optional= */ false); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("tlsCertificate is required"); + } + } + + @Test + public void validateTlsCertificate_nullOptional() { + assertThat( + SslContextSecretVolumeSecretProvider.validateTlsCertificate( + /* tlsCertificate= */ null, /* optional= */ true)) + .isNull(); + } + + @Test + public void validateTlsCertificate_defaultInstance_returnsNull() { + // tlsCertificate is not null but has no value (default instance): expect null + TlsCertificate tlsCert = TlsCertificate.getDefaultInstance(); + assertThat( + SslContextSecretVolumeSecretProvider.validateTlsCertificate( + tlsCert, /* optional= */ true)) + .isNull(); + } + + @Test + public void validateTlsCertificate_missingCertChainNotOptional_throwsException() { + // expect exception when tlsCertificate has missing certChain and not optional + TlsCertificate tlsCert = + TlsCertificate.newBuilder() + .setPrivateKey(DataSource.newBuilder().setInlineString("foo")) + .build(); + try { + SslContextSecretVolumeSecretProvider.validateTlsCertificate(tlsCert, /* optional= */ false); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("filename expected"); + } + } + + @Test + public void validateTlsCertificate_missingCertChainOptional_throwsException() { + // expect exception when tlsCertificate has missing certChain even if optional + TlsCertificate tlsCert = + TlsCertificate.newBuilder() + .setPrivateKey(DataSource.newBuilder().setInlineString("foo")) + .build(); + try { + SslContextSecretVolumeSecretProvider.validateTlsCertificate(tlsCert, /* optional= */ true); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("filename expected"); + } + } + + @Test + public void validateTlsCertificate_missingPrivateKeyNotOptional_throwsException() { + // expect exception when tlsCertificate has missing private key and not optional + TlsCertificate tlsCert = + TlsCertificate.newBuilder() + .setCertificateChain(DataSource.newBuilder().setInlineString("foo")) + .build(); + try { + SslContextSecretVolumeSecretProvider.validateTlsCertificate(tlsCert, /* optional= */ false); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("filename expected"); + } + } + + @Test + public void validateTlsCertificate_missingPrivateKeyOptional_throwsException() { + // expect exception when tlsCertificate has missing private key even if optional + TlsCertificate tlsCert = + TlsCertificate.newBuilder() + .setCertificateChain(DataSource.newBuilder().setInlineString("foo")) + .build(); + try { + SslContextSecretVolumeSecretProvider.validateTlsCertificate(tlsCert, /* optional= */ true); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("filename expected"); + } + } + + @Test + public void validateTlsCertificate_optional_returnsSameInstance() { + TlsCertificate tlsCert = + TlsCertificate.newBuilder() + .setCertificateChain(DataSource.newBuilder().setFilename("foo")) + .setPrivateKey(DataSource.newBuilder().setFilename("bar")) + .build(); + assertThat( + SslContextSecretVolumeSecretProvider.validateTlsCertificate( + tlsCert, /* optional= */ true)) + .isSameInstanceAs(tlsCert); + } + + @Test + public void validateTlsCertificate_notOptional_returnsSameInstance() { + TlsCertificate tlsCert = + TlsCertificate.newBuilder() + .setCertificateChain(DataSource.newBuilder().setFilename("foo")) + .setPrivateKey(DataSource.newBuilder().setFilename("bar")) + .build(); + assertThat( + SslContextSecretVolumeSecretProvider.validateTlsCertificate( + tlsCert, /* optional= */ false)) + .isSameInstanceAs(tlsCert); + } + + @Test + public void validateTlsCertificate_certChainInlineString_throwsException() { + // expect exception when tlsCertificate has certChain as inline string + TlsCertificate tlsCert = + TlsCertificate.newBuilder() + .setCertificateChain(DataSource.newBuilder().setInlineString("foo")) + .setPrivateKey(DataSource.newBuilder().setFilename("bar")) + .build(); + try { + SslContextSecretVolumeSecretProvider.validateTlsCertificate(tlsCert, /* optional= */ true); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("filename expected"); + } + } + + @Test + public void validateTlsCertificate_privateKeyInlineString_throwsException() { + // expect exception when tlsCertificate has private key as inline string + TlsCertificate tlsCert = + TlsCertificate.newBuilder() + .setPrivateKey(DataSource.newBuilder().setInlineString("foo")) + .setCertificateChain(DataSource.newBuilder().setFilename("bar")) + .build(); + try { + SslContextSecretVolumeSecretProvider.validateTlsCertificate(tlsCert, /* optional= */ true); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("filename expected"); + } + } + + @Test + public void getProviderForServer_nullTlsCertificate_throwsException() { + try { + SslContextSecretVolumeSecretProvider.getProviderForServer( + /* tlsCertificate= */ null, /* certContext= */ null); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("tlsCertificate is required"); + } + } + + @Test + public void getProviderForServer_defaultTlsCertificate_throwsException() { + TlsCertificate tlsCert = TlsCertificate.getDefaultInstance(); + try { + SslContextSecretVolumeSecretProvider.getProviderForServer(tlsCert, /* certContext= */ null); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("filename expected"); + } + } + + @Test + public void getProviderForServer_certContextWithInlineString_throwsException() { + TlsCertificate tlsCert = + TlsCertificate.newBuilder() + .setCertificateChain(DataSource.newBuilder().setFilename("foo")) + .setPrivateKey(DataSource.newBuilder().setFilename("bar")) + .build(); + CertificateValidationContext certContext = + CertificateValidationContext.newBuilder() + .setTrustedCa(DataSource.newBuilder().setInlineString("foo")) + .build(); + try { + SslContextSecretVolumeSecretProvider.getProviderForServer(tlsCert, certContext); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("filename expected"); + } + } + + @Test + public void getProviderForClient_nullCertContext_throwsException() { + try { + SslContextSecretVolumeSecretProvider.getProviderForClient( + /* tlsCertificate= */ null, /* certContext= */ null); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("certContext is required"); + } + } + + @Test + public void getProviderForClient_defaultCertContext_throwsException() { + CertificateValidationContext certContext = CertificateValidationContext.getDefaultInstance(); + try { + SslContextSecretVolumeSecretProvider.getProviderForClient( + /* tlsCertificate= */ null, certContext); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("certContext is required"); + } + } + + @Test + public void getProviderForClient_certWithPrivateKeyInlineString_throwsException() { + TlsCertificate tlsCert = + TlsCertificate.newBuilder() + .setCertificateChain(DataSource.newBuilder().setFilename("foo")) + .setPrivateKey(DataSource.newBuilder().setInlineString("bar")) + .build(); + CertificateValidationContext certContext = + CertificateValidationContext.newBuilder() + .setTrustedCa(DataSource.newBuilder().setFilename("foo")) + .build(); + try { + SslContextSecretVolumeSecretProvider.getProviderForClient(tlsCert, certContext); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("filename expected"); + } + } + + @Test + public void getProviderForClient_certWithCertChainInlineString_throwsException() { + TlsCertificate tlsCert = + TlsCertificate.newBuilder() + .setCertificateChain(DataSource.newBuilder().setInlineString("foo")) + .setPrivateKey(DataSource.newBuilder().setFilename("bar")) + .build(); + CertificateValidationContext certContext = + CertificateValidationContext.newBuilder() + .setTrustedCa(DataSource.newBuilder().setFilename("foo")) + .build(); + try { + SslContextSecretVolumeSecretProvider.getProviderForClient(tlsCert, certContext); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo("filename expected"); + } + } + + private static String getTempFileNameForResourcesFile(String resFile) throws IOException { + return TestUtils.loadCert(resFile).getAbsolutePath(); + } + + /** Helper method to build SslContextSecretVolumeSecretProvider from given files. */ + private static SslContextSecretVolumeSecretProvider getSslContextSecretVolumeSecretProvider( + boolean server, String certChainFilename, String privateKeyFilename, String trustedCaFilename) + throws IOException { + + // get temp file for each file + if (certChainFilename != null) { + certChainFilename = getTempFileNameForResourcesFile(certChainFilename); + } + if (privateKeyFilename != null) { + privateKeyFilename = getTempFileNameForResourcesFile(privateKeyFilename); + } + if (trustedCaFilename != null) { + trustedCaFilename = getTempFileNameForResourcesFile(trustedCaFilename); + } + + TlsCertificate tlsCert = + (certChainFilename == null && privateKeyFilename == null) + ? null + : TlsCertificate.newBuilder() + .setCertificateChain(DataSource.newBuilder().setFilename(certChainFilename)) + .setPrivateKey(DataSource.newBuilder().setFilename(privateKeyFilename)) + .build(); + CertificateValidationContext certContext = + trustedCaFilename == null + ? null + : CertificateValidationContext.newBuilder() + .setTrustedCa(DataSource.newBuilder().setFilename(trustedCaFilename)) + .build(); + + return server + ? SslContextSecretVolumeSecretProvider.getProviderForServer(tlsCert, certContext) + : SslContextSecretVolumeSecretProvider.getProviderForClient(tlsCert, certContext); + } + + /** + * Helper method to build SslContextSecretVolumeSecretProvider, call buildSslContext on it and + * check returned SslContext. + */ + private void sslContextForEitherWithBothCertAndTrust( + boolean server, String pemFile, String keyFile, String caFile) throws IOException { + SslContextSecretVolumeSecretProvider provider = + getSslContextSecretVolumeSecretProvider(server, pemFile, keyFile, caFile); + + SslContext sslContext = provider.buildSslContextFromSecrets(); + doChecksOnSslContext(server, sslContext); + } + + private void doChecksOnSslContext(boolean server, SslContext sslContext) { + if (server) { + assertThat(sslContext.isServer()).isTrue(); + } else { + assertThat(sslContext.isClient()).isTrue(); + } + List apnProtos = sslContext.applicationProtocolNegotiator().protocols(); + assertThat(apnProtos).isNotNull(); + assertThat(apnProtos).contains("h2"); + } + + @Test + public void getProviderForServer() throws IOException { + sslContextForEitherWithBothCertAndTrust( + true, SERVER_1_PEM_FILE, SERVER_1_KEY_FILE, CA_PEM_FILE); + } + + @Test + public void getProviderForClient() throws IOException { + sslContextForEitherWithBothCertAndTrust(false, CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE); + } + + @Test + public void getProviderForServer_onlyCert() throws IOException { + sslContextForEitherWithBothCertAndTrust(true, SERVER_1_PEM_FILE, SERVER_1_KEY_FILE, null); + } + + @Test + public void getProviderForClient_onlyTrust() throws IOException { + sslContextForEitherWithBothCertAndTrust(false, null, null, CA_PEM_FILE); + } + + @Test + public void getProviderForServer_badFile_throwsException() throws IOException { + try { + sslContextForEitherWithBothCertAndTrust(true, SERVER_1_PEM_FILE, SERVER_1_PEM_FILE, null); + Assert.fail("no exception thrown"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).contains("File does not contain valid private key"); + } + } + + static class TestCallback implements SecretProvider.Callback { + + T updatedSecret; + Throwable updatedThrowable; + + @Override + public void updateSecret(T secret) { + updatedSecret = secret; + } + + @Override + public void onException(Throwable throwable) { + updatedThrowable = throwable; + } + } + + /** + * Helper method to get the value thru directExecutor callback. Because of directExecutor this is + * a synchronous callback - so need to provide a listener. + */ + private static TestCallback getValueThruCallback( + SecretProvider provider) { + TestCallback testCallback = new TestCallback<>(); + provider.addCallback(testCallback, MoreExecutors.directExecutor()); + return testCallback; + } + + @Test + public void getProviderForServer_both_callsback() throws IOException { + SslContextSecretVolumeSecretProvider provider = + getSslContextSecretVolumeSecretProvider( + true, SERVER_1_PEM_FILE, SERVER_1_KEY_FILE, CA_PEM_FILE); + + TestCallback testCallback = getValueThruCallback(provider); + doChecksOnSslContext(true, testCallback.updatedSecret); + } + + @Test + public void getProviderForClient_both_callsback() throws IOException { + SslContextSecretVolumeSecretProvider provider = + getSslContextSecretVolumeSecretProvider( + false, CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE); + + TestCallback testCallback = getValueThruCallback(provider); + doChecksOnSslContext(false, testCallback.updatedSecret); + } + + @Test + public void getProviderForClient_both_callsback_setException() throws IOException { + SslContextSecretVolumeSecretProvider provider = + getSslContextSecretVolumeSecretProvider( + false, CLIENT_PEM_FILE, CLIENT_PEM_FILE, CA_PEM_FILE); + TestCallback testCallback = getValueThruCallback(provider); + assertThat(testCallback.updatedSecret).isNull(); + assertThat(testCallback.updatedThrowable).isInstanceOf(IllegalArgumentException.class); + assertThat(testCallback.updatedThrowable.getMessage()) + .contains("File does not contain valid private key"); + } +} diff --git a/xds/src/test/java/io/grpc/xds/sds/TlsCertificateSecretProviderMapTest.java b/xds/src/test/java/io/grpc/xds/sds/TlsCertificateSecretProviderMapTest.java index 681cab46d8..297b1b022b 100644 --- a/xds/src/test/java/io/grpc/xds/sds/TlsCertificateSecretProviderMapTest.java +++ b/xds/src/test/java/io/grpc/xds/sds/TlsCertificateSecretProviderMapTest.java @@ -58,27 +58,16 @@ public class TlsCertificateSecretProviderMapTest { .build(); } - static class TestCallback implements - SecretProvider.Callback { - - TlsCertificateStore updatedSecret; - - @Override - public void updateSecret(TlsCertificateStore secret) { - updatedSecret = secret; - } - } - /** * Helper method to get the value thru directExecutore callback. Used by other classes. */ static TlsCertificateStore getValueThruCallback(SecretProvider provider) { - TestCallback testCallback = new TestCallback(); + SslContextSecretVolumeSecretProviderTest.TestCallback testCallback + = new SslContextSecretVolumeSecretProviderTest.TestCallback<>(); provider.addCallback(testCallback, MoreExecutors.directExecutor()); return testCallback.updatedSecret; } - @Test public void createTest() throws IOException, ExecutionException, InterruptedException { ConfigSource configSource = createFileAndConfigSource(temporaryFolder); diff --git a/xds/src/test/java/io/grpc/xds/sds/TlsCertificateSecretVolumeSecretProviderTest.java b/xds/src/test/java/io/grpc/xds/sds/TlsCertificateSecretVolumeSecretProviderTest.java index 92b33f877a..cc202f4a5b 100644 --- a/xds/src/test/java/io/grpc/xds/sds/TlsCertificateSecretVolumeSecretProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/sds/TlsCertificateSecretVolumeSecretProviderTest.java @@ -95,6 +95,11 @@ public class TlsCertificateSecretVolumeSecretProviderTest { public void updateSecret(TlsCertificateStore secret) { listenerRun = true; } + + @Override + public void onException(Throwable throwable) { + + } }, MoreExecutors.directExecutor()); assertThat(listenerRun).isTrue(); } @@ -131,6 +136,11 @@ public class TlsCertificateSecretVolumeSecretProviderTest { public void updateSecret(TlsCertificateStore secret) { listenerRun = true; } + + @Override + public void onException(Throwable throwable) { + + } }, MoreExecutors.directExecutor()); assertThat(listenerRun).isFalse(); }