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 @@ -31,6 +31,7 @@

package com.google.auth.mtls;

import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.core.InternalApi;
import com.google.auth.http.HttpTransportFactory;
Expand Down Expand Up @@ -62,7 +63,7 @@ public MtlsHttpTransportFactory(KeyStore mtlsKeyStore) {
}

@Override
public NetHttpTransport create() {
public HttpTransport create() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

qq, is this change necessary for the PR? I know this is marked with @internalapi and this isn't exactly customers are expected to interact with directly.

There are some small source and binary compatibility changes (I think very small chance) but I would prefer to keep it as-is unless we absolutely need to

try {
// Build the mTLS transport using the provided KeyStore.
return new NetHttpTransport.Builder().trustCertificates(null, mtlsKeyStore, "").build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ public class MtlsUtils {
static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";

@com.google.common.annotations.VisibleForTesting
static String spiffeDirectory = "/var/run/secrets/workload-spiffe-credentials/";

static final String SPIFFE_CREDENTIAL_BUNDLE_FILE = "credentialbundle.pem";
static final String SPIFFE_CERTIFICATE_FILE = "certificates.pem";
static final String SPIFFE_PRIVATE_KEY_FILE = "private_key.pem";

public enum MtlsEndpointUsagePolicy {
ALWAYS,
NEVER,
AUTO
}
Comment on lines +61 to +65
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: Add some small javadoc/ comments for this public enum. I think we can reference the AIP as well: https://google.aip.dev/auth/4114


private MtlsUtils() {
// Prevent instantiation for Utility class
}
Expand Down Expand Up @@ -137,4 +150,98 @@ private static File getWellKnownCertificateConfigFile(
}
return new File(cloudConfigPath, WELL_KNOWN_CERTIFICATE_CONFIG_FILE);
}

/**
* Centralized helper method to determine if mutual TLS (mTLS) can be enabled.
*
* @param envProvider the environment provider to use for resolving environment variables
* @param propProvider the property provider to use for resolving system properties
* @param certConfigPathOverride optional override path for the configuration file
* @return true if mTLS should be enabled, false otherwise
* @throws IOException if the configuration file is present but contains missing or malformed
* files
*/
public static boolean canMtlsBeEnabled(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I’m not sure that cert being present == automatically use mTLS. They can be using different credentials / not using it at all. So then we’d be adding mTLS setup and calls for credentials that are not actually using it.

I think the decision should be based on the credential type, and perhaps expose some state from the credential that we can use to check if mTLS should happen for these calls.

EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
throws IOException {

// Check if client certificate usage is allowed
String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
if ("false".equalsIgnoreCase(useClientCertificate)) {
return false;
}

if (getMtlsEndpointUsagePolicy(envProvider) == MtlsEndpointUsagePolicy.NEVER) {
return false;
}

// Locate and process the certificate configuration file
String envPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
if (certConfigPathOverride != null || !Strings.isNullOrEmpty(envPath)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it possible for both certConfigPathOverride and envPath to be null/ empty? Do we need to check for that possibility?

File certConfigFile =
new File(certConfigPathOverride != null ? certConfigPathOverride : envPath);
if (!certConfigFile.isFile()) {
throw new CertificateSourceUnavailableException(
"Certificate configuration file does not exist or is not a file: "
+ certConfigFile.getAbsolutePath());
}
}

WorkloadCertificateConfiguration workloadCertConfig = null;
try {
workloadCertConfig =
getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride);
} catch (CertificateSourceUnavailableException e) {
// Config file is simply not present. This is fine, fallback to SPIFFE.
} catch (IOException e) {
// Config file exists but is malformed or points to invalid paths -> throw hard error
throw e;
Comment on lines +194 to +198
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For these cases, do we want to either log a message and/or make these errors actionable?

How will customers know that we have fallen back to SPIFFE or if there config malformed will the exception be clear enough for them to know that?

}

if (workloadCertConfig != null) {
// Validate referenced files exist
File certFile = new File(workloadCertConfig.getCertPath());
File keyFile = new File(workloadCertConfig.getPrivateKeyPath());
if (!certFile.isFile() || !keyFile.isFile()) {
throw new IOException(
String.format(
"Certificate configuration exists but referenced files are missing: cert_path=%s, key_path=%s",
workloadCertConfig.getCertPath(), workloadCertConfig.getPrivateKeyPath()));
}
return true;
}

// Fallback to SPIFFE discovery if the directory exists
File spiffeDir = new File(spiffeDirectory);
if (spiffeDir.isDirectory()) {
File credentialBundle = new File(spiffeDir, SPIFFE_CREDENTIAL_BUNDLE_FILE);
if (credentialBundle.isFile()) {
return true;
}
File certsFile = new File(spiffeDir, SPIFFE_CERTIFICATE_FILE);
File keyFile = new File(spiffeDir, SPIFFE_PRIVATE_KEY_FILE);
if (certsFile.isFile() && keyFile.isFile()) {
return true;
}
}
Comment on lines +215 to +226
Copy link
Copy Markdown
Member

@lqiu96 lqiu96 Jun 5, 2026

Choose a reason for hiding this comment

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

Tracing through the getWorkloadCertificateConfiguration's logic, it looks like it read the contents of the file and also does validation inside. Do we need to do similar here?

Hmm, is it possible to move the logic L201 - L212 into WorkloadCertificateConfiguration? That way if we return an object, all the configs inside are valid.

Side note: I'm wondering if we can break this into something like:

  1. canMtlsBeEnabled : boolean -> Checks the env vars and any configs that prevent us from using mtls
  2. loadMtlsConfigs : MtlsConfig -> Reads the workloadcert or SPIFFEE config file and returns all the info needed.


return false;
}

/**
* Returns the current mutual TLS endpoint usage policy.
*
* @param envProvider the environment provider to use for resolving environment variables
* @return the MtlsEndpointUsagePolicy enum value
*/
public static MtlsEndpointUsagePolicy getMtlsEndpointUsagePolicy(
EnvironmentProvider envProvider) {
String mtlsEndpointUsagePolicy = envProvider.getEnv("GOOGLE_API_USE_MTLS_ENDPOINT");
if ("never".equals(mtlsEndpointUsagePolicy)) {
return MtlsEndpointUsagePolicy.NEVER;
} else if ("always".equals(mtlsEndpointUsagePolicy)) {
return MtlsEndpointUsagePolicy.ALWAYS;
}
return MtlsEndpointUsagePolicy.AUTO;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.google.auth.oauth2.PropertyProvider;
import com.google.auth.oauth2.SystemEnvironmentProvider;
import com.google.auth.oauth2.SystemPropertyProvider;
import com.google.common.base.Strings;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
Expand Down Expand Up @@ -102,51 +103,83 @@ public X509Provider() {
*
* <ul>
* <li>The certificate config override path, if set.
* <li>The path pointed to by the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable
* <li>The path pointed to by the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable.
* <li>The well known gcloud location for the certificate configuration file.
* </ul>
*
* <p>If none of the above are available, it will attempt to discover and load certificates from
* SPIFFE credentials located under "/var/run/secrets/workload-spiffe-credentials/".
*
* @return a KeyStore containing the X.509 certificate specified by the certificate configuration.
* @throws CertificateSourceUnavailableException if the certificate source is unavailable (ex.
* missing configuration file)
* @throws IOException if a general I/O error occurs while creating the KeyStore
*/
@Override
public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
WorkloadCertificateConfiguration workloadCertConfig =
MtlsUtils.getWorkloadCertificateConfiguration(
envProvider, propProvider, certConfigPathOverride);
// Attempt to load from resolved Config File
WorkloadCertificateConfiguration workloadCertConfig = null;
try {
workloadCertConfig =
MtlsUtils.getWorkloadCertificateConfiguration(
envProvider, propProvider, certConfigPathOverride);
} catch (CertificateSourceUnavailableException e) {
// Ignore config-not-found error to fall back to SPIFFE discovery ONLY if not explicitly
// configured.
boolean isExplicitlyConfigured =
certConfigPathOverride != null
|| !Strings.isNullOrEmpty(
envProvider.getEnv(MtlsUtils.CERTIFICATE_CONFIGURATION_ENV_VARIABLE));
if (isExplicitlyConfigured) {
throw e;
}
}

// Read the certificate and private key file paths into streams.
try (InputStream certStream = new FileInputStream(new File(workloadCertConfig.getCertPath()));
InputStream privateKeyStream =
new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
SequenceInputStream certAndPrivateKeyStream =
new SequenceInputStream(certStream, privateKeyStream)) {
if (workloadCertConfig != null) {
try (InputStream certStream =
new FileInputStream(new File(workloadCertConfig.getCertPath()));
InputStream privateKeyStream =
new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
SequenceInputStream certAndPrivateKeyStream =
new SequenceInputStream(certStream, privateKeyStream)) {
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
} catch (Exception e) {
throw new IOException("X509Provider: Unexpected error loading from config file:", e);
}
}

// Build a key store using the combined stream.
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
} catch (CertificateSourceUnavailableException e) {
// Throw the CertificateSourceUnavailableException without wrapping.
throw e;
} catch (Exception e) {
// Wrap all other exception types to an IOException.
throw new IOException("X509Provider: Unexpected IOException:", e);
// Fallback: Load from SPIFFE Credentials
File spiffeDir = new File(MtlsUtils.spiffeDirectory);
if (spiffeDir.isDirectory()) {
File credentialBundle = new File(spiffeDir, MtlsUtils.SPIFFE_CREDENTIAL_BUNDLE_FILE);
if (credentialBundle.isFile()) {
try (InputStream bundleStream = new FileInputStream(credentialBundle)) {
return SecurityUtils.createMtlsKeyStore(bundleStream);
} catch (Exception e) {
throw new IOException("X509Provider: Unexpected error loading from SPIFFE bundle:", e);
}
}

File certsFile = new File(spiffeDir, MtlsUtils.SPIFFE_CERTIFICATE_FILE);
File keyFile = new File(spiffeDir, MtlsUtils.SPIFFE_PRIVATE_KEY_FILE);
if (certsFile.isFile() && keyFile.isFile()) {
try (InputStream certStream = new FileInputStream(certsFile);
InputStream privateKeyStream = new FileInputStream(keyFile);
SequenceInputStream certAndPrivateKeyStream =
new SequenceInputStream(certStream, privateKeyStream)) {
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
} catch (Exception e) {
throw new IOException(
"X509Provider: Unexpected error loading from separate SPIFFE files:", e);
}
}
}

throw new CertificateSourceUnavailableException("No certificate source was resolved.");
}

/**
* Returns true if the X509 mTLS provider is available.
*
* @throws IOException if a general I/O error occurs while determining availability.
*/
@Override
public boolean isAvailable() throws IOException {
try {
this.getKeyStore();
} catch (CertificateSourceUnavailableException e) {
return false;
}
return true;
return MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride);
}
Comment thread
vverman marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
import com.google.auth.RequestMetadataCallback;
import com.google.auth.http.AuthHttpConstants;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.mtls.MtlsHttpTransportFactory;
import com.google.auth.mtls.MtlsUtils;
import com.google.auth.mtls.X509Provider;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
Expand All @@ -51,6 +54,7 @@
import java.io.ObjectInputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -397,6 +401,20 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT
return;
}

try {
if (!(transportFactory instanceof MtlsHttpTransportFactory)
&& MtlsUtils.canMtlsBeEnabled(getEnvironmentProvider(), getPropertyProvider(), null)) {
X509Provider x509Provider =
new X509Provider(getEnvironmentProvider(), getPropertyProvider(), null);
KeyStore mtlsKeyStore = x509Provider.getKeyStore();
if (mtlsKeyStore != null) {
transportFactory = new MtlsHttpTransportFactory(mtlsKeyStore);
}
}
} catch (Exception e) {
// Graceful fallback to standard transport if mTLS initialization fails
}
Comment thread
vverman marked this conversation as resolved.

regionalAccessBoundaryManager.triggerAsyncRefresh(
transportFactory, (RegionalAccessBoundaryProvider) this, token);
}
Expand Down Expand Up @@ -843,6 +861,14 @@ HttpTransportFactory getTransportFactory() {
return null;
}

EnvironmentProvider getEnvironmentProvider() {
return SystemEnvironmentProvider.getInstance();
}

PropertyProvider getPropertyProvider() {
return SystemPropertyProvider.getInstance();
}

public static class Builder extends OAuth2Credentials.Builder {
@Nullable protected String quotaProjectId;
@Nullable protected String universeDomain;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ static RegionalAccessBoundary refresh(
throw new IllegalArgumentException("The provided access token is expired.");
}

if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe the transportFactory is a setter for a bunch of credentials. Is it possible to make this check a bit more robust and not based on the MtlsHttpTransportFactory class itself?

url = url.replace("iamcredentials.googleapis.com", "iamcredentials.mtls.googleapis.com");
}
Comment thread
vverman marked this conversation as resolved.

HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();
HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url));
// Disable automatic logging by google-http-java-client to prevent leakage of sensitive tokens.
Expand Down
Loading
Loading