Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.util.Locale;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
* Utility class for mTLS related operations.
Expand All @@ -57,6 +59,129 @@ private MtlsUtils() {
// Prevent instantiation for Utility class
}

/**
* Returns if mutual TLS client certificate should be used. Delegates directly to
* getWorkloadCertPath to avoid duplicate logic.
*/
public static boolean useMtlsClientCertificate(
EnvironmentProvider envProvider, PropertyProvider propProvider) {
return getWorkloadCertPath(envProvider, propProvider) != null;
}

/**
* Resolves and returns the path to the mutual TLS client certificate, or null if none should be
* used.
*/
public static @Nullable String getWorkloadCertPath(
EnvironmentProvider envProvider, PropertyProvider propProvider) {
String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
if ("false".equalsIgnoreCase(useClientCertificate)) {
return null;
}

String certConfigPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
if (!Strings.isNullOrEmpty(certConfigPath)) {
try {
WorkloadCertificateConfiguration config =
getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPath);

File certFile = new File(config.getCertPath());
File keyFile = new File(config.getPrivateKeyPath());
if (!certFile.exists() || !keyFile.exists()) {
throw new IllegalStateException(
"Certificate config points to certificate/key files that do not exist on disk: "
+ "cert_path="
+ config.getCertPath()
+ ", key_path="
+ config.getPrivateKeyPath());
}
return config.getCertPath();
} catch (CertificateSourceUnavailableException e) {
// Certificate config file does not exist on disk -> safe fallback
} catch (IllegalStateException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException("Failed to parse certificate config: " + certConfigPath, e);
}
} else {
try {
WorkloadCertificateConfiguration config =
getWorkloadCertificateConfiguration(envProvider, propProvider, null);
File certFile = new File(config.getCertPath());
File keyFile = new File(config.getPrivateKeyPath());
if (certFile.exists() && keyFile.exists()) {
return config.getCertPath();
}
} catch (CertificateSourceUnavailableException e) {
// Well-known gcloud certificate_config.json does not exist. Safe fallback to
// SPIFFE/well-known paths.
} catch (Exception e) {
// Ignore parsing errors for well-known config fallback
}
}

String gkeCertPath = getGkeWorkloadCertPath();
if (gkeCertPath != null) {
return gkeCertPath;
}

String gceCertPath = getGceWorkloadCertPath();
if (gceCertPath != null) {
return gceCertPath;
}

return null;
}

/** Dedicated GKE Fallback Resolution Path */
public static @Nullable String getGkeWorkloadCertPath() {
String gkePath = "/var/run/secrets/workload-spiffe-credentials";
File bundleFile = new File(gkePath, "credentialbundle.pem");
if (bundleFile.exists()) {
return bundleFile.getAbsolutePath();
}
return null;
}

/** Dedicated GCE Fallback Resolution Path */
public static @Nullable String getGceWorkloadCertPath() {
String gcePath = "/var/run/secrets/workload-spiffe-credentials";
File certFile = new File(gcePath, "certificates.pem");
File keyFile = new File(gcePath, "private_key.pem");
if (certFile.exists() && keyFile.exists()) {
return certFile.getAbsolutePath();
}
return null;
}

/** Centralized SHA-256 Fingerprint Calculator */
public static @Nullable String getCertificateFingerprint(@Nullable String certPath) {
if (certPath == null) {
return null;
}
File file = new File(certPath);
if (!file.exists()) {
return null;
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (FileInputStream fis = new FileInputStream(file)) {
byte[] byteArray = new byte[1024];
int bytesCount;
while ((bytesCount = fis.read(byteArray)) != -1) {
digest.update(byteArray, 0, bytesCount);
}
}
StringBuilder sb = new StringBuilder();
for (byte b : digest.digest()) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
return null;
}
}

/**
* Returns the path to the client certificate file specified by the loaded workload certificate
* configuration.
Expand All @@ -65,14 +190,17 @@ private MtlsUtils() {
* @throws IOException if the certificate configuration cannot be found or loaded.
*/
public static String getCertificatePath(
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
EnvironmentProvider envProvider,
PropertyProvider propProvider,
@Nullable String certConfigPathOverride)
throws IOException {
String certPath =
getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride)
.getCertPath();
if (Strings.isNullOrEmpty(certPath)) {
throw new CertificateSourceUnavailableException(
"Certificate configuration loaded successfully, but does not contain a 'certificate_file' path.");
"Certificate configuration loaded successfully, but does not contain a 'certificate_file'"
+ " path.");
}
return certPath;
}
Expand All @@ -92,7 +220,9 @@ public static String getCertificatePath(
* @throws IOException if the configuration file cannot be found, read, or parsed
*/
static WorkloadCertificateConfiguration getWorkloadCertificateConfiguration(
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
EnvironmentProvider envProvider,
PropertyProvider propProvider,
@Nullable String certConfigPathOverride)
throws IOException {
File certConfig;
if (certConfigPathOverride != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,95 @@ public String getProperty(String name, String def) {

assertEquals("APPDATA environment variable is not set on Windows.", exception.getMessage());
}

@Test
void useMtlsClientCertificate_trueWithNoCertsOnDisk_returnsFalseWithoutThrowing() {
EnvironmentProvider envProvider =
name -> "GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name) ? "true" : null;
PropertyProvider propProvider = (name, def) -> def;

assertFalse(MtlsUtils.useMtlsClientCertificate(envProvider, propProvider));
assertNull(MtlsUtils.getWorkloadCertPath(envProvider, propProvider));
}

@Test
void useMtlsClientCertificate_false_returnsFalse() {
EnvironmentProvider envProvider =
name -> "GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name) ? "false" : null;
PropertyProvider propProvider = (name, def) -> def;

assertFalse(MtlsUtils.useMtlsClientCertificate(envProvider, propProvider));
assertNull(MtlsUtils.getWorkloadCertPath(envProvider, propProvider));
}

@Test
void getWorkloadCertPath_missingConfigFile_returnsNullSafely() {
EnvironmentProvider envProvider =
name -> "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) ? "/nonexistent/config.json" : null;
PropertyProvider propProvider = (name, def) -> def;

assertNull(MtlsUtils.getWorkloadCertPath(envProvider, propProvider));
}

@Test
void getWorkloadCertPath_configPointsToMissingCertFiles_throwsIllegalStateException()
throws IOException {
Path configFile = tempDir.resolve("config.json");
Files.write(
configFile,
"{\"cert_configs\":{\"workload\":{\"cert_path\":\"/nonexistent/cert.pem\",\"key_path\":\"/nonexistent/key.pem\"}}}"
.getBytes());

EnvironmentProvider envProvider =
name -> "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) ? configFile.toString() : null;
PropertyProvider propProvider = (name, def) -> def;

IllegalStateException exception =
assertThrows(
IllegalStateException.class,
() -> MtlsUtils.getWorkloadCertPath(envProvider, propProvider));
assertTrue(exception.getMessage().contains("files that do not exist on disk"));
}

@Test
void getWorkloadCertPath_validConfig_returnsCertPath() throws IOException {
Path certFile = tempDir.resolve("cert.pem");
Path keyFile = tempDir.resolve("key.pem");
Files.write(certFile, "dummy cert".getBytes());
Files.write(keyFile, "dummy key".getBytes());

Path configFile = tempDir.resolve("config.json");
String configJson =
String.format(
"{\"cert_configs\":{\"workload\":{\"cert_path\":\"%s\",\"key_path\":\"%s\"}}}",
certFile.toString().replace("\\", "\\\\"), keyFile.toString().replace("\\", "\\\\"));
Files.write(configFile, configJson.getBytes());

EnvironmentProvider envProvider =
name -> "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) ? configFile.toString() : null;
PropertyProvider propProvider = (name, def) -> def;

assertTrue(MtlsUtils.useMtlsClientCertificate(envProvider, propProvider));
assertEquals(certFile.toString(), MtlsUtils.getWorkloadCertPath(envProvider, propProvider));
}

@Test
void getCertificateFingerprint_validFile_returnsSha256() throws IOException {
Path file = tempDir.resolve("test.crt");
Files.write(file, "hello world".getBytes());

String fingerprint = MtlsUtils.getCertificateFingerprint(file.toString());
assertNotNull(fingerprint);
assertEquals(64, fingerprint.length()); // SHA-256 hex string length
}

@Test
void getGkeWorkloadCertPath_nonexistent_returnsNull() {
assertNull(MtlsUtils.getGkeWorkloadCertPath());
}

@Test
void getGceWorkloadCertPath_nonexistent_returnsNull() {
assertNull(MtlsUtils.getGceWorkloadCertPath());
}
}
3 changes: 1 addition & 2 deletions sdk-platform-java/gax-java/gax-grpc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,7 @@
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- These tests require an Env Var to be set. Use -PenvVarTest to ONLY run these tests -->
<test>!InstantiatingGrpcChannelProviderTest#testLogDirectPathMisconfig_AttemptDirectPathNotSetAndAttemptDirectPathXdsSetViaEnv_warns,!InstantiatingGrpcChannelProviderTest#canUseDirectPath_directPathEnvVarNotSet_attemptDirectPathIsTrue,InstantiatingGrpcChannelProviderTest#testLogDirectPathMisconfigWrongCredential</test>
<!-- <test>!InstantiatingGrpcChannelProviderTest#testLogDirectPathMisconfig_AttemptDirectPathNotSetAndAttemptDirectPathXdsSetViaEnv_warns</test> -->
<test>!InstantiatingGrpcChannelProviderTest#testLogDirectPathMisconfig_AttemptDirectPathNotSetAndAttemptDirectPathXdsSetViaEnv_warns</test>
</configuration>
</plugin>
<plugin>
Expand Down
Loading
Loading