3131package com .google .auth .mtls ;
3232
3333import com .google .api .core .InternalApi ;
34+ import com .google .auth .http .HttpTransportFactory ;
3435import com .google .auth .oauth2 .EnvironmentProvider ;
36+ import com .google .auth .oauth2 .OAuth2Utils ;
3537import com .google .auth .oauth2 .PropertyProvider ;
3638import com .google .common .base .Strings ;
3739import java .io .File ;
3840import java .io .FileInputStream ;
3941import java .io .IOException ;
4042import java .io .InputStream ;
43+ import java .security .KeyStore ;
4144import java .util .Locale ;
45+ import java .util .logging .Logger ;
46+ import javax .annotation .Nullable ;
4247
4348/**
4449 * Utility class for mTLS related operations.
4752 */
4853@ InternalApi
4954public class MtlsUtils {
55+ private static final Logger LOGGER = Logger .getLogger (MtlsUtils .class .getName ());
56+
5057 static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG" ;
5158 static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json" ;
5259 static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud" ;
@@ -91,38 +98,27 @@ public static String getCertificatePath(
9198 }
9299
93100 /**
94- * Resolves and loads the workload certificate configuration.
101+ * Resolves and parses the workload certificate configuration.
95102 *
96- * <p>The configuration file is resolved in the following order of precedence: 1. The provided
97- * certConfigPathOverride (if not null). 2. The path specified by the
98- * GOOGLE_API_CERTIFICATE_CONFIG environment variable. 3. The well-known certificate configuration
99- * file in the gcloud config directory.
103+ * <p>This locates the certificate configuration file via {@link #resolveCertificateConfigFile}
104+ * and parses its contents into a {@link WorkloadCertificateConfiguration}.
100105 *
101106 * @param envProvider the environment provider to use for resolving environment variables
102107 * @param propProvider the property provider to use for resolving system properties
103108 * @param certConfigPathOverride optional override path for the configuration file
104109 * @return the loaded WorkloadCertificateConfiguration
105- * @throws IOException if the configuration file cannot be found , read, or parsed
110+ * @throws IOException if the configuration file cannot be resolved , read, or parsed
106111 */
107112 static WorkloadCertificateConfiguration getWorkloadCertificateConfiguration (
108113 EnvironmentProvider envProvider , PropertyProvider propProvider , String certConfigPathOverride )
109114 throws IOException {
110- File certConfig ;
111- if (certConfigPathOverride != null ) {
112- certConfig = new File (certConfigPathOverride );
113- } else {
114- String envCredentialsPath = envProvider .getEnv (CERTIFICATE_CONFIGURATION_ENV_VARIABLE );
115- if (!Strings .isNullOrEmpty (envCredentialsPath )) {
116- certConfig = new File (envCredentialsPath );
117- } else {
118- certConfig = getWellKnownCertificateConfigFile (envProvider , propProvider );
119- }
120- }
121-
122- if (!certConfig .isFile ()) {
115+ File certConfig =
116+ resolveCertificateConfigFile (envProvider , propProvider , certConfigPathOverride );
117+ if (certConfig == null ) {
118+ File wellKnownConfig = getWellKnownCertificateConfigFile (envProvider , propProvider );
123119 throw new CertificateSourceUnavailableException (
124120 "Certificate configuration file does not exist or is not a file: "
125- + certConfig .getAbsolutePath ());
121+ + wellKnownConfig .getAbsolutePath ());
126122 }
127123 try (InputStream certConfigStream = new FileInputStream (certConfig )) {
128124 return WorkloadCertificateConfiguration .fromCertificateConfigurationStream (certConfigStream );
@@ -181,33 +177,76 @@ public static boolean canMtlsBeEnabled(
181177 if (policy == MtlsEndpointUsagePolicy .NEVER ) {
182178 return false ;
183179 }
180+
184181 if (policy == MtlsEndpointUsagePolicy .ALWAYS ) {
185182 return true ;
186183 }
187184
188- // Locate and process the certificate configuration file
185+ File certConfigFile =
186+ resolveCertificateConfigFile (envProvider , propProvider , certConfigPathOverride );
187+ return certConfigFile != null ;
188+ }
189+
190+ /**
191+ * Resolves the mutual TLS (mTLS) certificate configuration file.
192+ *
193+ * <p>The configuration file is resolved in the following order of precedence:
194+ * <ol>
195+ * <li>The developer-provided {@code certConfigPathOverride} (if not null).
196+ * <li>The path specified by the {@code GOOGLE_API_CERTIFICATE_CONFIG} environment variable.
197+ * <li>The well-known automatic gcloud workload identity provisioning location (via {@link
198+ * #getWellKnownCertificateConfigFile}).
199+ * </ol>
200+ *
201+ * <p>If an explicit configuration file is specified (via override or environment variable) and it
202+ * is missing or invalid, an exception is thrown. If no explicit file is specified and the default
203+ * well-known file is missing, {@code null} is returned.
204+ *
205+ * @param envProvider the environment provider to use for resolving environment variables
206+ * @param propProvider the property provider to use for resolving system properties
207+ * @param certConfigPathOverride optional override path for the configuration file
208+ * @return the resolved File object, or null if no configuration was found
209+ * @throws IOException if an explicit configuration file is missing or malformed
210+ */
211+ @ Nullable
212+ static File resolveCertificateConfigFile (
213+ EnvironmentProvider envProvider , PropertyProvider propProvider , String certConfigPathOverride )
214+ throws IOException {
215+ // 1. Check explicit developer override
216+ if (certConfigPathOverride != null ) {
217+ File certConfigFile = new File (certConfigPathOverride );
218+ if (!certConfigFile .isFile ()) {
219+ throw new CertificateSourceUnavailableException (
220+ "Certificate configuration file does not exist or is not a file: "
221+ + certConfigFile .getAbsolutePath ());
222+ }
223+ return certConfigFile ;
224+ }
225+
226+ // 2. Check explicit environment variable
189227 String envPath = envProvider .getEnv (CERTIFICATE_CONFIGURATION_ENV_VARIABLE );
190- if (certConfigPathOverride != null || !Strings .isNullOrEmpty (envPath )) {
191- File certConfigFile =
192- new File (certConfigPathOverride != null ? certConfigPathOverride : envPath );
228+ if (!Strings .isNullOrEmpty (envPath )) {
229+ File certConfigFile = new File (envPath );
193230 if (!certConfigFile .isFile ()) {
194231 throw new CertificateSourceUnavailableException (
195232 "Certificate configuration file does not exist or is not a file: "
196233 + certConfigFile .getAbsolutePath ());
197234 }
198- return true ;
235+ return certConfigFile ;
199236 }
200237
238+ // 3. Check optional well-known automatic provisioning location
201239 try {
202240 File wellKnownConfig = getWellKnownCertificateConfigFile (envProvider , propProvider );
203241 if (wellKnownConfig .isFile ()) {
204- return true ;
242+ return wellKnownConfig ;
205243 }
206244 } catch (IOException e ) {
207- // ignore if well-known directory resolution fails (e.g. APPDATA not set on Windows)
245+ LOGGER .info (
246+ "Could not get the mutual TLS (mTLS) client certificate configuration. The library will fall back to making standard non-mTLS requests." );
208247 }
209248
210- return false ;
249+ return null ;
211250 }
212251
213252 /**
@@ -226,4 +265,55 @@ public static MtlsEndpointUsagePolicy getMtlsEndpointUsagePolicy(
226265 }
227266 return MtlsEndpointUsagePolicy .AUTO ;
228267 }
268+
269+ /**
270+ * Prepares and upgrades the HTTP transport factory for mutual TLS (mTLS) if applicable.
271+ *
272+ * @param baseTransportFactory the base HTTP transport factory to upgrade
273+ * @param envProvider the environment provider to use for resolving environment variables
274+ * @param propProvider the property provider to use for resolving system properties
275+ * @param certConfigPathOverride optional override path for the configuration file
276+ * @return the mTLS-configured HTTP transport factory, or the base factory if mTLS is not enabled
277+ * @throws IOException if mTLS is required/enabled but certificate initialization fails or an
278+ * incompatible transport factory was provided
279+ */
280+ public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled (
281+ HttpTransportFactory baseTransportFactory ,
282+ EnvironmentProvider envProvider ,
283+ PropertyProvider propProvider ,
284+ String certConfigPathOverride )
285+ throws IOException {
286+
287+ MtlsEndpointUsagePolicy mtlsPolicy = getMtlsEndpointUsagePolicy (envProvider );
288+ try {
289+ boolean canMtls = canMtlsBeEnabled (envProvider , propProvider , certConfigPathOverride );
290+ if (canMtls ) {
291+ if (baseTransportFactory instanceof MtlsHttpTransportFactory ) {
292+ // A custom MtlsHttpTransportFactory was already pre-configured by the user.
293+ // Keep using it as-is without re-initializing.
294+ return baseTransportFactory ;
295+ } else if (baseTransportFactory == OAuth2Utils .HTTP_TRANSPORT_FACTORY ) {
296+ // This is the default HttpTransportFactory assigned by credentials.
297+ // Automatically discover and load client certificates to construct an mTLS factory.
298+ X509Provider x509Provider =
299+ new X509Provider (envProvider , propProvider , certConfigPathOverride );
300+ KeyStore mtlsKeyStore = x509Provider .getKeyStore ();
301+ return new MtlsHttpTransportFactory (mtlsKeyStore );
302+ } else {
303+ // A user configured non-mTLS HttpTransportFactory was explicitly injected.
304+ // Reject it to avoid bypassing mTLS enforcement or overriding the user's factory.
305+ throw new IOException (
306+ "mTLS is enabled on the system, but a user configured non-mTLS HttpTransportFactory was provided: "
307+ + baseTransportFactory .getClass ().getName ());
308+ }
309+ }
310+ } catch (Exception e ) {
311+ if (mtlsPolicy == MtlsEndpointUsagePolicy .ALWAYS ) {
312+ throw new IOException (
313+ "mTLS is configured to ALWAYS, but initialization failed: " + e .getMessage (), e );
314+ }
315+ // Graceful fallback to standard transport if mTLS initialization fails under AUTO policy
316+ }
317+ return baseTransportFactory ;
318+ }
229319}
0 commit comments