From c9fd1b1a477329ae336accd151a57795a0c83955 Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Tue, 3 Jun 2025 10:01:18 -0700 Subject: [PATCH 1/9] feat(mtls): Introduce DefaultMtlsProviderFactory and SecureConnectProvider (#1730) This PR allows the Java auth lib to provide either an X509 mTLS provider or SecureConnect mTLS provider based on availability and under a common "MtlsProvider" interface. The Java GAX lib will be refactored to use this factory and interface in a subsequent PR. --- ...CertificateSourceUnavailableException.java | 9 +- .../auth/mtls/ContextAwareMetadataJson.java | 55 ++++++ .../auth/mtls/DefaultMtlsProviderFactory.java | 66 +++++++ .../com/google/auth/mtls/MtlsProvider.java | 61 +++++++ .../auth/mtls/SecureConnectProvider.java | 170 ++++++++++++++++++ .../WorkloadCertificateConfiguration.java | 9 +- .../com/google/auth/mtls/X509Provider.java | 68 +++---- .../auth/mtls/SecureConnectProviderTest.java | 161 +++++++++++++++++ oauth2_http/testresources/mtlsCertAndKey.pem | 30 ++++ .../mtls_context_aware_metadata.json | 9 + 10 files changed, 595 insertions(+), 43 deletions(-) create mode 100644 oauth2_http/java/com/google/auth/mtls/ContextAwareMetadataJson.java create mode 100644 oauth2_http/java/com/google/auth/mtls/DefaultMtlsProviderFactory.java create mode 100644 oauth2_http/java/com/google/auth/mtls/MtlsProvider.java create mode 100644 oauth2_http/java/com/google/auth/mtls/SecureConnectProvider.java create mode 100644 oauth2_http/javatests/com/google/auth/mtls/SecureConnectProviderTest.java create mode 100644 oauth2_http/testresources/mtlsCertAndKey.pem create mode 100644 oauth2_http/testresources/mtls_context_aware_metadata.json 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 From 3058b069e474fb06d16926c9313ca1f931934a11 Mon Sep 17 00:00:00 2001 From: Leo <39062083+lsirac@users.noreply.github.com> Date: Wed, 4 Jun 2025 08:55:15 -0700 Subject: [PATCH 2/9] Fix: Correct typo in ServiceAccountJwtAccessCredentials.java comment (#1765) Changed "preferrable" to "preferable" in a comment within the getRequestMetadata method. --- .../google/auth/oauth2/ServiceAccountJwtAccessCredentials.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java index 6e2ae2874..716a71bd4 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java @@ -374,7 +374,7 @@ public boolean hasRequestMetadataOnly() { @Override public void getRequestMetadata( final URI uri, Executor executor, final RequestMetadataCallback callback) { - // It doesn't use network. Only some CPU work on par with TLS handshake. So it's preferrable + // It doesn't use network. Only some CPU work on par with TLS handshake. So it's preferable // to do it in the current thread, which is likely to be the network thread. blockingGetToCallback(uri, callback); } From 5f7a0841b32c5e03ca1bbf49a7e612725062311b Mon Sep 17 00:00:00 2001 From: Leo <39062083+lsirac@users.noreply.github.com> Date: Wed, 4 Jun 2025 08:55:55 -0700 Subject: [PATCH 3/9] docs: duplicate "the" in Javadoc comments (#1764) * Fix duplicate "the" in Javadoc comments Removes occurrences of "the the" in Javadoc comments in the following files: - oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java - oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java - oauth2_http/java/com/google/auth/oauth2/JwtCredentials.java * fix format --- oauth2_http/java/com/google/auth/oauth2/JwtCredentials.java | 2 +- .../java/com/google/auth/oauth2/OAuth2Credentials.java | 4 ++-- .../auth/oauth2/ServiceAccountJwtAccessCredentials.java | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/JwtCredentials.java b/oauth2_http/java/com/google/auth/oauth2/JwtCredentials.java index 3c21d31b9..0bf9c537e 100644 --- a/oauth2_http/java/com/google/auth/oauth2/JwtCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/JwtCredentials.java @@ -140,7 +140,7 @@ private boolean shouldRefresh() { /** * Returns a copy of these credentials with modified claims. * - * @param newClaims new claims. Any unspecified claim fields default to the the current values. + * @param newClaims new claims. Any unspecified claim fields default to the current values. * @return new credentials */ @Override diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java b/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java index 3d569b02f..dfeb5966a 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java @@ -574,8 +574,8 @@ public void onFailure(Throwable throwable) { /** * Result from {@link com.google.auth.oauth2.OAuth2Credentials#getOrCreateRefreshTask()}. * - *

Contains the the refresh task and a flag indicating if the task is newly created. If the - * task is newly created, it is the caller's responsibility to execute it. + *

Contains the refresh task and a flag indicating if the task is newly created. If the task is + * newly created, it is the caller's responsibility to execute it. */ static class AsyncRefreshResult { private final RefreshTask task; diff --git a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java index 716a71bd4..9f59d3f13 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java @@ -336,8 +336,7 @@ public JwtCredentials load(JwtClaims claims) throws Exception { /** * Returns a new JwtCredentials instance with modified claims. * - * @param newClaims new claims. Any unspecified claim fields will default to the the current - * values. + * @param newClaims new claims. Any unspecified claim fields will default to the current values. * @return new credentials */ @Override From 5eb3659c131969e674ea1bb4b84698202befbc9b Mon Sep 17 00:00:00 2001 From: Leo <39062083+lsirac@users.noreply.github.com> Date: Wed, 4 Jun 2025 08:56:23 -0700 Subject: [PATCH 4/9] Fix: Update Javadoc reference in ExternalAccountCredentials (#1763) The Javadoc for ExternalAccountCredentials.java incorrectly referred to `GoogleCredential.Builder`. This commit corrects the reference to `GoogleCredentials.Builder`. --- .../java/com/google/auth/oauth2/ExternalAccountCredentials.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index a376f55be..73c44d3e8 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -763,7 +763,7 @@ public abstract static class Builder extends GoogleCredentials.Builder { @Nullable protected ServiceAccountImpersonationOptions serviceAccountImpersonationOptions; /* The field is not being used and value not set. Superseded by the same field in the - {@link GoogleCredential.Builder}. + {@link GoogleCredentials.Builder}. */ @Nullable @Deprecated protected String universeDomain; From 42b9602886b00b0090e519c79cfc96d9b876ffeb Mon Sep 17 00:00:00 2001 From: Leo <39062083+lsirac@users.noreply.github.com> Date: Wed, 4 Jun 2025 08:57:29 -0700 Subject: [PATCH 5/9] Fix: Correct misspelling of OAuth in comments (#1762) ServiceAccountCredentials.java repeatedly referred to "Oauth Endpoint" instead of "OAuth Endpoint" in comments. This commit corrects these misspellings. --- .../com/google/auth/oauth2/ServiceAccountCredentials.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java index 3246f1800..356223859 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java @@ -582,7 +582,7 @@ public AccessToken refreshAccessToken() throws IOException { } /** - * Returns a Google ID Token from either the Oauth or IAM Endpoint. For Credentials that are in + * Returns a Google ID Token from either the OAuth or IAM Endpoint. For Credentials that are in * the Google Default Universe (googleapis.com), the ID Token will be retrieved from the Oauth * Endpoint. Otherwise, it will be retrieved from the IAM Endpoint. * @@ -601,7 +601,7 @@ public IdToken idTokenWithAudience(String targetAudience, List