Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
12 changes: 12 additions & 0 deletions xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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();
Expand All @@ -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;
}

Expand All @@ -99,6 +108,9 @@ public <ReqT, RespT> StreamingCall<ReqT, RespT> createStreamingCall(
@Override
public void shutdown() {
channel.shutdown();
if (resourceAllocatingChannelCredentials != null) {
resourceAllocatingChannelCredentials.releaseChannelCredentials();
}
}

private class XdsStreamingCall<ReqT, RespT> implements
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ImmutableList<Closeable>> resourcesSupplier) {
return new ResourceAllocatingChannelCredentials(channelCreds, resourcesSupplier);
}

private final ChannelCredentials channelCreds;
private final Supplier<ImmutableList<Closeable>> resourcesSupplier;
private int refCount;
private ImmutableList<Closeable> resourcesReleaser;

private ResourceAllocatingChannelCredentials(
ChannelCredentials channelCreds, Supplier<ImmutableList<Closeable>> 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");
}
}
167 changes: 166 additions & 1 deletion xds/src/main/java/io/grpc/xds/internal/TlsXdsCredentialsProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Copy link
Contributor

@kannanjgithub kannanjgithub Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The grfc says implementations should be able to reuse the FileWatcherCertificateProvider. It is a bit more work though - we could do similar to CertProvidersslContextProvider's usage here. (You can see it getting used during XdsClientServerSecurityTest)

  1. passing the plugin name as file_watcher and
  2. Make this class (TlsXdsCredentialsProvider) implement Watcher interface for the Watcher parameter that receives certificate updates
  3. Add the returned Handle to the list of Closeables in ResourceAllocationChannelProvider.
  4. This will require making CertificateProviderStore.Handle public.

@ejona86 WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're reading C++-isms, which ignore everything that isn't C++. Java has two sets of classes: FileWatcherCertificateProvider and AdvancedTlsX509TrustManager/KeyManager. C++ only has one. I have no desire to use FileWatcherCertificateProvider and the abomination of plumbing to make that work. If anything, we should delete parts of FileWatcherCertificateProvider to use more of AdvancedTlsX509TrustManager/KeyManager (which were created later). The closest code in Java to FileWatcherCertificateProvider for this purpose is AdvancedTlsX509TrustManager/KeyManager.

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<String, ?> 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
Expand All @@ -48,4 +121,96 @@ public int priority() {
return 5;
}

private static final class ResourcesSupplier implements Supplier<ImmutableList<Closeable>> {
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<Closeable> get() {
ImmutableList.Builder<Closeable> 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<Closeable> 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();
}
}
41 changes: 39 additions & 2 deletions xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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() {
Expand Down
Loading