Skip to content

Commit 7189bbb

Browse files
vvermanlqiu96
authored andcommitted
chore: Refactor x509Provider to create a shared Utils class for Mtls (#1907)
* feat: Refactor X509Provider * Added back accidentally removed tests. * Lint fixes * Addressed the PR comments. * Test for windows OS cert default path * lint fixes. * Added Copyright headers. * Addressed comments. * Test fix. * Added separate test class for mTLS-utils. Added missing cert and malformed cert tests to X509Provider. * Lint fix. Original-PR: googleapis/google-auth-library-java#1907
1 parent bf926fb commit 7189bbb

17 files changed

+894
-296
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
package com.google.auth.mtls;
32+
33+
import com.google.api.core.InternalApi;
34+
import com.google.auth.oauth2.EnvironmentProvider;
35+
import com.google.auth.oauth2.PropertyProvider;
36+
import com.google.common.base.Strings;
37+
import java.io.File;
38+
import java.io.FileInputStream;
39+
import java.io.IOException;
40+
import java.io.InputStream;
41+
import java.util.Locale;
42+
43+
/**
44+
* Utility class for mTLS related operations.
45+
*
46+
* <p>For internal use only.
47+
*/
48+
@InternalApi
49+
public class MtlsUtils {
50+
static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG";
51+
static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
52+
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";
53+
54+
private MtlsUtils() {
55+
// Prevent instantiation for Utility class
56+
}
57+
58+
/**
59+
* Returns the path to the client certificate file specified by the loaded workload certificate
60+
* configuration.
61+
*
62+
* @return The path to the certificate file.
63+
* @throws IOException if the certificate configuration cannot be found or loaded.
64+
*/
65+
public static String getCertificatePath(
66+
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
67+
throws IOException {
68+
String certPath =
69+
getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride)
70+
.getCertPath();
71+
if (Strings.isNullOrEmpty(certPath)) {
72+
throw new CertificateSourceUnavailableException(
73+
"Certificate configuration loaded successfully, but does not contain a 'certificate_file' path.");
74+
}
75+
return certPath;
76+
}
77+
78+
/**
79+
* Resolves and loads the workload certificate configuration.
80+
*
81+
* <p>The configuration file is resolved in the following order of precedence: 1. The provided
82+
* certConfigPathOverride (if not null). 2. The path specified by the
83+
* GOOGLE_API_CERTIFICATE_CONFIG environment variable. 3. The well-known certificate configuration
84+
* file in the gcloud config directory.
85+
*
86+
* @param envProvider the environment provider to use for resolving environment variables
87+
* @param propProvider the property provider to use for resolving system properties
88+
* @param certConfigPathOverride optional override path for the configuration file
89+
* @return the loaded WorkloadCertificateConfiguration
90+
* @throws IOException if the configuration file cannot be found, read, or parsed
91+
*/
92+
static WorkloadCertificateConfiguration getWorkloadCertificateConfiguration(
93+
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
94+
throws IOException {
95+
File certConfig;
96+
if (certConfigPathOverride != null) {
97+
certConfig = new File(certConfigPathOverride);
98+
} else {
99+
String envCredentialsPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
100+
if (!Strings.isNullOrEmpty(envCredentialsPath)) {
101+
certConfig = new File(envCredentialsPath);
102+
} else {
103+
certConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
104+
}
105+
}
106+
107+
if (!certConfig.isFile()) {
108+
throw new CertificateSourceUnavailableException(
109+
"Certificate configuration file does not exist or is not a file: "
110+
+ certConfig.getAbsolutePath());
111+
}
112+
try (InputStream certConfigStream = new FileInputStream(certConfig)) {
113+
return WorkloadCertificateConfiguration.fromCertificateConfigurationStream(certConfigStream);
114+
}
115+
}
116+
117+
private static File getWellKnownCertificateConfigFile(
118+
EnvironmentProvider envProvider, PropertyProvider propProvider) throws IOException {
119+
File cloudConfigPath;
120+
String envPath = envProvider.getEnv("CLOUDSDK_CONFIG");
121+
if (envPath != null) {
122+
cloudConfigPath = new File(envPath);
123+
} else {
124+
String osName = propProvider.getProperty("os.name", "").toLowerCase(Locale.US);
125+
if (osName.indexOf("windows") >= 0) {
126+
String appData = envProvider.getEnv("APPDATA");
127+
if (Strings.isNullOrEmpty(appData)) {
128+
throw new CertificateSourceUnavailableException(
129+
"APPDATA environment variable is not set on Windows.");
130+
}
131+
File appDataPath = new File(appData);
132+
cloudConfigPath = new File(appDataPath, CLOUDSDK_CONFIG_DIRECTORY);
133+
} else {
134+
File configPath = new File(propProvider.getProperty("user.home", ""), ".config");
135+
cloudConfigPath = new File(configPath, CLOUDSDK_CONFIG_DIRECTORY);
136+
}
137+
}
138+
return new File(cloudConfigPath, WELL_KNOWN_CERTIFICATE_CONFIG_FILE);
139+
}
140+
}

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

Lines changed: 34 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -31,40 +31,61 @@
3131
package com.google.auth.mtls;
3232

3333
import com.google.api.client.util.SecurityUtils;
34-
import com.google.common.base.Strings;
34+
import com.google.api.core.InternalApi;
35+
import com.google.auth.oauth2.EnvironmentProvider;
36+
import com.google.auth.oauth2.PropertyProvider;
37+
import com.google.auth.oauth2.SystemEnvironmentProvider;
38+
import com.google.auth.oauth2.SystemPropertyProvider;
3539
import java.io.File;
3640
import java.io.FileInputStream;
37-
import java.io.FileNotFoundException;
3841
import java.io.IOException;
3942
import java.io.InputStream;
4043
import java.io.SequenceInputStream;
4144
import java.security.KeyStore;
42-
import java.util.Locale;
4345

4446
/**
4547
* This class implements {@link MtlsProvider} for the Google Auth library transport layer via {@link
4648
* WorkloadCertificateConfiguration}. This is only meant to be used internally by Google Cloud
4749
* libraries, and the public facing methods may be changed without notice, and have no guarantee of
4850
* backwards compatibility.
4951
*/
52+
@InternalApi
5053
public class X509Provider implements MtlsProvider {
51-
static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG";
52-
static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
53-
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";
54-
54+
private final EnvironmentProvider envProvider;
55+
private final PropertyProvider propProvider;
5556
private final String certConfigPathOverride;
5657

5758
/**
5859
* Creates an X509 provider with an override path for the certificate configuration, bypassing the
5960
* normal checks for the well known certificate configuration file path and environment variable.
6061
* This is meant for internal Google Cloud usage and behavior may be changed without warning.
6162
*
63+
* @param envProvider environment provider used for environment variables
64+
* @param propProvider property provider used for system properties
6265
* @param certConfigPathOverride the path to read the certificate configuration from.
6366
*/
64-
public X509Provider(String certConfigPathOverride) {
67+
@InternalApi
68+
public X509Provider(
69+
EnvironmentProvider envProvider,
70+
PropertyProvider propProvider,
71+
String certConfigPathOverride) {
72+
this.envProvider = envProvider;
73+
this.propProvider = propProvider;
6574
this.certConfigPathOverride = certConfigPathOverride;
6675
}
6776

77+
/**
78+
* Creates an X509 provider with an override path for the certificate configuration.
79+
*
80+
* @param certConfigPathOverride the path to read the certificate configuration from.
81+
*/
82+
public X509Provider(String certConfigPathOverride) {
83+
this(
84+
SystemEnvironmentProvider.getInstance(),
85+
SystemPropertyProvider.getInstance(),
86+
certConfigPathOverride);
87+
}
88+
6889
/**
6990
* Creates a new X.509 provider that will check the environment variable path and the well known
7091
* Gcloud certificate configuration location. This is meant for internal Google Cloud usage and
@@ -74,29 +95,6 @@ public X509Provider() {
7495
this(null);
7596
}
7697

77-
/**
78-
* Returns the path to the client certificate file specified by the loaded workload certificate
79-
* configuration.
80-
*
81-
* <p>If the configuration has not been loaded yet (e.g., if {@link #getKeyStore()} has not been
82-
* called), this method will attempt to load it first by searching the override path, environment
83-
* variable, and well-known locations.
84-
*
85-
* @return The path to the certificate file.
86-
* @throws IOException if the certificate configuration cannot be found or loaded, or if the
87-
* configuration file does not specify a certificate path.
88-
* @throws CertificateSourceUnavailableException if the configuration file is not found.
89-
*/
90-
public String getCertificatePath() throws IOException {
91-
String certPath = getWorkloadCertificateConfiguration().getCertPath();
92-
if (Strings.isNullOrEmpty(certPath)) {
93-
// Ensure the loaded configuration actually contains the required path.
94-
throw new CertificateSourceUnavailableException(
95-
"Certificate configuration loaded successfully, but does not contain a 'certificate_file' path.");
96-
}
97-
return certPath;
98-
}
99-
10098
/**
10199
* Finds the certificate configuration file, then builds a Keystore using the X.509 certificate
102100
* and private key pointed to by the configuration. This will check the following locations in
@@ -115,12 +113,14 @@ public String getCertificatePath() throws IOException {
115113
*/
116114
@Override
117115
public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
118-
WorkloadCertificateConfiguration workloadCertConfig = getWorkloadCertificateConfiguration();
116+
WorkloadCertificateConfiguration workloadCertConfig =
117+
MtlsUtils.getWorkloadCertificateConfiguration(
118+
envProvider, propProvider, certConfigPathOverride);
119119

120120
// Read the certificate and private key file paths into streams.
121-
try (InputStream certStream = createInputStream(new File(workloadCertConfig.getCertPath()));
121+
try (InputStream certStream = new FileInputStream(new File(workloadCertConfig.getCertPath()));
122122
InputStream privateKeyStream =
123-
createInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
123+
new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
124124
SequenceInputStream certAndPrivateKeyStream =
125125
new SequenceInputStream(certStream, privateKeyStream)) {
126126

@@ -149,74 +149,4 @@ public boolean isAvailable() throws IOException {
149149
}
150150
return true;
151151
}
152-
153-
private WorkloadCertificateConfiguration getWorkloadCertificateConfiguration()
154-
throws IOException {
155-
File certConfig;
156-
if (this.certConfigPathOverride != null) {
157-
certConfig = new File(certConfigPathOverride);
158-
} else {
159-
String envCredentialsPath = getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
160-
if (!Strings.isNullOrEmpty(envCredentialsPath)) {
161-
certConfig = new File(envCredentialsPath);
162-
} else {
163-
certConfig = getWellKnownCertificateConfigFile();
164-
}
165-
}
166-
InputStream certConfigStream = null;
167-
try {
168-
if (!isFile(certConfig)) {
169-
// Path will be put in the message from the catch block below
170-
throw new CertificateSourceUnavailableException("File does not exist.");
171-
}
172-
certConfigStream = createInputStream(certConfig);
173-
return WorkloadCertificateConfiguration.fromCertificateConfigurationStream(certConfigStream);
174-
} finally {
175-
if (certConfigStream != null) {
176-
certConfigStream.close();
177-
}
178-
}
179-
}
180-
181-
/*
182-
* Start of methods to allow overriding in the test code to isolate from the environment.
183-
*/
184-
boolean isFile(File file) {
185-
return file.isFile();
186-
}
187-
188-
InputStream createInputStream(File file) throws FileNotFoundException {
189-
return new FileInputStream(file);
190-
}
191-
192-
String getEnv(String name) {
193-
return System.getenv(name);
194-
}
195-
196-
String getOsName() {
197-
return getProperty("os.name", "").toLowerCase(Locale.US);
198-
}
199-
200-
String getProperty(String property, String def) {
201-
return System.getProperty(property, def);
202-
}
203-
204-
/*
205-
* End of methods to allow overriding in the test code to isolate from the environment.
206-
*/
207-
208-
private File getWellKnownCertificateConfigFile() {
209-
File cloudConfigPath;
210-
String envPath = getEnv("CLOUDSDK_CONFIG");
211-
if (envPath != null) {
212-
cloudConfigPath = new File(envPath);
213-
} else if (getOsName().indexOf("windows") >= 0) {
214-
File appDataPath = new File(getEnv("APPDATA"));
215-
cloudConfigPath = new File(appDataPath, CLOUDSDK_CONFIG_DIRECTORY);
216-
} else {
217-
File configPath = new File(getProperty("user.home", ""), ".config");
218-
cloudConfigPath = new File(configPath, CLOUDSDK_CONFIG_DIRECTORY);
219-
}
220-
return new File(cloudConfigPath, WELL_KNOWN_CERTIFICATE_CONFIG_FILE);
221-
}
222152
}

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,6 @@ String getRegionalCredentialVerificationUrl() {
196196
return this.regionalCredentialVerificationUrl;
197197
}
198198

199-
@VisibleForTesting
200-
String getEnv(String name) {
201-
return System.getenv(name);
202-
}
203-
204199
@VisibleForTesting
205200
AwsSecurityCredentialsSupplier getAwsSecurityCredentialsSupplier() {
206201
return this.awsSecurityCredentialsSupplier;
Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,43 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
131
package com.google.auth.oauth2;
232

3-
/** Interface for an environment provider. */
4-
interface EnvironmentProvider {
33+
import com.google.api.core.InternalApi;
34+
35+
/**
36+
* Interface for an environment provider.
37+
*
38+
* <p>For internal use only.
39+
*/
40+
@InternalApi
41+
public interface EnvironmentProvider {
542
String getEnv(String name);
643
}

0 commit comments

Comments
 (0)