Skip to content
Draft
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 @@ -51,6 +51,12 @@ 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";

private MtlsUtils() {
// Prevent instantiation for Utility class
}
Expand Down Expand Up @@ -137,4 +143,73 @@ 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(
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;
}

// Locate and process the certificate configuration file
String envPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
if (certConfigPathOverride != null || !Strings.isNullOrEmpty(envPath)) {
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;
}

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

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,41 +112,64 @@ public X509Provider() {
* @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);

// 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.
public boolean isAvailable() throws IOException {
try {
return MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride);
} catch (IOException e) {
// Broken configuration state defaults to throwing a failure
throw e;
} catch (Exception e) {
// Wrap all other exception types to an IOException.
throw new IOException("X509Provider: Unexpected IOException:", e);
}
}

/**
* 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 {
public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
if (!MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride)) {
throw new CertificateSourceUnavailableException("mTLS is not enabled or cannot be established.");
}

// 1. Attempt to load from resolved Config File
WorkloadCertificateConfiguration workloadCertConfig = null;
try {
this.getKeyStore();
} catch (CertificateSourceUnavailableException e) {
return false;
workloadCertConfig = MtlsUtils.getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride);
} catch (IOException e) {
// Ignore configuration file errors here to fall back to SPIFFE discovery
}

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);
}
}

// 2. 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);
}
}
}
return true;

throw new CertificateSourceUnavailableException("mTLS is enabled, but no certificate source was resolved.");
}
Comment on lines +125 to 174
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.

medium

The getKeyStore() method currently calls MtlsUtils.canMtlsBeEnabled(...) at the beginning, which performs redundant file existence checks and parses the JSON configuration file. Immediately after, it calls MtlsUtils.getWorkloadCertificateConfiguration(...) which parses the same JSON configuration file again. This results in unnecessary file I/O and CPU overhead.

We can optimize this by directly attempting to load the configuration and credentials, and throwing CertificateSourceUnavailableException if mTLS is not enabled or cannot be established. This avoids duplicate file checks and JSON parsing.

  public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
    String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
    if ("false".equalsIgnoreCase(useClientCertificate)) {
      throw new CertificateSourceUnavailableException("mTLS is explicitly disabled via GOOGLE_API_USE_CLIENT_CERTIFICATE.");
    }

    // 1. Attempt to load from resolved Config File
    WorkloadCertificateConfiguration workloadCertConfig = null;
    try {
      workloadCertConfig = MtlsUtils.getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride);
    } catch (CertificateSourceUnavailableException e) {
      // Config file is simply not present. This is fine, fallback to SPIFFE.
    }

    if (workloadCertConfig != null) {
      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()));
      }
      try (InputStream certStream = new FileInputStream(certFile);
          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 config file:", e);
      }
    }

    // 2. 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("mTLS is enabled, but no certificate source was resolved.");
  }
References
  1. When implementing property parsing or validation logic, ensure that null checks and validation steps are not redundant with checks already performed by upstream callers or preceding logic in the same method.

}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
import com.google.auth.mtls.MtlsHttpTransportFactory;
import com.google.auth.mtls.MtlsUtils;
import com.google.auth.mtls.X509Provider;
import java.security.KeyStore;


/** Base type for credentials for authorizing calls to Google APIs using OAuth2. */
public class GoogleCredentials extends OAuth2Credentials implements QuotaProjectIdProvider {
Expand Down Expand Up @@ -397,6 +402,24 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT
return;
}

try {
if (MtlsUtils.canMtlsBeEnabled(
SystemEnvironmentProvider.getInstance(),
SystemPropertyProvider.getInstance(),
null)) {
X509Provider x509Provider = new X509Provider(
SystemEnvironmentProvider.getInstance(),
SystemPropertyProvider.getInstance(),
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 on lines +405 to +421
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.

medium

Calling MtlsUtils.canMtlsBeEnabled(...) here is redundant because x509Provider.getKeyStore() already internally checks if mTLS can be enabled and throws a CertificateSourceUnavailableException if it cannot. Removing this redundant check avoids duplicate file I/O and JSON parsing during credential initialization/refresh.

    try {
      X509Provider x509Provider = new X509Provider(
          SystemEnvironmentProvider.getInstance(),
          SystemPropertyProvider.getInstance(),
          null);
      KeyStore mtlsKeyStore = x509Provider.getKeyStore();
      if (mtlsKeyStore != null) {
        transportFactory = new MtlsHttpTransportFactory(mtlsKeyStore);
      }
    } catch (Exception e) {
      // Graceful fallback to standard transport if mTLS initialization fails
    }
References
  1. When implementing property parsing or validation logic, ensure that null checks and validation steps are not redundant with checks already performed by upstream callers or preceding logic in the same method.


regionalAccessBoundaryManager.triggerAsyncRefresh(
transportFactory, (RegionalAccessBoundaryProvider) this, token);
}
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) {
url = url.replace("https://iamcredentials.googleapis.com/", "https://iamcredentials.mtls.googleapis.com/");
}
Comment on lines +192 to +194
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.

medium

Replacing the entire prefix https://iamcredentials.googleapis.com/ is fragile because it depends on the exact scheme and trailing slash. It is more robust to replace only the host name iamcredentials.googleapis.com with iamcredentials.mtls.googleapis.com.

Suggested change
if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory) {
url = url.replace("https://iamcredentials.googleapis.com/", "https://iamcredentials.mtls.googleapis.com/");
}
if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory) {
url = url.replace("iamcredentials.googleapis.com", "iamcredentials.mtls.googleapis.com");
}


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