diff --git a/sdk-platform-java/gax-java/gax-httpjson/BUILD.bazel b/sdk-platform-java/gax-java/gax-httpjson/BUILD.bazel index c4f78edb79a2..e97aedebabfd 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/BUILD.bazel +++ b/sdk-platform-java/gax-java/gax-httpjson/BUILD.bazel @@ -53,6 +53,10 @@ java_library( srcs = glob(["src/test/java/**/*.java"]), javacopts = _JAVA_COPTS, plugins = ["//:auto_value_plugin"], + resources = glob([ + "src/test/resources/com/google/api/gax/httpjson/tls13TestCa.pem", + "src/test/resources/com/google/api/gax/httpjson/tls13TestServerCertAndKey.pem", + ]), visibility = ["//visibility:public"], deps = [":gax_httpjson"] + _COMPILE_DEPS + _TEST_COMPILE_DEPS, ) diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java index 226c5a155feb..2bfcd40b305c 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java @@ -54,6 +54,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -223,10 +224,15 @@ private NetHttpTransport.Builder configureMtls(NetHttpTransport.Builder builder) // and trust manager factory (TMF) are bound to Conscrypt's TLS implementation (supporting PQC // key exchange). SSLContext sslContext = SSLContext.getInstance("TLS", conscryptProvider); + // The TrustManagerFactory must come from the same provider as the SSLContext. On TLS 1.3, + // Conscrypt passes authType "GENERIC" to the trust manager, which the JDK (SunJSSE) PKIX + // trust manager rejects for CA-issued server certificates that carry a KeyUsage extension + // (e.g. Google front ends), failing the handshake with "Unknown authType: GENERIC". + // Conscrypt's trust manager loads the same default trust store as the JDK. SslUtils.initSslContext( sslContext, null, - SslUtils.getPkixTrustManagerFactory(), + TrustManagerFactory.getInstance("PKIX", conscryptProvider), mtlsKeyStore, "", SslUtils.getDefaultKeyManagerFactory()); diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTls13Test.java b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTls13Test.java new file mode 100644 index 000000000000..c7374e986700 --- /dev/null +++ b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTls13Test.java @@ -0,0 +1,278 @@ +/* + * Copyright 2026 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.api.gax.httpjson; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.util.SecurityUtils; +import com.google.api.gax.rpc.mtls.CertificateBasedAccess; +import com.google.api.gax.rpc.testing.FakeMtlsProvider; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +/** + * Handshake test for the Conscrypt-backed mTLS {@link HttpTransport} built by {@link + * InstantiatingHttpJsonChannelProvider}. + * + *
Regression test for "Unknown authType: GENERIC": on TLS 1.3, Conscrypt passes the authType + * {@code "GENERIC"} to its trust manager. If the trust manager comes from the JDK (SunJSSE) rather + * than Conscrypt, it rejects server certificates that are issued by a trusted CA and carry a + * KeyUsage extension (as Google front ends' certificates do), and every mTLS HTTP/JSON handshake + * fails. A self-signed server certificate that is itself the trust anchor does not reproduce the + * issue, so the server here presents a CA-issued leaf with KeyUsage. + * + *
The transport always uses the default trust store, so this test points {@code
+ * javax.net.ssl.trustStore} at a temporary store containing only the test CA, and restores the
+ * previous value afterwards.
+ */
+@EnabledIf(
+ value = "isConscryptAndTls13Available",
+ disabledReason = "Conscrypt native library or TLS 1.3 is unavailable on this platform")
+class InstantiatingHttpJsonChannelProviderTls13Test {
+
+ private static final String RESOURCE_DIR = "com/google/api/gax/httpjson/";
+ private static final String TRUST_STORE_PROPERTY = "javax.net.ssl.trustStore";
+ private static final String TRUST_STORE_PASSWORD_PROPERTY = "javax.net.ssl.trustStorePassword";
+ private static final String TRUST_STORE_PASSWORD = "changeit";
+
+ @TempDir File tempDir;
+
+ private String previousTrustStore;
+ private String previousTrustStorePassword;
+ private ExecutorService serverExecutor;
+
+ /** Condition for {@link EnabledIf}; must be static because it is used at class level. */
+ static boolean isConscryptAndTls13Available() {
+ return HttpJsonConscryptUtils.getConscryptProvider() != null && isTls13Supported();
+ }
+
+ @BeforeEach
+ void setUp() throws Exception {
+ previousTrustStore = System.getProperty(TRUST_STORE_PROPERTY);
+ previousTrustStorePassword = System.getProperty(TRUST_STORE_PASSWORD_PROPERTY);
+ File trustStoreFile = writeTrustStoreWithTestCa();
+ System.setProperty(TRUST_STORE_PROPERTY, trustStoreFile.getAbsolutePath());
+ System.setProperty(TRUST_STORE_PASSWORD_PROPERTY, TRUST_STORE_PASSWORD);
+
+ serverExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @AfterEach
+ void tearDown() {
+ restoreProperty(TRUST_STORE_PROPERTY, previousTrustStore);
+ restoreProperty(TRUST_STORE_PASSWORD_PROPERTY, previousTrustStorePassword);
+ if (serverExecutor != null) {
+ serverExecutor.shutdownNow();
+ }
+ }
+
+ @Test
+ void createHttpTransport_withMtlsAndConscrypt_completesTls13HandshakeWithCaIssuedServerCert()
+ throws Exception {
+ CertificateBasedAccess certificateBasedAccess = Mockito.mock(CertificateBasedAccess.class);
+ Mockito.when(certificateBasedAccess.useMtlsClientCertificate()).thenReturn(true);
+ InstantiatingHttpJsonChannelProvider channelProvider =
+ InstantiatingHttpJsonChannelProvider.newBuilder()
+ .setEndpoint("localhost:443")
+ .setMtlsProvider(
+ new FakeMtlsProvider(FakeMtlsProvider.createTestMtlsKeyStore(), "", false))
+ .setCertificateBasedAccess(certificateBasedAccess)
+ .build();
+ HttpTransport transport = channelProvider.createHttpTransport();
+ assertThat(transport).isNotNull();
+
+ final SSLServerSocket serverSocket = createTls13ServerSocket();
+ try {
+ Future