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 ;
4246import org .jspecify .annotations .NullMarked ;
47+ import org .jspecify .annotations .Nullable ;
4348
4449/**
4550 * Utility class for mTLS related operations.
4954@ NullMarked
5055@ InternalApi
5156public class MtlsUtils {
57+ private static final Logger LOGGER = Logger .getLogger (MtlsUtils .class .getName ());
58+
5259 static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG" ;
5360 static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json" ;
5461 static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud" ;
5562
63+ /**
64+ * The policy determining when to use mutual TLS (mTLS) endpoints.
65+ *
66+ * <p>See <a href="https://google.aip.dev/auth/4114">AIP-4114</a> for the specification on mTLS
67+ * endpoint usage.
68+ */
69+ public enum MtlsEndpointUsagePolicy {
70+ /** Always use the mTLS endpoint, and fail if client certificates are not configured. */
71+ ALWAYS ,
72+ /** Never use the mTLS endpoint. */
73+ NEVER ,
74+ /** Use the mTLS endpoint if client certificates are configured (auto-discovery). */
75+ AUTO
76+ }
77+
5678 private MtlsUtils () {
5779 // Prevent instantiation for Utility class
5880 }
@@ -65,7 +87,9 @@ private MtlsUtils() {
6587 * @throws IOException if the certificate configuration cannot be found or loaded.
6688 */
6789 public static String getCertificatePath (
68- EnvironmentProvider envProvider , PropertyProvider propProvider , String certConfigPathOverride )
90+ EnvironmentProvider envProvider ,
91+ PropertyProvider propProvider ,
92+ @ Nullable String certConfigPathOverride )
6993 throws IOException {
7094 String certPath =
7195 getWorkloadCertificateConfiguration (envProvider , propProvider , certConfigPathOverride )
@@ -78,39 +102,38 @@ public static String getCertificatePath(
78102 }
79103
80104 /**
81- * Resolves and loads the workload certificate configuration.
105+ * Resolves and parses the workload certificate configuration.
82106 *
83- * <p>The configuration file is resolved in the following order of precedence: 1. The provided
84- * certConfigPathOverride (if not null). 2. The path specified by the
85- * GOOGLE_API_CERTIFICATE_CONFIG environment variable. 3. The well-known certificate configuration
86- * file in the gcloud config directory.
107+ * <p>This locates the certificate configuration file via {@link #resolveCertificateConfigFile}
108+ * and parses its contents into a {@link WorkloadCertificateConfiguration}.
87109 *
88110 * @param envProvider the environment provider to use for resolving environment variables
89111 * @param propProvider the property provider to use for resolving system properties
90112 * @param certConfigPathOverride optional override path for the configuration file
91113 * @return the loaded WorkloadCertificateConfiguration
92- * @throws IOException if the configuration file cannot be found , read, or parsed
114+ * @throws IOException if the configuration file cannot be resolved , read, or parsed
93115 */
94116 static WorkloadCertificateConfiguration getWorkloadCertificateConfiguration (
95- EnvironmentProvider envProvider , PropertyProvider propProvider , String certConfigPathOverride )
117+ EnvironmentProvider envProvider ,
118+ PropertyProvider propProvider ,
119+ @ Nullable String certConfigPathOverride )
96120 throws IOException {
97- File certConfig ;
98- if (certConfigPathOverride != null ) {
99- certConfig = new File (certConfigPathOverride );
100- } else {
101- String envCredentialsPath = envProvider .getEnv (CERTIFICATE_CONFIGURATION_ENV_VARIABLE );
102- if (!Strings .isNullOrEmpty (envCredentialsPath )) {
103- certConfig = new File (envCredentialsPath );
104- } else {
105- certConfig = getWellKnownCertificateConfigFile (envProvider , propProvider );
121+ File certConfig =
122+ resolveCertificateConfigFile (envProvider , propProvider , certConfigPathOverride );
123+ if (certConfig == null ) {
124+ try {
125+ File wellKnownConfig = getWellKnownCertificateConfigFile (envProvider , propProvider );
126+ throw new CertificateSourceUnavailableException (
127+ "Certificate configuration file does not exist or is not a file: "
128+ + wellKnownConfig .getAbsolutePath ());
129+ } catch (IOException e ) {
130+ if (e instanceof CertificateSourceUnavailableException ) {
131+ throw (CertificateSourceUnavailableException ) e ;
132+ }
133+ throw new CertificateSourceUnavailableException (
134+ "Failed to get well-known certificate config file path" , e );
106135 }
107136 }
108-
109- if (!certConfig .isFile ()) {
110- throw new CertificateSourceUnavailableException (
111- "Certificate configuration file does not exist or is not a file: "
112- + certConfig .getAbsolutePath ());
113- }
114137 try (InputStream certConfigStream = new FileInputStream (certConfig )) {
115138 return WorkloadCertificateConfiguration .fromCertificateConfigurationStream (certConfigStream );
116139 }
@@ -139,4 +162,204 @@ private static File getWellKnownCertificateConfigFile(
139162 }
140163 return new File (cloudConfigPath , WELL_KNOWN_CERTIFICATE_CONFIG_FILE );
141164 }
165+
166+ /**
167+ * Centralized helper method to determine if mutual TLS (mTLS) can be enabled.
168+ *
169+ * @param envProvider the environment provider to use for resolving environment variables
170+ * @param propProvider the property provider to use for resolving system properties
171+ * @param certConfigPathOverride optional override path for the configuration file
172+ * @return true if mTLS should be enabled, false otherwise
173+ * @throws IOException if the configuration file is present but contains missing or malformed
174+ * files
175+ */
176+ public static boolean canBeEnabled (
177+ EnvironmentProvider envProvider ,
178+ PropertyProvider propProvider ,
179+ @ Nullable String certConfigPathOverride )
180+ throws IOException {
181+
182+ // Check if client certificate usage is allowed
183+ String useClientCertificate = envProvider .getEnv ("GOOGLE_API_USE_CLIENT_CERTIFICATE" );
184+ if ("false" .equalsIgnoreCase (useClientCertificate )) {
185+ return false ;
186+ }
187+
188+ MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy (envProvider );
189+ if (policy == MtlsEndpointUsagePolicy .NEVER ) {
190+ return false ;
191+ }
192+
193+ File certConfigFile =
194+ resolveCertificateConfigFile (envProvider , propProvider , certConfigPathOverride );
195+ if (certConfigFile == null ) {
196+ return false ;
197+ }
198+
199+ try {
200+ WorkloadCertificateConfiguration config =
201+ getWorkloadCertificateConfiguration (envProvider , propProvider , certConfigPathOverride );
202+ File certFile = new File (config .getCertPath ());
203+ File keyFile = new File (config .getPrivateKeyPath ());
204+ return certFile .isFile () && keyFile .isFile ();
205+ } catch (IOException e ) {
206+ return false ;
207+ }
208+ }
209+
210+ /**
211+ * Returns whether the mutual TLS (mTLS) endpoint should be used.
212+ *
213+ * @param envProvider the environment provider to use for resolving environment variables
214+ * @param propProvider the property provider to use for resolving system properties
215+ * @param certConfigPathOverride optional override path for the configuration file
216+ * @return true if the mTLS endpoint should be used, false otherwise
217+ */
218+ public static boolean shouldMtlsEndpointBeUsed (
219+ EnvironmentProvider envProvider ,
220+ PropertyProvider propProvider ,
221+ @ Nullable String certConfigPathOverride ) {
222+ MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy (envProvider );
223+ if (policy == MtlsEndpointUsagePolicy .ALWAYS ) {
224+ return true ;
225+ }
226+ if (policy == MtlsEndpointUsagePolicy .NEVER ) {
227+ return false ;
228+ }
229+ // policy is AUTO: use mTLS endpoint if client certificate can be enabled
230+ try {
231+ return canBeEnabled (envProvider , propProvider , certConfigPathOverride );
232+ } catch (IOException e ) {
233+ return false ;
234+ }
235+ }
236+
237+ /**
238+ * Resolves the mutual TLS (mTLS) certificate configuration file.
239+ *
240+ * <p>The configuration file is resolved in the following order of precedence:
241+ *
242+ * <ol>
243+ * <li>The developer-provided {@code certConfigPathOverride} (if not null).
244+ * <li>The path specified by the {@code GOOGLE_API_CERTIFICATE_CONFIG} environment variable.
245+ * <li>The well-known automatic gcloud workload identity provisioning location (via {@link
246+ * #getWellKnownCertificateConfigFile}).
247+ * </ol>
248+ *
249+ * <p>If an explicit configuration file is specified (via override or environment variable) and it
250+ * is missing or invalid, an exception is thrown. If no explicit file is specified and the default
251+ * well-known file is missing, {@code null} is returned.
252+ *
253+ * @param envProvider the environment provider to use for resolving environment variables
254+ * @param propProvider the property provider to use for resolving system properties
255+ * @param certConfigPathOverride optional override path for the configuration file
256+ * @return the resolved File object, or null if no configuration was found
257+ * @throws IOException if an explicit configuration file is missing or malformed
258+ */
259+ static @ Nullable File resolveCertificateConfigFile (
260+ EnvironmentProvider envProvider ,
261+ PropertyProvider propProvider ,
262+ @ Nullable String certConfigPathOverride )
263+ throws IOException {
264+ // 1. Check explicit developer override
265+ if (certConfigPathOverride != null ) {
266+ File certConfigFile = new File (certConfigPathOverride );
267+ if (!certConfigFile .isFile ()) {
268+ throw new CertificateSourceUnavailableException (
269+ "Certificate configuration file does not exist or is not a file: "
270+ + certConfigFile .getAbsolutePath ());
271+ }
272+ return certConfigFile ;
273+ }
274+
275+ // 2. Check explicit environment variable
276+ String envPath = envProvider .getEnv (CERTIFICATE_CONFIGURATION_ENV_VARIABLE );
277+ if (!Strings .isNullOrEmpty (envPath )) {
278+ File certConfigFile = new File (envPath );
279+ if (!certConfigFile .isFile ()) {
280+ throw new CertificateSourceUnavailableException (
281+ "Certificate configuration file does not exist or is not a file: "
282+ + certConfigFile .getAbsolutePath ());
283+ }
284+ return certConfigFile ;
285+ }
286+
287+ // 3. Check optional well-known automatic provisioning location
288+ try {
289+ File wellKnownConfig = getWellKnownCertificateConfigFile (envProvider , propProvider );
290+ if (wellKnownConfig .isFile ()) {
291+ return wellKnownConfig ;
292+ }
293+ } catch (IOException e ) {
294+ LOGGER .info (
295+ "Could not get the mutual TLS (mTLS) client certificate configuration. The library will fall back to making standard non-mTLS requests." );
296+ }
297+
298+ return null ;
299+ }
300+
301+ /**
302+ * Returns the current mutual TLS endpoint usage policy.
303+ *
304+ * @param envProvider the environment provider to use for resolving environment variables
305+ * @return the MtlsEndpointUsagePolicy enum value
306+ */
307+ public static MtlsEndpointUsagePolicy getMtlsEndpointUsagePolicy (
308+ EnvironmentProvider envProvider ) {
309+ String mtlsEndpointUsagePolicy = envProvider .getEnv ("GOOGLE_API_USE_MTLS_ENDPOINT" );
310+ if ("never" .equalsIgnoreCase (mtlsEndpointUsagePolicy )) {
311+ return MtlsEndpointUsagePolicy .NEVER ;
312+ } else if ("always" .equalsIgnoreCase (mtlsEndpointUsagePolicy )) {
313+ return MtlsEndpointUsagePolicy .ALWAYS ;
314+ }
315+ return MtlsEndpointUsagePolicy .AUTO ;
316+ }
317+
318+ /**
319+ * Prepares and upgrades the HTTP transport factory for mutual TLS (mTLS) if applicable.
320+ *
321+ * @param baseTransportFactory the base HTTP transport factory to upgrade
322+ * @param envProvider the environment provider to use for resolving environment variables
323+ * @param propProvider the property provider to use for resolving system properties
324+ * @param certConfigPathOverride optional override path for the configuration file
325+ * @return the mTLS-configured HTTP transport factory, or the base factory if mTLS is not enabled
326+ * @throws IOException if mTLS is required/enabled but certificate initialization fails or an
327+ * incompatible transport factory was provided
328+ */
329+ public static @ Nullable HttpTransportFactory prepareTransportFactoryIfMtlsEnabled (
330+ @ Nullable HttpTransportFactory baseTransportFactory ,
331+ EnvironmentProvider envProvider ,
332+ PropertyProvider propProvider ,
333+ @ Nullable String certConfigPathOverride )
334+ throws IOException {
335+
336+ if (baseTransportFactory == null ) {
337+ return null ;
338+ }
339+
340+ if (!canBeEnabled (envProvider , propProvider , certConfigPathOverride )) {
341+ return baseTransportFactory ;
342+ }
343+
344+ if (baseTransportFactory != OAuth2Utils .HTTP_TRANSPORT_FACTORY ) {
345+ // A user configured HttpTransportFactory was explicitly injected.
346+ // Trust the developer's custom factory and return it as-is.
347+ return baseTransportFactory ;
348+ }
349+
350+ try {
351+ // This is the default HttpTransportFactory assigned by credentials.
352+ // Automatically discover and load client certificates to construct an mTLS factory.
353+ X509Provider x509Provider =
354+ new X509Provider (envProvider , propProvider , certConfigPathOverride );
355+ KeyStore mtlsKeyStore = x509Provider .getKeyStore ();
356+ return new MtlsHttpTransportFactory (mtlsKeyStore );
357+ } catch (Exception e ) {
358+ LOGGER .warning (
359+ "mTLS transport factory initialization failed, falling back to non-mTLS transport: "
360+ + e .getMessage ());
361+ // Graceful fallback to standard transport if mTLS initialization fails
362+ return baseTransportFactory ;
363+ }
364+ }
142365}
0 commit comments