Skip to content

Commit 2c53152

Browse files
committed
Added changes to always call mTLS RAB endpoint. Removed SPIFFE fallback.
1 parent 4c8b061 commit 2c53152

7 files changed

Lines changed: 111 additions & 226 deletions

File tree

google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,18 @@ public class MtlsUtils {
5151
static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
5252
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";
5353

54-
@com.google.common.annotations.VisibleForTesting
55-
static String spiffeDirectory = "/var/run/secrets/workload-spiffe-credentials/";
56-
57-
static final String SPIFFE_CREDENTIAL_BUNDLE_FILE = "credentialbundle.pem";
58-
static final String SPIFFE_CERTIFICATE_FILE = "certificates.pem";
59-
static final String SPIFFE_PRIVATE_KEY_FILE = "private_key.pem";
60-
54+
/**
55+
* The policy determining when to use mutual TLS (mTLS) endpoints.
56+
*
57+
* <p>See <a href="https://google.aip.dev/auth/4114">AIP-4114</a> for the specification on mTLS
58+
* endpoint usage.
59+
*/
6160
public enum MtlsEndpointUsagePolicy {
61+
/** Always use the mTLS endpoint, and fail if client certificates are not configured. */
6262
ALWAYS,
63+
/** Never use the mTLS endpoint. */
6364
NEVER,
65+
/** Use the mTLS endpoint if client certificates are configured (auto-discovery). */
6466
AUTO
6567
}
6668

@@ -167,13 +169,21 @@ public static boolean canMtlsBeEnabled(
167169

168170
// Check if client certificate usage is allowed
169171
String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
172+
MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider);
170173
if ("false".equalsIgnoreCase(useClientCertificate)) {
174+
if (policy == MtlsEndpointUsagePolicy.ALWAYS) {
175+
throw new CertificateSourceUnavailableException(
176+
"mTLS is configured to ALWAYS, but client certificate usage was explicitly disabled via GOOGLE_API_USE_CLIENT_CERTIFICATE=false.");
177+
}
171178
return false;
172179
}
173180

174-
if (getMtlsEndpointUsagePolicy(envProvider) == MtlsEndpointUsagePolicy.NEVER) {
181+
if (policy == MtlsEndpointUsagePolicy.NEVER) {
175182
return false;
176183
}
184+
if (policy == MtlsEndpointUsagePolicy.ALWAYS) {
185+
return true;
186+
}
177187

178188
// Locate and process the certificate configuration file
179189
String envPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
@@ -185,44 +195,16 @@ public static boolean canMtlsBeEnabled(
185195
"Certificate configuration file does not exist or is not a file: "
186196
+ certConfigFile.getAbsolutePath());
187197
}
188-
}
189-
190-
WorkloadCertificateConfiguration workloadCertConfig = null;
191-
try {
192-
workloadCertConfig =
193-
getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride);
194-
} catch (CertificateSourceUnavailableException e) {
195-
// Config file is simply not present. This is fine, fallback to SPIFFE.
196-
} catch (IOException e) {
197-
// Config file exists but is malformed or points to invalid paths -> throw hard error
198-
throw e;
199-
}
200-
201-
if (workloadCertConfig != null) {
202-
// Validate referenced files exist
203-
File certFile = new File(workloadCertConfig.getCertPath());
204-
File keyFile = new File(workloadCertConfig.getPrivateKeyPath());
205-
if (!certFile.isFile() || !keyFile.isFile()) {
206-
throw new IOException(
207-
String.format(
208-
"Certificate configuration exists but referenced files are missing: cert_path=%s, key_path=%s",
209-
workloadCertConfig.getCertPath(), workloadCertConfig.getPrivateKeyPath()));
210-
}
211198
return true;
212199
}
213200

214-
// Fallback to SPIFFE discovery if the directory exists
215-
File spiffeDir = new File(spiffeDirectory);
216-
if (spiffeDir.isDirectory()) {
217-
File credentialBundle = new File(spiffeDir, SPIFFE_CREDENTIAL_BUNDLE_FILE);
218-
if (credentialBundle.isFile()) {
219-
return true;
220-
}
221-
File certsFile = new File(spiffeDir, SPIFFE_CERTIFICATE_FILE);
222-
File keyFile = new File(spiffeDir, SPIFFE_PRIVATE_KEY_FILE);
223-
if (certsFile.isFile() && keyFile.isFile()) {
201+
try {
202+
File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
203+
if (wellKnownConfig.isFile()) {
224204
return true;
225205
}
206+
} catch (IOException e) {
207+
// ignore if well-known directory resolution fails (e.g. APPDATA not set on Windows)
226208
}
227209

228210
return false;

google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java

Lines changed: 11 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import com.google.auth.oauth2.PropertyProvider;
3737
import com.google.auth.oauth2.SystemEnvironmentProvider;
3838
import com.google.auth.oauth2.SystemPropertyProvider;
39-
import com.google.common.base.Strings;
4039
import java.io.File;
4140
import java.io.FileInputStream;
4241
import java.io.IOException;
@@ -107,9 +106,6 @@ public X509Provider() {
107106
* <li>The well known gcloud location for the certificate configuration file.
108107
* </ul>
109108
*
110-
* <p>If none of the above are available, it will attempt to discover and load certificates from
111-
* SPIFFE credentials located under "/var/run/secrets/workload-spiffe-credentials/".
112-
*
113109
* @return a KeyStore containing the X.509 certificate specified by the certificate configuration.
114110
* @throws CertificateSourceUnavailableException if the certificate source is unavailable (ex.
115111
* missing configuration file)
@@ -118,64 +114,19 @@ public X509Provider() {
118114
@Override
119115
public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
120116
// Attempt to load from resolved Config File
121-
WorkloadCertificateConfiguration workloadCertConfig = null;
122-
try {
123-
workloadCertConfig =
124-
MtlsUtils.getWorkloadCertificateConfiguration(
125-
envProvider, propProvider, certConfigPathOverride);
126-
} catch (CertificateSourceUnavailableException e) {
127-
// Ignore config-not-found error to fall back to SPIFFE discovery ONLY if not explicitly
128-
// configured.
129-
boolean isExplicitlyConfigured =
130-
certConfigPathOverride != null
131-
|| !Strings.isNullOrEmpty(
132-
envProvider.getEnv(MtlsUtils.CERTIFICATE_CONFIGURATION_ENV_VARIABLE));
133-
if (isExplicitlyConfigured) {
134-
throw e;
135-
}
136-
}
137-
138-
if (workloadCertConfig != null) {
139-
try (InputStream certStream =
140-
new FileInputStream(new File(workloadCertConfig.getCertPath()));
141-
InputStream privateKeyStream =
142-
new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
143-
SequenceInputStream certAndPrivateKeyStream =
144-
new SequenceInputStream(certStream, privateKeyStream)) {
145-
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
146-
} catch (Exception e) {
147-
throw new IOException("X509Provider: Unexpected error loading from config file:", e);
148-
}
149-
}
150-
151-
// Fallback: Load from SPIFFE Credentials
152-
File spiffeDir = new File(MtlsUtils.spiffeDirectory);
153-
if (spiffeDir.isDirectory()) {
154-
File credentialBundle = new File(spiffeDir, MtlsUtils.SPIFFE_CREDENTIAL_BUNDLE_FILE);
155-
if (credentialBundle.isFile()) {
156-
try (InputStream bundleStream = new FileInputStream(credentialBundle)) {
157-
return SecurityUtils.createMtlsKeyStore(bundleStream);
158-
} catch (Exception e) {
159-
throw new IOException("X509Provider: Unexpected error loading from SPIFFE bundle:", e);
160-
}
161-
}
117+
WorkloadCertificateConfiguration workloadCertConfig =
118+
MtlsUtils.getWorkloadCertificateConfiguration(
119+
envProvider, propProvider, certConfigPathOverride);
162120

163-
File certsFile = new File(spiffeDir, MtlsUtils.SPIFFE_CERTIFICATE_FILE);
164-
File keyFile = new File(spiffeDir, MtlsUtils.SPIFFE_PRIVATE_KEY_FILE);
165-
if (certsFile.isFile() && keyFile.isFile()) {
166-
try (InputStream certStream = new FileInputStream(certsFile);
167-
InputStream privateKeyStream = new FileInputStream(keyFile);
168-
SequenceInputStream certAndPrivateKeyStream =
169-
new SequenceInputStream(certStream, privateKeyStream)) {
170-
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
171-
} catch (Exception e) {
172-
throw new IOException(
173-
"X509Provider: Unexpected error loading from separate SPIFFE files:", e);
174-
}
175-
}
121+
try (InputStream certStream = new FileInputStream(new File(workloadCertConfig.getCertPath()));
122+
InputStream privateKeyStream =
123+
new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
124+
SequenceInputStream certAndPrivateKeyStream =
125+
new SequenceInputStream(certStream, privateKeyStream)) {
126+
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
127+
} catch (Exception e) {
128+
throw new IOException("X509Provider: Unexpected error loading from config file:", e);
176129
}
177-
178-
throw new CertificateSourceUnavailableException("No certificate source was resolved.");
179130
}
180131

181132
@Override

google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -401,17 +401,27 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT
401401
return;
402402
}
403403

404+
MtlsUtils.MtlsEndpointUsagePolicy mtlsPolicy =
405+
MtlsUtils.getMtlsEndpointUsagePolicy(getEnvironmentProvider());
404406
try {
405-
if (!(transportFactory instanceof MtlsHttpTransportFactory)
406-
&& MtlsUtils.canMtlsBeEnabled(getEnvironmentProvider(), getPropertyProvider(), null)) {
407+
boolean canMtls =
408+
MtlsUtils.canMtlsBeEnabled(getEnvironmentProvider(), getPropertyProvider(), null);
409+
// Initialize mTLS transport factory if mTLS can be enabled and the user hasn't already
410+
// configured a custom MtlsHttpTransportFactory.
411+
if (!(transportFactory instanceof MtlsHttpTransportFactory) && canMtls) {
407412
X509Provider x509Provider =
408413
new X509Provider(getEnvironmentProvider(), getPropertyProvider(), null);
409414
KeyStore mtlsKeyStore = x509Provider.getKeyStore();
410-
if (mtlsKeyStore != null) {
411-
transportFactory = new MtlsHttpTransportFactory(mtlsKeyStore);
412-
}
415+
transportFactory = new MtlsHttpTransportFactory(mtlsKeyStore);
413416
}
414417
} catch (Exception e) {
418+
if (mtlsPolicy == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) {
419+
if (e instanceof IOException) {
420+
throw (IOException) e;
421+
}
422+
throw new IOException(
423+
"mTLS is configured to ALWAYS, but initialization failed: " + e.getMessage(), e);
424+
}
415425
// Graceful fallback to standard transport if mTLS initialization fails
416426
}
417427

@@ -463,6 +473,10 @@ public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException
463473
// Sets off an async refresh for request-metadata.
464474
refreshRegionalAccessBoundaryIfExpired(uri, getAccessToken());
465475
} catch (IOException e) {
476+
if (MtlsUtils.getMtlsEndpointUsagePolicy(getEnvironmentProvider())
477+
== MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) {
478+
throw e;
479+
}
466480
// Ignore failure in async refresh trigger.
467481
}
468482
return metadata;
@@ -493,6 +507,11 @@ public void onSuccess(Map<String, List<String>> metadata) {
493507
try {
494508
refreshRegionalAccessBoundaryIfExpired(uri, getAccessToken());
495509
} catch (IOException e) {
510+
if (MtlsUtils.getMtlsEndpointUsagePolicy(getEnvironmentProvider())
511+
== MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) {
512+
callback.onFailure(e);
513+
return;
514+
}
496515
// Ignore failure in async refresh trigger.
497516
}
498517
callback.onSuccess(metadata);

google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundary.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import com.google.api.client.util.ExponentialBackOff;
4646
import com.google.api.client.util.Key;
4747
import com.google.auth.http.HttpTransportFactory;
48+
import com.google.auth.mtls.MtlsUtils;
4849
import com.google.common.base.MoreObjects;
4950
import com.google.common.base.Preconditions;
5051
import java.io.IOException;
@@ -189,7 +190,10 @@ static RegionalAccessBoundary refresh(
189190
throw new IllegalArgumentException("The provided access token is expired.");
190191
}
191192

192-
if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory) {
193+
MtlsUtils.MtlsEndpointUsagePolicy mtlsPolicy =
194+
MtlsUtils.getMtlsEndpointUsagePolicy(SystemEnvironmentProvider.getInstance());
195+
if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory
196+
|| mtlsPolicy == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) {
193197
url = url.replace("iamcredentials.googleapis.com", "iamcredentials.mtls.googleapis.com");
194198
}
195199

google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsUtilsTest.java

Lines changed: 12 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -351,81 +351,23 @@ public String getProperty(String name, String def) {
351351
assertTrue(MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null));
352352
}
353353

354-
// If the configuration file exists but the certificate path it references does not exist,
355-
// canMtlsBeEnabled should throw an IOException.
356354
@Test
357-
void canMtlsBeEnabled_configMissingCertFile_throwsIOException() throws IOException {
358-
Path configFile = tempDir.resolve("config.json");
359-
Path nonExistentCert = tempDir.resolve("non_existent_cert.pem");
360-
Path keyFile = tempDir.resolve("key.pem");
361-
Files.createFile(keyFile);
362-
Files.write(configFile, createJsonConfigString(nonExistentCert, keyFile).getBytes());
363-
364-
EnvironmentProvider envProvider =
365-
name -> "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) ? configFile.toString() : null;
366-
PropertyProvider propProvider = (name, def) -> def;
367-
368-
assertThrows(
369-
IOException.class, () -> MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null));
370-
}
371-
372-
// If the configuration file exists but the private key path it references does not exist,
373-
// canMtlsBeEnabled should throw an IOException.
374-
@Test
375-
void canMtlsBeEnabled_configMissingKeyFile_throwsIOException() throws IOException {
376-
Path configFile = tempDir.resolve("config.json");
377-
Path certFile = tempDir.resolve("cert.pem");
378-
Path nonExistentKey = tempDir.resolve("non_existent_key.pem");
379-
Files.createFile(certFile);
380-
Files.write(configFile, createJsonConfigString(certFile, nonExistentKey).getBytes());
381-
355+
void canMtlsBeEnabled_alwaysPolicy_clientCertDisabled_throwsException() {
382356
EnvironmentProvider envProvider =
383-
name -> "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) ? configFile.toString() : null;
357+
name -> {
358+
if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) {
359+
return "false";
360+
}
361+
if ("GOOGLE_API_USE_MTLS_ENDPOINT".equals(name)) {
362+
return "always";
363+
}
364+
return null;
365+
};
384366
PropertyProvider propProvider = (name, def) -> def;
385367

386368
assertThrows(
387-
IOException.class, () -> MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null));
388-
}
389-
390-
// If no configuration file exists but a SPIFFE credential bundle file is present,
391-
// canMtlsBeEnabled should return true.
392-
@Test
393-
void canMtlsBeEnabled_unset_spiffeBundlePresent_returnsTrue() throws IOException {
394-
Path spiffeDir = tempDir.resolve("spiffe_workload_bundle");
395-
Files.createDirectory(spiffeDir);
396-
Files.createFile(spiffeDir.resolve("credentialbundle.pem"));
397-
398-
String originalSpiffeDir = MtlsUtils.spiffeDirectory;
399-
MtlsUtils.spiffeDirectory = spiffeDir.toString() + "/";
400-
try {
401-
EnvironmentProvider envProvider = name -> null;
402-
PropertyProvider propProvider = (name, def) -> def;
403-
404-
assertTrue(MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null));
405-
} finally {
406-
MtlsUtils.spiffeDirectory = originalSpiffeDir;
407-
}
408-
}
409-
410-
// If no configuration file exists but separate SPIFFE certificate and key files are present,
411-
// canMtlsBeEnabled should return true.
412-
@Test
413-
void canMtlsBeEnabled_unset_spiffeCertsPresent_returnsTrue() throws IOException {
414-
Path spiffeDir = tempDir.resolve("spiffe_workload_certs");
415-
Files.createDirectory(spiffeDir);
416-
Files.createFile(spiffeDir.resolve("certificates.pem"));
417-
Files.createFile(spiffeDir.resolve("private_key.pem"));
418-
419-
String originalSpiffeDir = MtlsUtils.spiffeDirectory;
420-
MtlsUtils.spiffeDirectory = spiffeDir.toString() + "/";
421-
try {
422-
EnvironmentProvider envProvider = name -> null;
423-
PropertyProvider propProvider = (name, def) -> def;
424-
425-
assertTrue(MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null));
426-
} finally {
427-
MtlsUtils.spiffeDirectory = originalSpiffeDir;
428-
}
369+
CertificateSourceUnavailableException.class,
370+
() -> MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null));
429371
}
430372

431373
@Test

0 commit comments

Comments
 (0)