diff --git a/oauth2_http/java/com/google/auth/mtls/CertificateSourceUnavailableException.java b/oauth2_http/java/com/google/auth/mtls/CertificateSourceUnavailableException.java
index 71d5b11d6..22a96ed22 100644
--- a/oauth2_http/java/com/google/auth/mtls/CertificateSourceUnavailableException.java
+++ b/oauth2_http/java/com/google/auth/mtls/CertificateSourceUnavailableException.java
@@ -1,18 +1,17 @@
/*
- * Copyright 2025, Google Inc. All rights reserved.
+ * Copyright 2025 Google LLC
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
- * * Redistributions of source code must retain the above copyright
+ * * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above
+ * * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
- *
- * * Neither the name of Google Inc. nor the names of its
+ * * Neither the name of Google LLC nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
diff --git a/oauth2_http/java/com/google/auth/mtls/ContextAwareMetadataJson.java b/oauth2_http/java/com/google/auth/mtls/ContextAwareMetadataJson.java
new file mode 100644
index 000000000..11583c4d0
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/mtls/ContextAwareMetadataJson.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.mtls;
+
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.util.Key;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+/**
+ * Data class representing context_aware_metadata.json file. This is meant for internal Google Cloud
+ * usage and behavior may be changed without warning.
+ *
+ *
Note: This implementation is duplicated from the existing ContextAwareMetadataJson found in
+ * the Gax library. The Gax library version of ContextAwareMetadataJson will be marked as deprecated
+ * in the future.
+ */
+public class ContextAwareMetadataJson extends GenericJson {
+ /** Cert provider command */
+ @Key("cert_provider_command")
+ private List commands;
+
+ /** Returns the cert provider command. */
+ public final ImmutableList getCommands() {
+ return ImmutableList.copyOf(commands);
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/mtls/DefaultMtlsProviderFactory.java b/oauth2_http/java/com/google/auth/mtls/DefaultMtlsProviderFactory.java
new file mode 100644
index 000000000..b57accd4c
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/mtls/DefaultMtlsProviderFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.mtls;
+
+import java.io.IOException;
+
+public class DefaultMtlsProviderFactory {
+
+ /**
+ * Creates an instance of {@link MtlsProvider}. It first attempts to create an {@link
+ * com.google.auth.mtls.X509Provider}. If the certificate source is unavailable, it falls back to
+ * creating a {@link SecureConnectProvider}. If the secure connect provider also fails, it throws
+ * a {@link com.google.auth.mtls.CertificateSourceUnavailableException}.
+ *
+ * This is only meant to be used internally by Google Cloud libraries, and the public facing
+ * methods may be changed without notice, and have no guarantee of backwards compatibility.
+ *
+ * @return an instance of {@link MtlsProvider}.
+ * @throws com.google.auth.mtls.CertificateSourceUnavailableException if neither provider can be
+ * created.
+ * @throws IOException if an I/O error occurs during provider creation.
+ */
+ public static MtlsProvider create() throws IOException {
+ // Note: The caller should handle CertificateSourceUnavailableException gracefully, since
+ // it is an expected error case. All other IOExceptions are unexpected and should be surfaced
+ // up the call stack.
+ MtlsProvider mtlsProvider = new X509Provider();
+ if (mtlsProvider.isAvailable()) {
+ return mtlsProvider;
+ }
+ mtlsProvider = new SecureConnectProvider();
+ if (mtlsProvider.isAvailable()) {
+ return mtlsProvider;
+ }
+ throw new CertificateSourceUnavailableException(
+ "No Certificate Source is available on this device.");
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/mtls/MtlsProvider.java b/oauth2_http/java/com/google/auth/mtls/MtlsProvider.java
new file mode 100644
index 000000000..edc412552
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/mtls/MtlsProvider.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.mtls;
+
+import java.io.IOException;
+import java.security.KeyStore;
+
+/**
+ * MtlsProvider is used by the Gax library for configuring mutual TLS in the HTTP and GRPC transport
+ * layer. The source of the client certificate is up to the implementation.
+ *
+ *
Note: This interface will replace the identically named "MtlsProvider" implementation in the
+ * Gax library. The Gax library version of MtlsProvider will be marked as deprecated. See
+ * https://github.com/googleapis/google-auth-library-java/issues/1758
+ */
+public interface MtlsProvider {
+ /**
+ * Returns a mutual TLS key store.
+ *
+ * @return KeyStore for configuring mTLS.
+ * @throws CertificateSourceUnavailableException if the certificate source is unavailable (ex.
+ * missing configuration file).
+ * @throws IOException if a general I/O error occurs while creating the KeyStore
+ */
+ KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException;
+
+ /**
+ * Returns true if the underlying mTLS provider is available.
+ *
+ * @throws IOException if a general I/O error occurs while determining availability.
+ */
+ boolean isAvailable() throws IOException;
+}
diff --git a/oauth2_http/java/com/google/auth/mtls/SecureConnectProvider.java b/oauth2_http/java/com/google/auth/mtls/SecureConnectProvider.java
new file mode 100644
index 000000000..9d30d6800
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/mtls/SecureConnectProvider.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.mtls;
+
+import com.google.api.client.json.JsonParser;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.client.util.SecurityUtils;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class implements {@link MtlsProvider} for the Google Auth library transport layer via {@link
+ * ContextAwareMetadataJson}. This is only meant to be used internally by Google Cloud libraries,
+ * and the public facing methods may be changed without notice, and have no guarantee of backwards
+ * compatibility.
+ *
+ *
Note: This implementation is derived from the existing "MtlsProvider" found in the Gax
+ * library, with two notable differences: 1) All logic associated with parsing environment variables
+ * related to "mTLS usage" are omitted - a separate helper class will be introduced in the Gax
+ * library to serve this purpose. 2) getKeyStore throws {@link
+ * com.google.auth.mtls.CertificateSourceUnavailableException} instead of returning "null" if this
+ * cert source is not available on the device.
+ *
+ *
Additionally, this implementation will replace the existing "MtlsProvider" in the Gax library.
+ * The Gax library version of MtlsProvider will be marked as deprecated.
+ */
+public class SecureConnectProvider implements MtlsProvider {
+ interface ProcessProvider {
+ public Process createProcess(InputStream metadata) throws IOException;
+ }
+
+ static class DefaultProcessProvider implements ProcessProvider {
+ @Override
+ public Process createProcess(InputStream metadata) throws IOException {
+ if (metadata == null) {
+ throw new IOException("Error creating Process: metadata is null");
+ }
+ List command = extractCertificateProviderCommand(metadata);
+ return new ProcessBuilder(command).start();
+ }
+ }
+
+ private static final String DEFAULT_CONTEXT_AWARE_METADATA_PATH =
+ System.getProperty("user.home") + "/.secureConnect/context_aware_metadata.json";
+
+ private String metadataPath;
+ private ProcessProvider processProvider;
+
+ @VisibleForTesting
+ SecureConnectProvider(ProcessProvider processProvider, String metadataPath) {
+ this.processProvider = processProvider;
+ this.metadataPath = metadataPath;
+ }
+
+ public SecureConnectProvider() {
+ this(new DefaultProcessProvider(), DEFAULT_CONTEXT_AWARE_METADATA_PATH);
+ }
+
+ /**
+ * Returns a mutual TLS key store backed by the certificate provided by the SecureConnect tool.
+ *
+ * @return a KeyStore containing the certificate provided by the SecureConnect tool.
+ * @throws CertificateSourceUnavailableException if the certificate source is unavailable (ex.
+ * missing configuration file).
+ * @throws IOException if a general I/O error occurs while creating the KeyStore.
+ */
+ @Override
+ public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
+ try (InputStream stream = new FileInputStream(metadataPath)) {
+ return getKeyStore(stream, processProvider);
+ } catch (InterruptedException e) {
+ throw new IOException("SecureConnect: Interrupted executing certificate provider command", e);
+ } catch (GeneralSecurityException e) {
+ throw new CertificateSourceUnavailableException(
+ "SecureConnect encountered GeneralSecurityException:", e);
+ } catch (FileNotFoundException exception) {
+ // If the metadata file doesn't exist, then there is no key store, so we will throw sentinel
+ // error
+ throw new CertificateSourceUnavailableException("SecureConnect metadata does not exist.");
+ }
+ }
+
+ /**
+ * Returns true if the SecureConnect mTLS provider is available.
+ *
+ * @throws IOException if a general I/O error occurs while determining availability.
+ */
+ @Override
+ public boolean isAvailable() throws IOException {
+ try {
+ this.getKeyStore();
+ } catch (CertificateSourceUnavailableException e) {
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ static KeyStore getKeyStore(InputStream metadata, ProcessProvider processProvider)
+ throws IOException, InterruptedException, GeneralSecurityException {
+ Process process = processProvider.createProcess(metadata);
+
+ // Run the command and timeout after 1000 milliseconds.
+ // The cert provider command usually finishes instantly (if it doesn't hang),
+ // so 1000 milliseconds is plenty of time.
+ int exitCode = runCertificateProviderCommand(process, 1000);
+ if (exitCode != 0) {
+ throw new IOException(
+ "SecureConnect: Cert provider command failed with exit code: " + exitCode);
+ }
+
+ // Create mTLS key store with the input certificates from shell command.
+ return SecurityUtils.createMtlsKeyStore(process.getInputStream());
+ }
+
+ @VisibleForTesting
+ static ImmutableList extractCertificateProviderCommand(InputStream contextAwareMetadata)
+ throws IOException {
+ JsonParser parser = new GsonFactory().createJsonParser(contextAwareMetadata);
+ ContextAwareMetadataJson json = parser.parse(ContextAwareMetadataJson.class);
+ return json.getCommands();
+ }
+
+ @VisibleForTesting
+ static int runCertificateProviderCommand(Process commandProcess, long timeoutMilliseconds)
+ throws IOException, InterruptedException {
+ boolean terminated = commandProcess.waitFor(timeoutMilliseconds, TimeUnit.MILLISECONDS);
+ if (!terminated) {
+ commandProcess.destroy();
+ throw new IOException("SecureConnect: Cert provider command timed out");
+ }
+ return commandProcess.exitValue();
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/mtls/WorkloadCertificateConfiguration.java b/oauth2_http/java/com/google/auth/mtls/WorkloadCertificateConfiguration.java
index 5da318ff6..db439eea5 100644
--- a/oauth2_http/java/com/google/auth/mtls/WorkloadCertificateConfiguration.java
+++ b/oauth2_http/java/com/google/auth/mtls/WorkloadCertificateConfiguration.java
@@ -1,18 +1,17 @@
/*
- * Copyright 2025, Google Inc. All rights reserved.
+ * Copyright 2025 Google LLC
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
- * * Redistributions of source code must retain the above copyright
+ * * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above
+ * * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
- *
- * * Neither the name of Google Inc. nor the names of its
+ * * Neither the name of Google LLC nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
diff --git a/oauth2_http/java/com/google/auth/mtls/X509Provider.java b/oauth2_http/java/com/google/auth/mtls/X509Provider.java
index cb08c2229..7ff490f0f 100644
--- a/oauth2_http/java/com/google/auth/mtls/X509Provider.java
+++ b/oauth2_http/java/com/google/auth/mtls/X509Provider.java
@@ -1,18 +1,17 @@
/*
- * Copyright 2025, Google Inc. All rights reserved.
+ * Copyright 2025 Google LLC
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
- * * Redistributions of source code must retain the above copyright
+ * * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above
+ * * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
- *
- * * Neither the name of Google Inc. nor the names of its
+ * * Neither the name of Google LLC nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
@@ -43,12 +42,12 @@
import java.util.Locale;
/**
- * This class provides certificate key stores to the Google Auth library transport layer via
- * certificate configuration files. This is only meant to be used internally to Google Cloud
+ * This class implements {@link MtlsProvider} for the Google Auth library transport layer via {@link
+ * WorkloadCertificateConfiguration}. This is only meant to be used internally by Google Cloud
* libraries, and the public facing methods may be changed without notice, and have no guarantee of
- * backwards compatability.
+ * backwards compatibility.
*/
-public class X509Provider {
+public class X509Provider implements MtlsProvider {
static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG";
static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";
@@ -110,22 +109,20 @@ public String getCertificatePath() throws IOException {
*
*
* @return a KeyStore containing the X.509 certificate specified by the certificate configuration.
- * @throws IOException if there is an error retrieving the certificate configuration.
+ * @throws CertificateSourceUnavailableException if the certificate source is unavailable (ex.
+ * missing configuration file)
+ * @throws IOException if a general I/O error occurs while creating the KeyStore
*/
- public KeyStore getKeyStore() throws IOException {
+ @Override
+ public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
WorkloadCertificateConfiguration workloadCertConfig = getWorkloadCertificateConfiguration();
- InputStream certStream = null;
- InputStream privateKeyStream = null;
- SequenceInputStream certAndPrivateKeyStream = null;
- try {
- // Read the certificate and private key file paths into separate streams.
- File certFile = new File(workloadCertConfig.getCertPath());
- File privateKeyFile = new File(workloadCertConfig.getPrivateKeyPath());
- certStream = createInputStream(certFile);
- privateKeyStream = createInputStream(privateKeyFile);
- // Merge the two streams into a single stream.
- certAndPrivateKeyStream = new SequenceInputStream(certStream, privateKeyStream);
+ // Read the certificate and private key file paths into streams.
+ try (InputStream certStream = createInputStream(new File(workloadCertConfig.getCertPath()));
+ InputStream privateKeyStream =
+ createInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
+ SequenceInputStream certAndPrivateKeyStream =
+ new SequenceInputStream(certStream, privateKeyStream)) {
// Build a key store using the combined stream.
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
@@ -134,18 +131,23 @@ public KeyStore getKeyStore() throws IOException {
throw e;
} catch (Exception e) {
// Wrap all other exception types to an IOException.
- throw new IOException(e);
- } finally {
- if (certStream != null) {
- certStream.close();
- }
- if (privateKeyStream != null) {
- privateKeyStream.close();
- }
- if (certAndPrivateKeyStream != null) {
- certAndPrivateKeyStream.close();
- }
+ throw new IOException("X509Provider: Unexpected IOException:", e);
+ }
+ }
+
+ /**
+ * Returns true if the X509 mTLS provider is available.
+ *
+ * @throws IOException if a general I/O error occurs while determining availability.
+ */
+ @Override
+ public boolean isAvailable() throws IOException {
+ try {
+ this.getKeyStore();
+ } catch (CertificateSourceUnavailableException e) {
+ return false;
}
+ return true;
}
private WorkloadCertificateConfiguration getWorkloadCertificateConfiguration()
diff --git a/oauth2_http/javatests/com/google/auth/mtls/SecureConnectProviderTest.java b/oauth2_http/javatests/com/google/auth/mtls/SecureConnectProviderTest.java
new file mode 100644
index 000000000..9592a52d6
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/mtls/SecureConnectProviderTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2025, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.mtls;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.util.List;
+import org.junit.Test;
+
+public class SecureConnectProviderTest {
+
+ private static class TestCertProviderCommandProcess extends Process {
+
+ private boolean runForever;
+ private int exitValue;
+
+ public TestCertProviderCommandProcess(int exitValue, boolean runForever) {
+ this.runForever = runForever;
+ this.exitValue = exitValue;
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ return null;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return null;
+ }
+
+ @Override
+ public InputStream getErrorStream() {
+ return null;
+ }
+
+ @Override
+ public int waitFor() throws InterruptedException {
+ return 0;
+ }
+
+ @Override
+ public int exitValue() {
+ if (runForever) {
+ throw new IllegalThreadStateException();
+ }
+ return exitValue;
+ }
+
+ @Override
+ public void destroy() {}
+ }
+
+ static class TestProcessProvider implements SecureConnectProvider.ProcessProvider {
+
+ private int exitCode;
+
+ public TestProcessProvider(int exitCode) {
+ this.exitCode = exitCode;
+ }
+
+ @Override
+ public Process createProcess(InputStream metadata) throws IOException {
+ return new TestCertProviderCommandProcess(exitCode, false);
+ }
+ }
+
+ @Test
+ public void testGetKeyStoreNonZeroExitCode()
+ throws IOException, InterruptedException, GeneralSecurityException {
+ InputStream metadata =
+ this.getClass()
+ .getClassLoader()
+ .getResourceAsStream("com/google/api/gax/rpc/mtls/mtlsCertAndKey.pem");
+ IOException actual =
+ assertThrows(
+ IOException.class,
+ () -> SecureConnectProvider.getKeyStore(metadata, new TestProcessProvider(1)));
+ assertTrue(
+ "expected to fail with nonzero exit code",
+ actual
+ .getMessage()
+ .contains("SecureConnect: Cert provider command failed with exit code: 1"));
+ }
+
+ @Test
+ public void testExtractCertificateProviderCommand() throws IOException {
+ InputStream inputStream =
+ this.getClass().getClassLoader().getResourceAsStream("mtls_context_aware_metadata.json");
+ List command = SecureConnectProvider.extractCertificateProviderCommand(inputStream);
+ assertEquals(2, command.size());
+ assertEquals("some_binary", command.get(0));
+ assertEquals("some_argument", command.get(1));
+ }
+
+ @Test
+ public void testRunCertificateProviderCommandSuccess() throws IOException, InterruptedException {
+ Process certCommandProcess = new TestCertProviderCommandProcess(0, false);
+ int exitValue = SecureConnectProvider.runCertificateProviderCommand(certCommandProcess, 100);
+ assertEquals(0, exitValue);
+ }
+
+ @Test
+ public void testRunCertificateProviderCommandTimeout() throws InterruptedException {
+ Process certCommandProcess = new TestCertProviderCommandProcess(0, true);
+ IOException actual =
+ assertThrows(
+ IOException.class,
+ () -> SecureConnectProvider.runCertificateProviderCommand(certCommandProcess, 100));
+ assertTrue(
+ "expected to fail with timeout",
+ actual.getMessage().contains("SecureConnect: Cert provider command timed out"));
+ }
+
+ @Test
+ public void testGetKeyStore_FileNotFoundException()
+ throws IOException, GeneralSecurityException, InterruptedException {
+ SecureConnectProvider provider =
+ new SecureConnectProvider(new TestProcessProvider(0), "/invalid/metadata/path.json");
+
+ CertificateSourceUnavailableException exception =
+ assertThrows(CertificateSourceUnavailableException.class, provider::getKeyStore);
+
+ assertEquals("SecureConnect metadata does not exist.", exception.getMessage());
+ }
+}
diff --git a/oauth2_http/testresources/mtlsCertAndKey.pem b/oauth2_http/testresources/mtlsCertAndKey.pem
new file mode 100644
index 000000000..d6c045125
--- /dev/null
+++ b/oauth2_http/testresources/mtlsCertAndKey.pem
@@ -0,0 +1,30 @@
+-----BEGIN CERTIFICATE-----
+MIICGzCCAYSgAwIBAgIIWrt6xtmHPs4wDQYJKoZIhvcNAQEFBQAwMzExMC8GA1UE
+AxMoMTAwOTEyMDcyNjg3OC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbTAeFw0x
+MjEyMDExNjEwNDRaFw0yMjExMjkxNjEwNDRaMDMxMTAvBgNVBAMTKDEwMDkxMjA3
+MjY4NzguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20wgZ8wDQYJKoZIhvcNAQEB
+BQADgY0AMIGJAoGBAL1SdY8jTUVU7O4/XrZLYTw0ON1lV6MQRGajFDFCqD2Fd9tQ
+GLW8Iftx9wfXe1zuaehJSgLcyCxazfyJoN3RiONBihBqWY6d3lQKqkgsRTNZkdFJ
+Wdzl/6CxhK9sojh2p0r3tydtv9iwq5fuuWIvtODtT98EgphhncQAqkKoF3zVAgMB
+AAGjODA2MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQM
+MAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4GBAD8XQEqzGePa9VrvtEGpf+R4
+fkxKbcYAzqYq202nKu0kfjhIYkYSBj6gi348YaxE64yu60TVl42l5HThmswUheW4
+uQIaq36JvwvsDP5Zoj5BgiNSnDAFQp+jJFBRUA5vooJKgKgMDf/r/DCOsbO6VJF1
+kWwa9n19NFiV0z3m6isj
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAL1SdY8jTUVU7O4/
+XrZLYTw0ON1lV6MQRGajFDFCqD2Fd9tQGLW8Iftx9wfXe1zuaehJSgLcyCxazfyJ
+oN3RiONBihBqWY6d3lQKqkgsRTNZkdFJWdzl/6CxhK9sojh2p0r3tydtv9iwq5fu
+uWIvtODtT98EgphhncQAqkKoF3zVAgMBAAECgYB51B9cXe4yiGTzJ4pOKpHGySAy
+sC1F/IjXt2eeD3PuKv4m/hL4l7kScpLx0+NJuQ4j8U2UK/kQOdrGANapB1ZbMZAK
+/q0xmIUzdNIDiGSoTXGN2mEfdsEpQ/Xiv0lyhYBBPC/K4sYIpHccnhSRQUZlWLLY
+lE5cFNKC9b7226mNvQJBAPt0hfCNIN0kUYOA9jdLtx7CE4ySGMPf5KPBuzPd8ty1
+fxaFm9PB7B76VZQYmHcWy8rT5XjoLJHrmGW1ZvP+iDsCQQDAvnKoarPOGb5iJfkq
+RrA4flf1TOlf+1+uqIOJ94959jkkJeb0gv/TshDnm6/bWn+1kJylQaKygCizwPwB
+Z84vAkA0Duur4YvsPJijoQ9YY1SGCagCcjyuUKwFOxaGpmyhRPIKt56LOJqpzyno
+fy8ReKa4VyYq4eZYT249oFCwMwIBAkAROPNF2UL3x5UbcAkznd1hLujtIlI4IV4L
+XUNjsJtBap7we/KHJq11XRPlniO4lf2TW7iji5neGVWJulTKS1xBAkAerktk4Hsw
+ErUaUG1s/d+Sgc8e/KMeBElV+NxGhcWEeZtfHMn/6VOlbzY82JyvC9OKC80A5CAE
+VUV6b25kqrcu
+-----END PRIVATE KEY-----
\ No newline at end of file
diff --git a/oauth2_http/testresources/mtls_context_aware_metadata.json b/oauth2_http/testresources/mtls_context_aware_metadata.json
new file mode 100644
index 000000000..52e6f432e
--- /dev/null
+++ b/oauth2_http/testresources/mtls_context_aware_metadata.json
@@ -0,0 +1,9 @@
+{
+ "cert_provider_command": [
+ "some_binary",
+ "some_argument"
+ ],
+ "device_resource_ids": [
+ "123"
+ ]
+}
\ No newline at end of file