diff --git a/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java b/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java index b4b9b25d1de..7030aa6d684 100644 --- a/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java +++ b/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java @@ -460,7 +460,7 @@ public Builder setSslSocketAndEnginePeerVerifier(SslSocketAndEnginePeerVerifier return this; } - public AdvancedTlsX509TrustManager build() throws CertificateException { + public AdvancedTlsX509TrustManager build() { return new AdvancedTlsX509TrustManager(this.verification, this.socketAndEnginePeerVerifier); } } diff --git a/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java b/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java index 0da51bf47f7..3d9b07a049e 100644 --- a/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java +++ b/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java @@ -55,6 +55,7 @@ public XdsTransport createForTest(ManagedChannel channel) { static class GrpcXdsTransport implements XdsTransport { private final ManagedChannel channel; + private final ResourceAllocatingChannelCredentials resourceAllocatingChannelCredentials; private final CallCredentials callCredentials; public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo) { @@ -69,6 +70,13 @@ public GrpcXdsTransport(ManagedChannel channel) { public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo, CallCredentials callCredentials) { String target = serverInfo.target(); ChannelCredentials channelCredentials = (ChannelCredentials) serverInfo.implSpecificConfig(); + if (channelCredentials instanceof ResourceAllocatingChannelCredentials) { + this.resourceAllocatingChannelCredentials = + (ResourceAllocatingChannelCredentials) channelCredentials; + channelCredentials = resourceAllocatingChannelCredentials.acquireChannelCredentials(); + } else { + this.resourceAllocatingChannelCredentials = null; + } this.channel = Grpc.newChannelBuilder(target, channelCredentials) .keepAliveTime(5, TimeUnit.MINUTES) .build(); @@ -78,6 +86,7 @@ public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo, CallCredentials call @VisibleForTesting public GrpcXdsTransport(ManagedChannel channel, CallCredentials callCredentials) { this.channel = checkNotNull(channel, "channel"); + this.resourceAllocatingChannelCredentials = null; this.callCredentials = callCredentials; } @@ -99,6 +108,9 @@ public StreamingCall createStreamingCall( @Override public void shutdown() { channel.shutdown(); + if (resourceAllocatingChannelCredentials != null) { + resourceAllocatingChannelCredentials.releaseChannelCredentials(); + } } private class XdsStreamingCall implements diff --git a/xds/src/main/java/io/grpc/xds/ResourceAllocatingChannelCredentials.java b/xds/src/main/java/io/grpc/xds/ResourceAllocatingChannelCredentials.java new file mode 100644 index 00000000000..20a1d09aa9d --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/ResourceAllocatingChannelCredentials.java @@ -0,0 +1,76 @@ +/* + * Copyright 2025 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; + +import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import io.grpc.ChannelCredentials; +import io.grpc.internal.GrpcUtil; +import java.io.Closeable; + +/** + * {@code ChannelCredentials} which holds allocated resources (e.g. file watchers) upon + * instantiation of a given {@code ChannelCredentials} object, which must be closed once + * mentioned {@code ChannelCredentials} are no longer in use. + */ +public final class ResourceAllocatingChannelCredentials extends ChannelCredentials { + public static ChannelCredentials create( + ChannelCredentials channelCreds, Supplier> resourcesSupplier) { + return new ResourceAllocatingChannelCredentials(channelCreds, resourcesSupplier); + } + + private final ChannelCredentials channelCreds; + private final Supplier> resourcesSupplier; + private int refCount; + private ImmutableList resourcesReleaser; + + private ResourceAllocatingChannelCredentials( + ChannelCredentials channelCreds, Supplier> resourcesSupplier) { + this.channelCreds = Preconditions.checkNotNull(channelCreds, "channelCreds"); + this.resourcesSupplier = Preconditions.checkNotNull(resourcesSupplier, "resourcesSupplier"); + this.refCount = 0; + this.resourcesReleaser = null; + } + + public synchronized ChannelCredentials acquireChannelCredentials() { + if (refCount++ == 0) { + resourcesReleaser = resourcesSupplier.get(); + } + return channelCreds; + } + + public synchronized void releaseChannelCredentials() { + if (--refCount == 0) { + for (Closeable resource : resourcesReleaser) { + GrpcUtil.closeQuietly(resource); + } + resourcesReleaser = null; + } + Preconditions.checkState( + refCount >= 0, "Channel credentials were released more times than they were acquired"); + } + + /** + * Please use {@link #acquireChannelCredentials()} to get a shared instance of + * {@code ChannelCredentials} for which stripped tokens can be obtained. + */ + @Override + public ChannelCredentials withoutBearerTokens() { + throw new UnsupportedOperationException("Cannot get stripped tokens"); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/TlsXdsCredentialsProvider.java b/xds/src/main/java/io/grpc/xds/internal/TlsXdsCredentialsProvider.java index f4d26a83795..bd4329c5297 100644 --- a/xds/src/main/java/io/grpc/xds/internal/TlsXdsCredentialsProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/TlsXdsCredentialsProvider.java @@ -16,21 +16,94 @@ package io.grpc.xds.internal; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.Duration; +import com.google.protobuf.util.Durations; import io.grpc.ChannelCredentials; import io.grpc.TlsChannelCredentials; +import io.grpc.internal.GrpcUtil; +import io.grpc.internal.JsonUtil; +import io.grpc.util.AdvancedTlsX509KeyManager; +import io.grpc.util.AdvancedTlsX509TrustManager; +import io.grpc.xds.ResourceAllocatingChannelCredentials; import io.grpc.xds.XdsCredentialsProvider; +import java.io.Closeable; +import java.io.File; +import java.text.ParseException; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; /** * A wrapper class that supports {@link TlsChannelCredentials} for Xds * by implementing {@link XdsCredentialsProvider}. */ public final class TlsXdsCredentialsProvider extends XdsCredentialsProvider { + private static final Logger logger = Logger.getLogger(TlsXdsCredentialsProvider.class.getName()); private static final String CREDS_NAME = "tls"; + private static final String CERT_FILE_KEY = "certificate_file"; + private static final String KEY_FILE_KEY = "private_key_file"; + private static final String ROOT_FILE_KEY = "ca_certificate_file"; + private static final String REFRESH_INTERVAL_KEY = "refresh_interval"; + private static final long REFRESH_INTERVAL_DEFAULT = 600L; + private static final ScheduledExecutorServiceFactory scheduledExecutorServiceFactory = + ScheduledExecutorServiceFactory.DEFAULT_INSTANCE; @Override protected ChannelCredentials newChannelCredentials(Map jsonConfig) { - return TlsChannelCredentials.create(); + TlsChannelCredentials.Builder tlsChannelCredsBuilder = TlsChannelCredentials.newBuilder(); + + if (jsonConfig == null) { + return tlsChannelCredsBuilder.build(); + } + + // use refresh interval from bootstrap config if provided; else defaults to 600s + long refreshIntervalSeconds = REFRESH_INTERVAL_DEFAULT; + String refreshIntervalFromConfig = JsonUtil.getString(jsonConfig, REFRESH_INTERVAL_KEY); + if (refreshIntervalFromConfig != null) { + try { + Duration duration = Durations.parse(refreshIntervalFromConfig); + refreshIntervalSeconds = Durations.toSeconds(duration); + } catch (ParseException e) { + logger.log(Level.WARNING, "Unable to parse refresh interval", e); + return null; + } + } + + // use trust certificate file path from bootstrap config if provided; else use system default + String rootCertPath = JsonUtil.getString(jsonConfig, ROOT_FILE_KEY); + AdvancedTlsX509TrustManager trustManager = null; + if (rootCertPath != null) { + trustManager = AdvancedTlsX509TrustManager.newBuilder().build(); + tlsChannelCredsBuilder.trustManager(trustManager); + } + + // use certificate chain and private key file paths from bootstrap config if provided. Mind that + // both JSON values must be either set (mTLS case) or both unset (TLS case) + String certChainPath = JsonUtil.getString(jsonConfig, CERT_FILE_KEY); + String privateKeyPath = JsonUtil.getString(jsonConfig, KEY_FILE_KEY); + AdvancedTlsX509KeyManager keyManager = null; + if (certChainPath != null && privateKeyPath != null) { + keyManager = new AdvancedTlsX509KeyManager(); + tlsChannelCredsBuilder.keyManager(keyManager); + } else if (certChainPath != null || privateKeyPath != null) { + logger.log(Level.WARNING, "Certificate chain and private key must be both set or unset"); + return null; + } + + return ResourceAllocatingChannelCredentials.create( + tlsChannelCredsBuilder.build(), + new ResourcesSupplier( + refreshIntervalSeconds, + rootCertPath, + trustManager, + certChainPath, + privateKeyPath, + keyManager)); } @Override @@ -48,4 +121,96 @@ public int priority() { return 5; } + private static final class ResourcesSupplier implements Supplier> { + private final long refreshIntervalSeconds; + private final String rootCertPath; + private final AdvancedTlsX509TrustManager trustManager; + private final String certChainPath; + private final String privateKeyPath; + private final AdvancedTlsX509KeyManager keyManager; + + ResourcesSupplier( + long refreshIntervalSeconds, + String rootCertPath, + AdvancedTlsX509TrustManager trustManager, + String certChainPath, + String privateKeyPath, + AdvancedTlsX509KeyManager keyManager) { + this.refreshIntervalSeconds = refreshIntervalSeconds; + this.rootCertPath = rootCertPath; + this.trustManager = trustManager; + this.certChainPath = certChainPath; + this.privateKeyPath = privateKeyPath; + this.keyManager = keyManager; + } + + @Override + public ImmutableList get() { + ImmutableList.Builder resourcesBuilder = ImmutableList.builder(); + + ScheduledExecutorService scheduledExecutorService = + (trustManager != null || keyManager != null) + ? scheduledExecutorService = scheduledExecutorServiceFactory.create() + : null; + if (scheduledExecutorService != null) { + resourcesBuilder.add(asCloseable(scheduledExecutorService)); + } + + if (trustManager != null) { + try { + Closeable trustManagerFuture = trustManager.updateTrustCredentials( + new File(rootCertPath), + refreshIntervalSeconds, + TimeUnit.SECONDS, + scheduledExecutorService); + resourcesBuilder.add(trustManagerFuture); + } catch (Exception e) { + cleanupResources(resourcesBuilder.build()); + throw new RuntimeException("Unable to read root certificates", e); + } + } + + if (keyManager != null) { + try { + Closeable keyManagerFuture = keyManager.updateIdentityCredentials( + new File(certChainPath), + new File(privateKeyPath), + refreshIntervalSeconds, + TimeUnit.SECONDS, + scheduledExecutorService); + resourcesBuilder.add(keyManagerFuture); + } catch (Exception e) { + cleanupResources(resourcesBuilder.build()); + throw new RuntimeException("Unable to read certificate chain or private key", e); + } + } + + return resourcesBuilder.build(); + } + + private static Closeable asCloseable(ScheduledExecutorService scheduledExecutorService) { + return () -> scheduledExecutorService.shutdownNow(); + } + + private static void cleanupResources(ImmutableList resources) { + for (Closeable resource : resources) { + GrpcUtil.closeQuietly(resource); + } + } + } + + abstract static class ScheduledExecutorServiceFactory { + + private static final ScheduledExecutorServiceFactory DEFAULT_INSTANCE = + new ScheduledExecutorServiceFactory() { + + @Override + ScheduledExecutorService create() { + return Executors.newSingleThreadScheduledExecutor( + GrpcUtil.getThreadFactory("grpc-certificate-files-%d", true)); + } + }; + + abstract ScheduledExecutorService create(); + } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java index 3f93cc6f191..d9b6a931a90 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -17,6 +17,9 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CA_PEM_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CLIENT_KEY_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CLIENT_PEM_FILE; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -25,9 +28,9 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import io.grpc.InsecureChannelCredentials; -import io.grpc.TlsChannelCredentials; import io.grpc.internal.GrpcUtil; import io.grpc.internal.GrpcUtil.GrpcBuildVersion; +import io.grpc.internal.testing.TestUtils; import io.grpc.xds.client.Bootstrapper; import io.grpc.xds.client.Bootstrapper.AuthorityInfo; import io.grpc.xds.client.Bootstrapper.BootstrapInfo; @@ -169,7 +172,7 @@ public void parseBootstrap_multipleXdsServers() throws XdsInitializationExceptio assertThat(serverInfoList.get(0).target()) .isEqualTo("trafficdirector-foo.googleapis.com:443"); assertThat(serverInfoList.get(0).implSpecificConfig()) - .isInstanceOf(TlsChannelCredentials.class); + .isInstanceOf(ResourceAllocatingChannelCredentials.class); assertThat(serverInfoList.get(1).target()) .isEqualTo("trafficdirector-bar.googleapis.com:443"); assertThat(serverInfoList.get(1).implSpecificConfig()) @@ -898,6 +901,40 @@ public void badFederationConfig() { } } + @Test + public void parseTlsChannelCredentialsWithCustomCertificatesConfig() + throws XdsInitializationException, IOException { + String rootCertPath = TestUtils.loadCert(CA_PEM_FILE).getAbsolutePath(); + String certChainPath = TestUtils.loadCert(CLIENT_PEM_FILE).getAbsolutePath(); + String privateKeyPath = TestUtils.loadCert(CLIENT_KEY_FILE).getAbsolutePath(); + + String rawData = "{\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\n" + + " \"type\": \"tls\"," + + " \"config\": {\n" + + " \"ca_certificate_file\": \"" + rootCertPath + "\",\n" + + " \"certificate_file\": \"" + certChainPath + "\",\n" + + " \"private_key_file\": \"" + privateKeyPath + "\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + + bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); + BootstrapInfo info = bootstrapper.bootstrap(); + assertThat(info.servers()).hasSize(1); + ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); + assertThat(serverInfo.target()).isEqualTo(SERVER_URI); + assertThat(serverInfo.implSpecificConfig()) + .isInstanceOf(ResourceAllocatingChannelCredentials.class); + } + private static BootstrapperImpl.FileReader createFileReader( final String expectedPath, final String rawData) { return new BootstrapperImpl.FileReader() { diff --git a/xds/src/test/java/io/grpc/xds/ResourceAllocatingChannelCredentialsTest.java b/xds/src/test/java/io/grpc/xds/ResourceAllocatingChannelCredentialsTest.java new file mode 100644 index 00000000000..24d921173d7 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/ResourceAllocatingChannelCredentialsTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2025 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import io.grpc.ChannelCredentials; +import java.io.Closeable; +import java.io.IOException; +import java.lang.reflect.Constructor; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link ResourceAllocatingChannelCredentials}. */ +@RunWith(JUnit4.class) +public class ResourceAllocatingChannelCredentialsTest { + private ChannelCredentials channelCreds = new ChannelCredentials() { + @Override + public ChannelCredentials withoutBearerTokens() { + return this; + } + }; + + private Closeable mockCloseable = mock(Closeable.class); + + @SuppressWarnings("unchecked") + private Supplier> mockSupplier = + (Supplier>) mock(Supplier.class); + + private ResourceAllocatingChannelCredentials unit; + + @Before + public void setUp() throws Exception { + Constructor ctor = + ResourceAllocatingChannelCredentials.class.getDeclaredConstructor( + ChannelCredentials.class, Supplier.class); + ctor.setAccessible(true); + this.unit = ctor.newInstance(channelCreds, mockSupplier); + + when(mockSupplier.get()).thenReturn(ImmutableList.of(mockCloseable)); + } + + @Test + public void withoutBearerTokenThrows() { + Exception ex = assertThrows(UnsupportedOperationException.class, () -> { + unit.withoutBearerTokens(); + }); + + String expectedMsg = "Cannot get stripped tokens"; + String actualMsg = ex.getMessage(); + + assertThat(actualMsg).isEqualTo(expectedMsg); + } + + @Test + public void channelCredentialsAcquiredAndReleasedEqualNumberOfTimes() throws IOException { + int cycles = 5; + + for (int idx = 0; idx < cycles; ++idx) { + assertSame(unit.acquireChannelCredentials(), channelCreds); + } + + for (int idx = 0; idx < cycles; ++idx) { + unit.releaseChannelCredentials(); + } + + verify(mockCloseable, times(1)).close(); + } + + @Test + public void channelCredentialsReleasedMoreTimesThanAcquired() { + assertSame(unit.acquireChannelCredentials(), channelCreds); + unit.releaseChannelCredentials(); + + Exception ex = assertThrows(IllegalStateException.class, () -> { + unit.releaseChannelCredentials(); + }); + + String expectedMsg = "Channel credentials were released more times than they were acquired"; + String actualMsg = ex.getMessage(); + + assertThat(actualMsg).isEqualTo(expectedMsg); + } + + + + @Test + public void channelCredentialsAcquiredMoreTimesThanReleased() throws IOException { + assertSame(unit.acquireChannelCredentials(), channelCreds); + assertSame(unit.acquireChannelCredentials(), channelCreds); + unit.releaseChannelCredentials(); + + verify(mockCloseable, never()).close(); + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/TlsXdsCredentialsProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/TlsXdsCredentialsProviderTest.java index 3ba26bdb281..1f28ee5be3f 100644 --- a/xds/src/test/java/io/grpc/xds/internal/TlsXdsCredentialsProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/TlsXdsCredentialsProviderTest.java @@ -16,13 +16,30 @@ package io.grpc.xds.internal; +import static com.google.common.truth.Truth.assertThat; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CA_PEM_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CLIENT_KEY_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CLIENT_PEM_FILE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import io.grpc.ChannelCredentials; import io.grpc.InternalServiceProviders; import io.grpc.TlsChannelCredentials; +import io.grpc.internal.testing.TestUtils; +import io.grpc.util.AdvancedTlsX509KeyManager; +import io.grpc.util.AdvancedTlsX509TrustManager; +import io.grpc.xds.ResourceAllocatingChannelCredentials; import io.grpc.xds.XdsCredentialsProvider; +import java.util.Map; +import javax.net.ssl.KeyManager; +import javax.net.ssl.TrustManager; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -50,8 +67,95 @@ public void isAvailable() { } @Test - public void channelCredentials() { + public void channelCredentialsWhenNullConfig() { assertSame(TlsChannelCredentials.class, provider.newChannelCredentials(null).getClass()); } + + @Test + public void channelCredentialsWhenInvalidRefreshInterval() { + Map jsonConfig = ImmutableMap.of( + "refresh_interval", "invalid-duration-format"); + assertNull(provider.newChannelCredentials(jsonConfig)); + } + + @Test + public void channelCredentialsWhenNotExistingTrustFileConfig() { + Map jsonConfig = ImmutableMap.of( + "ca_certificate_file", "/tmp/not-exisiting-file.txt"); + + ChannelCredentials channelCredentials = provider.newChannelCredentials(jsonConfig); + assertSame(ResourceAllocatingChannelCredentials.class, channelCredentials.getClass()); + + ResourceAllocatingChannelCredentials resourceAllocatingChannelCredentials = + (ResourceAllocatingChannelCredentials) channelCredentials; + Exception ex = assertThrows(RuntimeException.class, () -> { + resourceAllocatingChannelCredentials.acquireChannelCredentials(); + }); + + String expectedMsg = "Unable to read root certificates"; + String actualMsg = ex.getMessage(); + + assertEquals(expectedMsg, actualMsg); + } + + @Test + public void channelCredentialsWhenNotExistingCertificateFileConfig() { + Map jsonConfig = ImmutableMap.of( + "certificate_file", "/tmp/not-exisiting-file.txt", + "private_key_file", "/tmp/not-exisiting-file-2.txt"); + + ChannelCredentials channelCredentials = provider.newChannelCredentials(jsonConfig); + assertSame(ResourceAllocatingChannelCredentials.class, channelCredentials.getClass()); + + ResourceAllocatingChannelCredentials resourceAllocatingChannelCredentials = + (ResourceAllocatingChannelCredentials) channelCredentials; + Exception ex = assertThrows(RuntimeException.class, () -> { + resourceAllocatingChannelCredentials.acquireChannelCredentials(); + }); + + String expectedMsg = "Unable to read certificate chain or private key"; + String actualMsg = ex.getMessage(); + + assertEquals(expectedMsg, actualMsg); + } + + @Test + public void channelCredentialsWhenInvalidConfig() throws Exception { + String certChainPath = TestUtils.loadCert(CLIENT_PEM_FILE).getAbsolutePath(); + Map jsonConfig = ImmutableMap.of("certificate_file", certChainPath.toString()); + assertNull(provider.newChannelCredentials(jsonConfig)); + } + + @Test + public void channelCredentialsWhenValidConfig() throws Exception { + String rootCertPath = TestUtils.loadCert(CA_PEM_FILE).getAbsolutePath(); + String certChainPath = TestUtils.loadCert(CLIENT_PEM_FILE).getAbsolutePath(); + String privateKeyPath = TestUtils.loadCert(CLIENT_KEY_FILE).getAbsolutePath(); + + Map jsonConfig = ImmutableMap.of( + "ca_certificate_file", rootCertPath, + "certificate_file", certChainPath, + "private_key_file", privateKeyPath, + "refresh_interval", "440s"); + + ChannelCredentials channelCredentials = provider.newChannelCredentials(jsonConfig); + assertSame(ResourceAllocatingChannelCredentials.class, channelCredentials.getClass()); + + ResourceAllocatingChannelCredentials resourceAllocatingChannelCredentials = + (ResourceAllocatingChannelCredentials) channelCredentials; + ChannelCredentials acquiredChannelCredentials = + resourceAllocatingChannelCredentials.acquireChannelCredentials(); + assertSame(TlsChannelCredentials.class, acquiredChannelCredentials.getClass()); + + TlsChannelCredentials tlsChannelCredentials = + (TlsChannelCredentials) acquiredChannelCredentials; + assertThat(tlsChannelCredentials.getKeyManagers()).hasSize(1); + KeyManager keyManager = Iterables.getOnlyElement(tlsChannelCredentials.getKeyManagers()); + assertThat(keyManager).isInstanceOf(AdvancedTlsX509KeyManager.class); + assertThat(tlsChannelCredentials.getTrustManagers()).hasSize(1); + TrustManager trustManager = Iterables.getOnlyElement(tlsChannelCredentials.getTrustManagers()); + assertThat(trustManager).isInstanceOf(AdvancedTlsX509TrustManager.class); + resourceAllocatingChannelCredentials.releaseChannelCredentials(); + } }