diff --git a/.github/workflows/showcase.yaml b/.github/workflows/showcase.yaml index 3325dfadcc3a..6c1f135d0703 100644 --- a/.github/workflows/showcase.yaml +++ b/.github/workflows/showcase.yaml @@ -61,7 +61,22 @@ jobs: curl --location https://github.com/googleapis/gapic-showcase/releases/download/v${SHOWCASE_VERSION}/gapic-showcase-${SHOWCASE_VERSION}-linux-amd64.tar.gz --output /usr/src/showcase/showcase-${SHOWCASE_VERSION}-linux-amd64.tar.gz cd /usr/src/showcase/ tar -xf showcase-* + # Start standard insecure showcase server on default port 7469 for standard integration tests ./gapic-showcase run & + # Start secure TLS showcase server on port 7470 for PQC TLS integration tests + ./gapic-showcase run --port 7470 --tls --ca-cert-output-file /tmp/showcase-ca.pem & + # Wait deterministically for both background showcase servers to finish binding ports 7469/7470 + # and writing /tmp/showcase-ca.pem. Starting TLS requires RSA key generation and disk I/O, + # which can cause race conditions if tests start before /tmp/showcase-ca.pem is created. + for i in $(seq 1 30); do + if (echo > /dev/tcp/127.0.0.1/7469) 2>/dev/null && \ + (echo > /dev/tcp/127.0.0.1/7470) 2>/dev/null && \ + [ -f /tmp/showcase-ca.pem ]; then + echo "Showcase servers (ports 7469, 7470) and CA cert ready in attempt $i." + break + fi + sleep 0.2 + done cd - - name: Showcase integration tests working-directory: java-showcase @@ -166,7 +181,22 @@ jobs: curl --location https://github.com/googleapis/gapic-showcase/releases/download/v${SHOWCASE_VERSION}/gapic-showcase-${SHOWCASE_VERSION}-linux-amd64.tar.gz --output /usr/src/showcase/showcase-${SHOWCASE_VERSION}-linux-amd64.tar.gz cd /usr/src/showcase/ tar -xf showcase-* + # Start standard insecure showcase server on default port 7469 for standard integration tests ./gapic-showcase run & + # Start secure TLS showcase server on port 7470 for PQC TLS integration tests + ./gapic-showcase run --port 7470 --tls --ca-cert-output-file /tmp/showcase-ca.pem & + # Wait deterministically for both background showcase servers to finish binding ports 7469/7470 + # and writing /tmp/showcase-ca.pem. Starting TLS requires RSA key generation and disk I/O, + # which can cause race conditions if tests start before /tmp/showcase-ca.pem is created. + for i in $(seq 1 30); do + if (echo > /dev/tcp/127.0.0.1/7469) 2>/dev/null && \ + (echo > /dev/tcp/127.0.0.1/7470) 2>/dev/null && \ + [ -f /tmp/showcase-ca.pem ]; then + echo "Showcase servers (ports 7469, 7470) and CA cert ready in attempt $i." + break + fi + sleep 0.2 + done cd - - name: Showcase integration tests working-directory: java-showcase diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPostQuantumCryptography.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPostQuantumCryptography.java new file mode 100644 index 000000000000..7229a31d1a51 --- /dev/null +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPostQuantumCryptography.java @@ -0,0 +1,258 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.showcase.v1beta1.it; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.httpjson.HttpJsonConscryptUtils; +import com.google.api.gax.httpjson.HttpJsonMetadata; +import com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider; +import com.google.showcase.v1beta1.EchoClient; +import com.google.showcase.v1beta1.EchoRequest; +import com.google.showcase.v1beta1.EchoResponse; +import com.google.showcase.v1beta1.EchoSettings; +import com.google.showcase.v1beta1.it.util.HttpJsonCapturingClientInterceptor; +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.Collections; +import java.util.List; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import org.conscrypt.Conscrypt; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Integration tests to verify Post-Quantum Cryptography (PQC) TLS negotiation for HTTP/JSON (REST) + * clients. + * + *

These tests execute calls against a local secure (TLS-enabled) Showcase server. During the TLS + * handshake, the client and server negotiate cipher suites and key exchange groups. Showcase + * injects information about the negotiated TLS connection parameters into custom headers: + * + *

+ * + *

Verification cases: + * + *

    + *
  1. {@code testHttpJsonPqc}: Verifies that HTTP/JSON transport defaults to Conscrypt and + * negotiates the hybrid post-quantum group {@code X25519MLKEM768}. + *
  2. {@code testHttpJsonPqc_withExplicitSecurityProvider}: Verifies that overriding the + * transport's SSLSocketFactory to explicitly use standard JDK JSSE provider (SunJSSE) falls + * back gracefully to classical key exchange ({@code X25519}) instead of crashing. + *
+ */ +public class ITPostQuantumCryptography { + + // TLS response header names from Showcase server + private static final String TLS_GROUP_HEADER = "x-showcase-tls-group"; + private static final String TLS_SUPPORTED_GROUPS_HEADER = + "x-showcase-tls-client-supported-groups"; + + // Expected TLS parameters + private static final String EXPECTED_PQC_GROUP = "X25519MLKEM768"; + + private static final String DEFAULT_CA_CERT_PATH = getCaCertPath(); + + /** + * Resolves the absolute path to the Showcase server's CA certificate PEM file. + * + * @return absolute path to the CA certificate file + */ + private static String getCaCertPath() { + String prop = System.getProperty("showcase.ca.cert.path"); + if (prop != null) { + return prop; + } + if (new File("/tmp/showcase-ca.pem").isFile()) { + return "/tmp/showcase-ca.pem"; + } + return "target/showcase-ca.pem"; + } + + private static final String SECURE_ENDPOINT = + System.getProperty("showcase.secure.endpoint", "localhost:7470"); + + private static SSLContext originalSslContext; + + @BeforeAll + static void setUp() throws Exception { + File certFile = new File(DEFAULT_CA_CERT_PATH); + assertWithMessage("CA certificate file not found at " + DEFAULT_CA_CERT_PATH) + .that(certFile.isFile()) + .isTrue(); + + try { + originalSslContext = SSLContext.getDefault(); + } catch (Throwable t) { + // Ignore if default SSLContext cannot be retrieved + } + + // Register local Showcase CA cert in default SSLContext so the default NetHttpTransport trusts + // the server + KeyStore trustStore = loadCaCert(DEFAULT_CA_CERT_PATH); + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(trustStore); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), null); + SSLContext.setDefault(sslContext); + } + + @AfterAll + static void tearDown() { + if (originalSslContext != null) { + try { + SSLContext.setDefault(originalSslContext); + } catch (Throwable t) { + // Ignore during test cleanup + } + } + } + + @Test + void testHttpJsonPqc() throws Exception { + HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor(); + + // Test that the Java client creates a PQC-compliant NetHttpTransport by default (no custom + // transport provided) + InstantiatingHttpJsonChannelProvider transportChannelProvider = + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setEndpoint("https://" + SECURE_ENDPOINT) + .setInterceptorProvider(() -> Collections.singletonList(interceptor)) + .build(); + + EchoSettings settings = + EchoSettings.newHttpJsonBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider(transportChannelProvider) + .build(); + + try (EchoClient client = EchoClient.create(settings)) { + EchoResponse response = + client.echo(EchoRequest.newBuilder().setContent("pqc-httpjson-test").build()); + assertThat(response.getContent()).isEqualTo("pqc-httpjson-test"); + + HttpJsonMetadata capturedHeaders = interceptor.metadata; + assertThat(capturedHeaders).isNotNull(); + + String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER); + assertThat(negotiatedGroup).isEqualTo(EXPECTED_PQC_GROUP); + + String supportedGroups = getSingleHeaderString(capturedHeaders, TLS_SUPPORTED_GROUPS_HEADER); + assertThat(supportedGroups).isNotNull(); + } + } + + @Test + void testHttpJsonPqc_withExplicitClassicalGroupsNoPqc() throws Exception { + // This test explicitly configures non-PQC classical groups (X25519, SecP256r1) on Conscrypt + // to verify that the client falls back gracefully to classical TLS key exchange. + NetHttpTransport transport = + HttpJsonConscryptUtils.configureConscryptSecurityProvider(new NetHttpTransport.Builder()) + .setSslSocketConfigurator( + socket -> { + if (Conscrypt.isConscrypt(socket)) { + Conscrypt.setNamedGroups(socket, new String[] {"X25519", "SecP256r1"}); + } + }) + .build(); + + HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor(); + + InstantiatingHttpJsonChannelProvider transportChannelProvider = + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport(transport) + .setEndpoint("https://" + SECURE_ENDPOINT) + .setInterceptorProvider(() -> Collections.singletonList(interceptor)) + .build(); + + EchoSettings settings = + EchoSettings.newHttpJsonBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider(transportChannelProvider) + .build(); + + try (EchoClient client = EchoClient.create(settings)) { + EchoResponse response = + client.echo( + EchoRequest.newBuilder().setContent("pqc-httpjson-explicit-provider-test").build()); + assertThat(response.getContent()).isEqualTo("pqc-httpjson-explicit-provider-test"); + + HttpJsonMetadata capturedHeaders = interceptor.metadata; + assertThat(capturedHeaders).isNotNull(); + + String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER); + // Under classical non-PQC configuration, negotiated group is a classical curve + assertThat(negotiatedGroup).isAnyOf("X25519", "SecP256r1", "CurveP256"); + assertThat(negotiatedGroup).isNotEqualTo(EXPECTED_PQC_GROUP); + } + } + + /** + * Extracts the first string value of a specified HTTP response header from metadata. + * + * @param metadata the HTTP metadata containing response headers + * @param name the case-insensitive header key name + * @return header value string, or {@code null} if not found + */ + private static String getSingleHeaderString(HttpJsonMetadata metadata, String name) { + Object valueObj = metadata.getHeaders().get(name); + if (valueObj instanceof List) { + List list = (List) valueObj; + if (!list.isEmpty()) { + return String.valueOf(list.get(0)); + } + } else if (valueObj != null) { + return String.valueOf(valueObj); + } + return null; + } + + /** + * Loads an X.509 CA certificate file from disk into a new KeyStore instance. + * + * @param certPath path to the X.509 certificate file + * @return initialized KeyStore containing the certificate entry + * @throws Exception if reading or parsing the certificate fails + */ + private static KeyStore loadCaCert(String certPath) throws Exception { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + try (InputStream is = Files.newInputStream(Paths.get(certPath))) { + Certificate cert = cf.generateCertificate(is); + trustStore.setCertificateEntry("showcase-ca", cert); + } + return trustStore; + } +} diff --git a/sdk-platform-java/gax-java/gax-httpjson/pom.xml b/sdk-platform-java/gax-java/gax-httpjson/pom.xml index d46c32ffa025..792bee7621d4 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/pom.xml +++ b/sdk-platform-java/gax-java/gax-httpjson/pom.xml @@ -108,6 +108,11 @@ error_prone_annotations ${errorprone.version} + + org.conscrypt + conscrypt-openjdk-uber + ${conscrypt.version} + diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonConscryptUtils.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonConscryptUtils.java new file mode 100644 index 000000000000..9d38dab54627 --- /dev/null +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonConscryptUtils.java @@ -0,0 +1,133 @@ +/* + * 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 com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.core.InternalApi; +import java.security.Provider; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.conscrypt.Conscrypt; + +/** Utility class for creating and configuring {@link NetHttpTransport} instances. */ +@InternalApi +public class HttpJsonConscryptUtils { + + private static final Logger LOG = Logger.getLogger(HttpJsonConscryptUtils.class.getName()); + + /** + * Default TLS 1.3 Post-Quantum Cryptography (PQC) named groups configured when Conscrypt security + * provider is present: + * + * + */ + static final String[] DEFAULT_PQC_GROUPS = + new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519"}; + + /** + * Lazy initialization holder for Conscrypt {@link Provider}. + * + *

Caches the Conscrypt {@link Provider} instance (or {@code null} if initialization fails) to + * avoid repeated expensive JNI initialization operations on every transport creation. + * + *

Returns {@code null} on failure so that transport creation can fall back to default JDK TLS, + * ensuring that setting Conscrypt as the default security provider does not cause breaking + * failures for customers running on environments where Conscrypt is unsupported or unavailable. + */ + private static class ConscryptProviderHolder { + private static final Provider INSTANCE = createProvider(); + + private static Provider createProvider() { + try { + return Conscrypt.newProvider(); + } catch (Throwable t) { + LOG.log( + Level.WARNING, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); + return null; + } + } + } + + /** + * Configures the given {@link NetHttpTransport.Builder} with Conscrypt as the security provider + * by default if Conscrypt is available. Users can customize the {@link NetHttpTransport.Builder} + * to use a different security provider. + * + * @param builder the {@link NetHttpTransport.Builder} to configure + * @return the configured {@link NetHttpTransport.Builder} + */ + public static NetHttpTransport.Builder configureConscryptSecurityProvider( + NetHttpTransport.Builder builder) { + Provider conscryptProvider = ConscryptProviderHolder.INSTANCE; + if (conscryptProvider == null) { + return builder; + } + return builder + .setSecurityProvider(conscryptProvider) + .setSslSocketConfigurator( + socket -> { + if (!Conscrypt.isConscrypt(socket)) { + return; + } + try { + Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); + } catch (Exception e) { + // Native JNI linkage errors (e.g. UnsatisfiedLinkError) are caught during + // ConscryptProviderHolder initialization. Catching Exception here safely + // intercepts runtime socket configuration errors (e.g. unsupported groups or + // closed socket) without swallowing JVM errors like OutOfMemoryError. + LOG.log( + Level.WARNING, + "Failed to set PQC named groups on Conscrypt socket. Falling back to Conscrypt" + + " default TLS groups.", + e); + } + }); + } + + /** + * Returns the Conscrypt {@link Provider} instance, or {@code null} if Conscrypt is unavailable. + * + * @return the Conscrypt provider or null + */ + static Provider getConscryptProvider() { + return ConscryptProviderHolder.INSTANCE; + } + + private HttpJsonConscryptUtils() {} +} 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 4cffc99cea59..14f23fbca8dc 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 @@ -31,6 +31,7 @@ import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.util.SslUtils; import com.google.api.core.InternalExtensionOnly; import com.google.api.gax.core.ExecutorProvider; import com.google.api.gax.rpc.FixedHeaderProvider; @@ -45,11 +46,13 @@ import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.Provider; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.logging.Level; import java.util.logging.Logger; +import javax.net.ssl.SSLContext; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -192,17 +195,42 @@ public TransportChannelProvider withCredentials(Credentials credentials) { "InstantiatingHttpJsonChannelProvider doesn't need credentials"); } - @Nullable HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { - if (mtlsProvider == null) { - return null; + HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); + configureMtls(builder); + HttpJsonConscryptUtils.configureConscryptSecurityProvider(builder); + return builder.build(); + } + + private NetHttpTransport.Builder configureMtls(NetHttpTransport.Builder builder) + throws IOException, GeneralSecurityException { + if (mtlsProvider == null || !certificateBasedAccess.useMtlsClientCertificate()) { + return builder; } - if (certificateBasedAccess.useMtlsClientCertificate()) { - KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); - if (mtlsKeyStore != null) { - return new NetHttpTransport.Builder().trustCertificates(null, mtlsKeyStore, "").build(); - } + KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); + if (mtlsKeyStore == null) { + return builder; + } + Provider conscryptProvider = HttpJsonConscryptUtils.getConscryptProvider(); + if (conscryptProvider == null) { + // Fall back to standard JDK JSSE if Conscrypt provider is unavailable + builder.trustCertificates(null, mtlsKeyStore, ""); + return builder; } - return null; + // Explicitly initialize SSLContext with the Conscrypt provider so that the client certificate + // key managers + // and trust manager factory (TMF) are bound to Conscrypt's TLS implementation (supporting PQC + // key exchange). + SSLContext sslContext = SSLContext.getInstance("TLS", conscryptProvider); + SslUtils.initSslContext( + sslContext, + null, + SslUtils.getPkixTrustManagerFactory(), + mtlsKeyStore, + "", + SslUtils.getDefaultKeyManagerFactory()); + builder.setSslSocketFactory(sslContext.getSocketFactory()); + return builder; } private HttpJsonTransportChannel createChannel() throws IOException, GeneralSecurityException { @@ -366,7 +394,8 @@ public InstantiatingHttpJsonChannelProvider build() { "DefaultMtlsProviderFactory encountered unexpected IOException: " + e.getMessage()); LOG.log( Level.WARNING, - "mTLS configuration was detected on the device, but mTLS failed to initialize. Falling back to non-mTLS channel."); + "mTLS configuration was detected on the device, but mTLS failed to initialize." + + " Falling back to non-mTLS channel."); } } } diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonChannel.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonChannel.java index 8e24eafcce97..0208b8fc561f 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonChannel.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonChannel.java @@ -63,6 +63,10 @@ String getEndpoint() { return endpoint; } + HttpTransport getHttpTransport() { + return httpTransport; + } + private ManagedHttpJsonChannel( @Nullable Executor executor, boolean usingDefaultExecutor, @@ -71,7 +75,12 @@ private ManagedHttpJsonChannel( this.executor = executor; this.usingDefaultExecutor = usingDefaultExecutor; this.endpoint = endpoint; - this.httpTransport = httpTransport == null ? new NetHttpTransport() : httpTransport; + this.httpTransport = + httpTransport == null + ? HttpJsonConscryptUtils.configureConscryptSecurityProvider( + new NetHttpTransport.Builder()) + .build() + : httpTransport; this.deadlineScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); } @@ -227,11 +236,14 @@ public ManagedHttpJsonChannel build() { usingDefaultExecutor = true; } - return new ManagedHttpJsonChannel( - executor, - usingDefaultExecutor, - endpoint, - httpTransport == null ? new NetHttpTransport() : httpTransport); + if (httpTransport == null) { + httpTransport = + HttpJsonConscryptUtils.configureConscryptSecurityProvider( + new NetHttpTransport.Builder()) + .build(); + } + + return new ManagedHttpJsonChannel(executor, usingDefaultExecutor, endpoint, httpTransport); } } } diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonConscryptUtilsTest.java b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonConscryptUtilsTest.java new file mode 100644 index 000000000000..b3b55c5622f0 --- /dev/null +++ b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonConscryptUtilsTest.java @@ -0,0 +1,54 @@ +/* + * 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 java.security.Provider; +import org.junit.jupiter.api.Test; + +class HttpJsonConscryptUtilsTest { + + @Test + void testDefaultPqcGroups_containsExpectedGroups() { + assertThat(HttpJsonConscryptUtils.DEFAULT_PQC_GROUPS) + .asList() + .containsExactly("X25519MLKEM768", "SecP256r1MLKEM768", "X25519") + .inOrder(); + } + + @Test + void testGetConscryptProvider_doesNotThrow() { + Provider provider = HttpJsonConscryptUtils.getConscryptProvider(); + if (provider != null) { + assertThat(provider.getName()).isEqualTo("Conscrypt"); + } + } +} diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java index 0eeb2f27cd85..b8f112752c3f 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java @@ -33,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; +import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.gax.rpc.HeaderProvider; import com.google.api.gax.rpc.TransportChannelProvider; import com.google.api.gax.rpc.mtls.AbstractMtlsTransportChannelTest; @@ -194,6 +195,35 @@ protected Object getMtlsObjectFromTransportChannel( mock(HeaderProvider.class, Mockito.withSettings().withoutAnnotations())) .setExecutor(mock(Executor.class)) .build(); - return channelProvider.createHttpTransport(); + NetHttpTransport transport = (NetHttpTransport) channelProvider.createHttpTransport(); + return (transport != null && transport.isMtls()) ? transport : null; + } + + @Test + void testCreateHttpTransport_returnsValidTransport() throws Exception { + InstantiatingHttpJsonChannelProvider channelProvider = + InstantiatingHttpJsonChannelProvider.newBuilder() + .setEndpoint("localhost:8080") + .setHeaderProvider(Mockito.mock(HeaderProvider.class)) + .setExecutor(Mockito.mock(Executor.class)) + .build(); + NetHttpTransport transport = (NetHttpTransport) channelProvider.createHttpTransport(); + assertThat(transport).isNotNull(); + } + + @Test + void testDefaultPqcGroups_containsExpectedGroups() { + assertThat(HttpJsonConscryptUtils.DEFAULT_PQC_GROUPS) + .asList() + .containsExactly("X25519MLKEM768", "SecP256r1MLKEM768", "X25519") + .inOrder(); + } + + @Test + void testConfigureConscryptSecurityProvider_returnsConfiguredBuilder() { + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); + NetHttpTransport.Builder result = + HttpJsonConscryptUtils.configureConscryptSecurityProvider(builder); + assertThat(result).isSameInstanceAs(builder); } } diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/ManagedHttpJsonChannelTest.java b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/ManagedHttpJsonChannelTest.java new file mode 100644 index 000000000000..234a8afe34d5 --- /dev/null +++ b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/ManagedHttpJsonChannelTest.java @@ -0,0 +1,57 @@ +/* + * 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.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import org.junit.jupiter.api.Test; + +class ManagedHttpJsonChannelTest { + + @Test + void testBuilder_defaultHttpTransport_createsNetHttpTransport() { + ManagedHttpJsonChannel channel = + ManagedHttpJsonChannel.newBuilder().setEndpoint("localhost:8080").build(); + assertThat(channel.getHttpTransport()).isInstanceOf(NetHttpTransport.class); + } + + @Test + void testBuilder_customHttpTransport_usesProvidedTransport() { + HttpTransport customTransport = new NetHttpTransport.Builder().build(); + ManagedHttpJsonChannel channel = + ManagedHttpJsonChannel.newBuilder() + .setEndpoint("localhost:8080") + .setHttpTransport(customTransport) + .build(); + assertThat(channel.getHttpTransport()).isSameInstanceAs(customTransport); + } +} diff --git a/sdk-platform-java/java-core/google-cloud-core-http/pom.xml b/sdk-platform-java/java-core/google-cloud-core-http/pom.xml index 9bcac047678b..ee8beb09cbf0 100644 --- a/sdk-platform-java/java-core/google-cloud-core-http/pom.xml +++ b/sdk-platform-java/java-core/google-cloud-core-http/pom.xml @@ -75,6 +75,11 @@ error_prone_annotations + + org.conscrypt + conscrypt-openjdk-uber + + org.junit.platform diff --git a/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java b/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java index f5ad54532f66..d204381b1f87 100644 --- a/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java +++ b/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java @@ -25,6 +25,7 @@ import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.gax.core.GaxProperties; import com.google.api.gax.httpjson.HttpHeadersUtils; +import com.google.api.gax.httpjson.HttpJsonConscryptUtils; import com.google.api.gax.httpjson.HttpJsonStatusCode; import com.google.api.gax.rpc.ApiClientHeaderProvider; import com.google.api.gax.rpc.EndpointContext; @@ -41,6 +42,7 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.util.Objects; +import java.util.logging.Logger; /** Class representing service options for those services that use HTTP as the transport layer. */ public class HttpTransportOptions implements TransportOptions { @@ -54,6 +56,7 @@ public class HttpTransportOptions implements TransportOptions { public static class DefaultHttpTransportFactory implements HttpTransportFactory { + private static final Logger LOG = Logger.getLogger(DefaultHttpTransportFactory.class.getName()); private static final HttpTransportFactory INSTANCE = new DefaultHttpTransportFactory(); @Override @@ -66,7 +69,10 @@ public HttpTransport create() { // Maybe not on App Engine } } - return new NetHttpTransport(); + + return HttpJsonConscryptUtils.configureConscryptSecurityProvider( + new NetHttpTransport.Builder()) + .build(); } }