Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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() {
Comment thread
vverman marked this conversation as resolved.
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 @@ -31,14 +31,19 @@
package com.google.auth.mtls;

import com.google.api.core.InternalApi;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.EnvironmentProvider;
import com.google.auth.oauth2.OAuth2Utils;
import com.google.auth.oauth2.PropertyProvider;
import com.google.common.base.Strings;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.util.Locale;
import java.util.logging.Logger;
import javax.annotation.Nullable;

/**
* Utility class for mTLS related operations.
Expand All @@ -47,10 +52,27 @@
*/
@InternalApi
public class MtlsUtils {
private static final Logger LOGGER = Logger.getLogger(MtlsUtils.class.getName());

static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG";
static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";

/**
* The policy determining when to use mutual TLS (mTLS) endpoints.
*
* <p>See <a href="https://google.aip.dev/auth/4114">AIP-4114</a> for the specification on mTLS
* endpoint usage.
*/
public enum MtlsEndpointUsagePolicy {
/** Always use the mTLS endpoint, and fail if client certificates are not configured. */
ALWAYS,
/** Never use the mTLS endpoint. */
NEVER,
/** Use the mTLS endpoint if client certificates are configured (auto-discovery). */
AUTO
}
Comment thread
vverman marked this conversation as resolved.

private MtlsUtils() {
// Prevent instantiation for Utility class
}
Expand All @@ -76,38 +98,27 @@ public static String getCertificatePath(
}

/**
* Resolves and loads the workload certificate configuration.
* Resolves and parses the workload certificate configuration.
*
* <p>The configuration file is resolved in the following order of precedence: 1. The provided
* certConfigPathOverride (if not null). 2. The path specified by the
* GOOGLE_API_CERTIFICATE_CONFIG environment variable. 3. The well-known certificate configuration
* file in the gcloud config directory.
* <p>This locates the certificate configuration file via {@link #resolveCertificateConfigFile}
* and parses its contents into a {@link WorkloadCertificateConfiguration}.
*
* @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 the loaded WorkloadCertificateConfiguration
* @throws IOException if the configuration file cannot be found, read, or parsed
* @throws IOException if the configuration file cannot be resolved, read, or parsed
*/
static WorkloadCertificateConfiguration getWorkloadCertificateConfiguration(
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
throws IOException {
File certConfig;
if (certConfigPathOverride != null) {
certConfig = new File(certConfigPathOverride);
} else {
String envCredentialsPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
if (!Strings.isNullOrEmpty(envCredentialsPath)) {
certConfig = new File(envCredentialsPath);
} else {
certConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
}
}

if (!certConfig.isFile()) {
File certConfig =
resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride);
if (certConfig == null) {
File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
throw new CertificateSourceUnavailableException(
"Certificate configuration file does not exist or is not a file: "
+ certConfig.getAbsolutePath());
+ wellKnownConfig.getAbsolutePath());
}
try (InputStream certConfigStream = new FileInputStream(certConfig)) {
return WorkloadCertificateConfiguration.fromCertificateConfigurationStream(certConfigStream);
Expand Down Expand Up @@ -137,4 +148,172 @@ 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.

Comment thread
lqiu96 marked this conversation as resolved.
Outdated
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
throws IOException {

// Check if client certificate usage is allowed
String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider);
if ("false".equalsIgnoreCase(useClientCertificate)) {
if (policy == MtlsEndpointUsagePolicy.ALWAYS) {
throw new CertificateSourceUnavailableException(
"mTLS is configured to ALWAYS, but client certificate usage was explicitly disabled via GOOGLE_API_USE_CLIENT_CERTIFICATE=false.");
}
return false;
}

if (policy == MtlsEndpointUsagePolicy.NEVER) {
return false;
}
Comment thread
vverman marked this conversation as resolved.

if (policy == MtlsEndpointUsagePolicy.ALWAYS) {
return true;
}
Comment thread
vverman marked this conversation as resolved.

File certConfigFile =
resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride);
return certConfigFile != null;
}

/**
* Resolves the mutual TLS (mTLS) certificate configuration file.
*
* <p>The configuration file is resolved in the following order of precedence:
* <ol>
* <li>The developer-provided {@code certConfigPathOverride} (if not null).
* <li>The path specified by the {@code GOOGLE_API_CERTIFICATE_CONFIG} environment variable.
* <li>The well-known automatic gcloud workload identity provisioning location (via {@link
* #getWellKnownCertificateConfigFile}).
* </ol>
*
* <p>If an explicit configuration file is specified (via override or environment variable) and it
* is missing or invalid, an exception is thrown. If no explicit file is specified and the default
* well-known file is missing, {@code null} is returned.
*
* @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 the resolved File object, or null if no configuration was found
* @throws IOException if an explicit configuration file is missing or malformed
*/
@Nullable
static File resolveCertificateConfigFile(
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
throws IOException {
// 1. Check explicit developer override
if (certConfigPathOverride != null) {
File certConfigFile = new File(certConfigPathOverride);
if (!certConfigFile.isFile()) {
throw new CertificateSourceUnavailableException(
"Certificate configuration file does not exist or is not a file: "
+ certConfigFile.getAbsolutePath());
}
return certConfigFile;
}

// 2. Check explicit environment variable
String envPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
if (!Strings.isNullOrEmpty(envPath)) {
File certConfigFile = new File(envPath);
if (!certConfigFile.isFile()) {
throw new CertificateSourceUnavailableException(
"Certificate configuration file does not exist or is not a file: "
+ certConfigFile.getAbsolutePath());
}
return certConfigFile;
}

// 3. Check optional well-known automatic provisioning location
try {
File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
if (wellKnownConfig.isFile()) {
return wellKnownConfig;
}
} catch (IOException e) {
LOGGER.info(
"Could not get the mutual TLS (mTLS) client certificate configuration. The library will fall back to making standard non-mTLS requests.");
}

return null;
}

/**
* 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;
}

/**
* Prepares and upgrades the HTTP transport factory for mutual TLS (mTLS) if applicable.
*
* @param baseTransportFactory the base HTTP transport factory to upgrade
* @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 the mTLS-configured HTTP transport factory, or the base factory if mTLS is not enabled
* @throws IOException if mTLS is required/enabled but certificate initialization fails or an
* incompatible transport factory was provided
*/
public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled(
HttpTransportFactory baseTransportFactory,
EnvironmentProvider envProvider,
PropertyProvider propProvider,
String certConfigPathOverride)
throws IOException {

MtlsEndpointUsagePolicy mtlsPolicy = getMtlsEndpointUsagePolicy(envProvider);
try {
boolean canMtls = canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride);
Comment thread
lqiu96 marked this conversation as resolved.
Outdated
if (canMtls) {
Comment thread
vverman marked this conversation as resolved.
Outdated
if (baseTransportFactory instanceof MtlsHttpTransportFactory) {
// A custom MtlsHttpTransportFactory was already pre-configured by the user.
// Keep using it as-is without re-initializing.
return baseTransportFactory;
} else if (baseTransportFactory == OAuth2Utils.HTTP_TRANSPORT_FACTORY) {
// This is the default HttpTransportFactory assigned by credentials.
// Automatically discover and load client certificates to construct an mTLS factory.
X509Provider x509Provider =
new X509Provider(envProvider, propProvider, certConfigPathOverride);
KeyStore mtlsKeyStore = x509Provider.getKeyStore();
return new MtlsHttpTransportFactory(mtlsKeyStore);
} else {
// A user configured non-mTLS HttpTransportFactory was explicitly injected.
// Reject it to avoid bypassing mTLS enforcement or overriding the user's factory.
throw new IOException(
"mTLS is enabled on the system, but a user configured non-mTLS HttpTransportFactory was provided: "
+ baseTransportFactory.getClass().getName());
}
}
} catch (Exception e) {
if (mtlsPolicy == MtlsEndpointUsagePolicy.ALWAYS) {
throw new IOException(
"mTLS is configured to ALWAYS, but initialization failed: " + e.getMessage(), e);
}
// Graceful fallback to standard transport if mTLS initialization fails under AUTO policy
}
return baseTransportFactory;
}
Comment thread
vverman marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ 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>
*
Expand All @@ -113,40 +113,24 @@ public X509Provider() {
*/
@Override
public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
// Attempt to load from resolved Config File
WorkloadCertificateConfiguration workloadCertConfig =
MtlsUtils.getWorkloadCertificateConfiguration(
envProvider, propProvider, certConfigPathOverride);

// 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)) {

// 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);
throw new IOException("X509Provider: Unexpected error loading from config file:", e);
}
}
Comment thread
vverman marked this conversation as 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();
Comment thread
vverman marked this conversation as resolved.
} catch (CertificateSourceUnavailableException e) {
return false;
}
return true;
return MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import com.google.auth.RequestMetadataCallback;
import com.google.auth.http.AuthHttpConstants;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.mtls.MtlsUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
Expand Down Expand Up @@ -397,6 +398,11 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT
return;
}

// Automatically discover certificates or enforce mTLS policy if applicable
transportFactory =
MtlsUtils.prepareTransportFactoryIfMtlsEnabled(
transportFactory, getEnvironmentProvider(), getPropertyProvider(), null);
Comment thread
lqiu96 marked this conversation as resolved.
Outdated

regionalAccessBoundaryManager.triggerAsyncRefresh(
transportFactory, (RegionalAccessBoundaryProvider) this, token);
}
Expand Down Expand Up @@ -445,6 +451,10 @@ public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException
// Sets off an async refresh for request-metadata.
refreshRegionalAccessBoundaryIfExpired(uri, getAccessToken());
} catch (IOException e) {
if (MtlsUtils.getMtlsEndpointUsagePolicy(getEnvironmentProvider())
Comment thread
vverman marked this conversation as resolved.
Outdated
== MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) {
throw e;
}
// Ignore failure in async refresh trigger.
}
return metadata;
Expand Down Expand Up @@ -475,6 +485,11 @@ public void onSuccess(Map<String, List<String>> metadata) {
try {
refreshRegionalAccessBoundaryIfExpired(uri, getAccessToken());
} catch (IOException e) {
if (MtlsUtils.getMtlsEndpointUsagePolicy(getEnvironmentProvider())
== MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) {
callback.onFailure(e);
return;
}
// Ignore failure in async refresh trigger.
}
callback.onSuccess(metadata);
Expand Down Expand Up @@ -843,6 +858,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
Loading
Loading