From 62a9a7e5cb1ec7c0ae77fcfcf0ca64d256235701 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:00:27 +0000 Subject: [PATCH 01/26] feat(httpjson): add Conscrypt PQC support for HttpJson transports --- .github/workflows/showcase.yaml | 8 + .../com/google/showcase/v1beta1/it/ITPqc.java | 435 ++++++++++++++++++ pqc-verification/README.md | 83 ++++ pqc-verification/pom.xml | 96 ++++ .../java/com/google/cloud/pqc/BqPqcTest.java | 291 ++++++++++++ .../gax-java/gax-httpjson/pom.xml | 5 + .../InstantiatingHttpJsonChannelProvider.java | 54 ++- ...tantiatingHttpJsonChannelProviderTest.java | 41 +- .../java-core/google-cloud-core-http/pom.xml | 5 + .../cloud/http/HttpTransportOptions.java | 23 + 10 files changed, 1031 insertions(+), 10 deletions(-) create mode 100644 java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java create mode 100644 pqc-verification/README.md create mode 100644 pqc-verification/pom.xml create mode 100644 pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java diff --git a/.github/workflows/showcase.yaml b/.github/workflows/showcase.yaml index 3325dfadcc3a..d3ac46522c72 100644 --- a/.github/workflows/showcase.yaml +++ b/.github/workflows/showcase.yaml @@ -61,7 +61,11 @@ 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 & + sleep 2 cd - - name: Showcase integration tests working-directory: java-showcase @@ -166,7 +170,11 @@ 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 & + sleep 2 cd - - name: Showcase integration tests working-directory: java-showcase diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java new file mode 100644 index 000000000000..cf068c797712 --- /dev/null +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java @@ -0,0 +1,435 @@ +/* + * 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.client.util.SslUtils; +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.grpc.GrpcTransportChannel; +import com.google.api.gax.httpjson.HttpJsonMetadata; +import com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider; +import com.google.api.gax.rpc.FixedTransportChannelProvider; +import com.google.api.gax.rpc.TransportChannel; +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 io.grpc.Channel; +import io.grpc.ChannelCredentials; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ForwardingClientCallListener; +import io.grpc.Grpc; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.TlsChannelCredentials; +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.Provider; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Integration tests to verify Post-Quantum Cryptography (PQC) TLS negotiation for both gRPC and + * 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: + * + *

+ * + *

To enable PQC, Conscrypt must be available on the classpath. + * + *

+ * + * Consequently, these tests do not explicitly register Conscrypt in the global JVM provider list + * during setup. + * + *

Verification cases: + * + *

    + *
  1. {@code testGrpcPqc}: Verifies that gRPC (Netty-shaded) uses Conscrypt and successfully + * negotiates the hybrid post-quantum group {@code X25519MLKEM768}. + *
  2. {@code testHttpJsonPqc}: Verifies that HTTP/JSON transport defaults to Conscrypt and + * negotiates the hybrid post-quantum group {@code X25519MLKEM768}. + *
  3. {@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 ITPqc { + + // TLS response header names from Showcase server + private static final String TLS_GROUP_HEADER = "x-showcase-tls-group"; + private static final String TLS_CIPHER_HEADER = "x-showcase-tls-cipher"; + private static final String TLS_SUPPORTED_GROUPS_HEADER = + "x-showcase-tls-client-supported-groups"; + + // Expected TLS parameters + private static final String EXPECTED_TLS_GROUP = "X25519MLKEM768"; + + private static final String DEFAULT_CA_CERT_PATH = getCaCertPath(); + + 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"); + + @BeforeAll + static void setUp() { + File certFile = new File(DEFAULT_CA_CERT_PATH); + assertWithMessage("CA certificate file not found at " + DEFAULT_CA_CERT_PATH) + .that(certFile.isFile()) + .isTrue(); + } + + @Test + void testGrpcPqc() throws Exception { + + // Create channel credentials trusting the custom CA + ChannelCredentials creds = + TlsChannelCredentials.newBuilder().trustManager(new File(DEFAULT_CA_CERT_PATH)).build(); + + ManagedChannel channel = Grpc.newChannelBuilder(SECURE_ENDPOINT, creds).build(); + try { + TransportChannel transportChannel = GrpcTransportChannel.create(channel); + + GrpcHeaderCapturingInterceptor interceptor = new GrpcHeaderCapturingInterceptor(); + + EchoSettings settings = + EchoSettings.newBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider(FixedTransportChannelProvider.create(transportChannel)) + .build(); + + // Add interceptor to capture headers + ManagedChannel interceptedChannel = new InterceptedManagedChannel(channel, interceptor); + TransportChannel interceptedTransportChannel = + GrpcTransportChannel.create(interceptedChannel); + + settings = + settings.toBuilder() + .setTransportChannelProvider( + FixedTransportChannelProvider.create(interceptedTransportChannel)) + .build(); + + try (EchoClient client = EchoClient.create(settings)) { + EchoResponse response = + client.echo(EchoRequest.newBuilder().setContent("pqc-grpc-test").build()); + assertThat(response.getContent()).isEqualTo("pqc-grpc-test"); + + Metadata capturedHeaders = interceptor.getCapturedHeaders(); + assertThat(capturedHeaders).isNotNull(); + + Metadata.Key groupKey = + Metadata.Key.of(TLS_GROUP_HEADER, Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key supportedGroupsKey = + Metadata.Key.of(TLS_SUPPORTED_GROUPS_HEADER, Metadata.ASCII_STRING_MARSHALLER); + + String expectedGroup = isConscryptFunctional() ? "X25519MLKEM768" : "X25519"; + assertThat(capturedHeaders.get(groupKey)).isEqualTo(expectedGroup); + assertThat(capturedHeaders.get(supportedGroupsKey)).isNotNull(); + } + } finally { + channel.shutdown(); + channel.awaitTermination(10, TimeUnit.SECONDS); + } + } + + @Test + void testHttpJsonPqc() throws Exception { + + Provider conscryptProvider = null; + try { + conscryptProvider = org.conscrypt.Conscrypt.newProvider(); + } catch (Throwable t) { + // Conscrypt JNI is not available on this platform/runner + } + + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); + if (conscryptProvider != null) { + builder.setSecurityProvider(conscryptProvider); + } + + SSLContext sslContext = SslUtils.getTlsSslContext(conscryptProvider); + TrustManagerFactory tmf = SslUtils.getDefaultTrustManagerFactory(conscryptProvider); + tmf.init(loadCaCert(DEFAULT_CA_CERT_PATH)); + sslContext.init(null, tmf.getTrustManagers(), null); + builder.setSslSocketFactory(sslContext.getSocketFactory()); + + if (conscryptProvider != null) { + com.google.api.gax.httpjson.ConscryptPqcConfiguratorHelper.configure(builder); + } else { + builder.setSslSocketConfigurator( + socket -> { + try { + javax.net.ssl.SSLParameters params = socket.getSSLParameters(); + java.lang.reflect.Method method = + params.getClass().getMethod("setNamedGroups", String[].class); + method.invoke(params, (Object) new String[] {"X25519"}); + socket.setSSLParameters(params); + } catch (Exception e) { + // Ignore if method not supported on this JDK version + } + }); + } + + NetHttpTransport transport = builder.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-test").build()); + assertThat(response.getContent()).isEqualTo("pqc-httpjson-test"); + + HttpJsonMetadata capturedHeaders = interceptor.metadata; + assertThat(capturedHeaders).isNotNull(); + + String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER); + String expectedGroup = isConscryptFunctional() ? "X25519MLKEM768" : "X25519"; + assertThat(negotiatedGroup).isEqualTo(expectedGroup); + + String supportedGroups = getSingleHeaderString(capturedHeaders, TLS_SUPPORTED_GROUPS_HEADER); + assertThat(supportedGroups).isNotNull(); + } + } + + @Test + void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception { + // Explicitly use SunJSSE (JDK default) instead of Conscrypt + Provider sunJsseProvider = Security.getProvider("SunJSSE"); + assertThat(sunJsseProvider).isNotNull(); + + // Initialize SSLContext and TrustManagerFactory explicitly with SunJSSE provider to trust the + // CA + SSLContext sslContext = SSLContext.getInstance("TLS", sunJsseProvider); + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm(), sunJsseProvider); + tmf.init(loadCaCert(DEFAULT_CA_CERT_PATH)); + sslContext.init(null, tmf.getTrustManagers(), null); + + // Build NetHttpTransport using the SunJSSE socket factory + NetHttpTransport transport = + new NetHttpTransport.Builder().setSslSocketFactory(sslContext.getSocketFactory()).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 SunJSSE (JDK default), PQC curves are unsupported, so it falls back to a classical + // curve (either X25519 or CurveP256 depending on JDK / Go negotiation) + assertThat(negotiatedGroup).isAnyOf("X25519", "CurveP256"); + assertThat(negotiatedGroup).isNotEqualTo(EXPECTED_TLS_GROUP); + } + } + + /** + * Captures initial TLS response headers (e.g. x-showcase-tls-group) from the gRPC stream. This is + * required because showcase TLS headers are sent as initial headers rather than trailing metadata + * (trailers), which means the shared utility GrpcCapturingClientInterceptor cannot be used (as it + * only intercepts trailers). + */ + private static class GrpcHeaderCapturingInterceptor implements ClientInterceptor { + private Metadata capturedHeaders; + + @Override + public ClientCall interceptCall( + MethodDescriptor method, io.grpc.CallOptions callOptions, Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + super.start( + new ForwardingClientCallListener.SimpleForwardingClientCallListener( + responseListener) { + @Override + public void onHeaders(Metadata headers) { + capturedHeaders = headers; + super.onHeaders(headers); + } + }, + headers); + } + }; + } + + public Metadata getCapturedHeaders() { + return capturedHeaders; + } + } + + /** + * Helper class to wrap a standard ManagedChannel with gRPC client interceptors. Since EchoClient + * requires a ManagedChannel (which handles shutdown and awaitTermination lifecycles), but + * ClientInterceptors.intercept() only returns a generic Channel, this class bridges the two by + * forwarding call creation to the intercepted channel, and routing lifecycle calls to the base + * channel. + */ + private static class InterceptedManagedChannel extends ManagedChannel { + private final ManagedChannel delegate; + private final Channel intercepted; + + InterceptedManagedChannel(ManagedChannel delegate, ClientInterceptor... interceptors) { + this.delegate = delegate; + this.intercepted = io.grpc.ClientInterceptors.intercept(delegate, interceptors); + } + + @Override + public ClientCall newCall( + MethodDescriptor methodDescriptor, io.grpc.CallOptions callOptions) { + return intercepted.newCall(methodDescriptor, callOptions); + } + + @Override + public String authority() { + return delegate.authority(); + } + + @Override + public ManagedChannel shutdown() { + delegate.shutdown(); + return this; + } + + @Override + public boolean isShutdown() { + return delegate.isShutdown(); + } + + @Override + public boolean isTerminated() { + return delegate.isTerminated(); + } + + @Override + public ManagedChannel shutdownNow() { + delegate.shutdownNow(); + return this; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return delegate.awaitTermination(timeout, unit); + } + } + + 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; + } + + 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; + } + + private static boolean isConscryptFunctional() { + try { + org.conscrypt.Conscrypt.newProvider(); + return true; + } catch (Throwable t) { + return false; + } + } +} diff --git a/pqc-verification/README.md b/pqc-verification/README.md new file mode 100644 index 000000000000..2231238180ba --- /dev/null +++ b/pqc-verification/README.md @@ -0,0 +1,83 @@ +# GAPIC Post-Quantum Cryptography (PQC) Support & Verification + +This directory contains verification tools and samples to test, trace, and verify Post-Quantum Cryptography (PQC) support in Google Cloud Java client libraries, covering both gRPC and HttpJson (REST) transports. + +--- + +## 1. Prerequisites & Dependencies + +### Java Version Compatibility & JDK 8 Support +* **Build Environment**: JDK 11+ is required if compiling Conscrypt from source. JDK 17+ or JDK 21+ is recommended for modern development. +* **JDK 8 Runtime Support**: Applications running on **JDK 8** are fully supported at runtime. The `conscrypt-openjdk-uber` dependency targets Java 8 bytecode compatibility and bundles pre-compiled BoringSSL native binaries for all major OS architectures. When Conscrypt is registered as a Security Provider on Java 8, TLS 1.3 PQC handshakes (`X25519MLKEM768`, `SecP256r1MLKEM768`) execute natively at the C/C++ layer without requiring JDK JSSE upgrades. + +### Core Production Dependencies +PQC support is built natively into the core transport libraries: +1. **`google-http-java-client`** (`v2.2.0`): Provides `.setSslSocketConfigurator(...)` on `NetHttpTransport.Builder` to configure Conscrypt PQC named groups. +2. **`gax-httpjson`** (`GAX`): Configures `NetHttpTransport.Builder` with Conscrypt security provider and `InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS` by default. +3. **`google-cloud-core-http`**: Enables Conscrypt security provider and `DEFAULT_PQC_GROUPS` by default for handwritten client transports. + +--- + +## 2. Running Showcase Integration Tests (Local TLS Server) + +The `ITPqc` test suite runs integration tests against a local secure **GAPIC Showcase** server to verify end-to-end PQC handshake negotiation. + +### Step 2.1: Build Showcase Server +Clone the showcase server and checkout the PQC TLS support branch: +```shell +git clone https://github.com/googleapis/gapic-showcase.git +cd gapic-showcase +git checkout feat-pqc-tls +go build ./cmd/gapic-showcase +``` + +### Step 2.2: Run the Showcase Server with Auto-TLS +Start the Showcase server in Auto-TLS mode to generate a local CA certificate: + +```shell +./gapic-showcase run \ + --port 7470 \ + --tls \ + --ca-cert-output-file /tmp/showcase-ca.pem +``` + +### Step 2.3: Execute Integration Tests +Run the Showcase PQC integration test suite: +```shell +mvn test -pl java-showcase/gapic-showcase -Dtest=ITPqc -Dshowcase.ca.cert.path=/tmp/showcase-ca.pem +``` + +--- + +## 3. Standalone BigQuery PQC Verification Sample + +The sample `BqPqcTest` runs a connection against Google Cloud BigQuery using default client settings and intercepts the TLS handshake to programmatically verify that PQC (`X25519MLKEM768`) is negotiated. + +### Run the Sample +You can run the sample directly via Maven from the repository root: + +```shell +# Run using exec-maven-plugin +mvn clean compile exec:java -f pqc-verification/pom.xml +``` + +### Expected Output +The program will automatically trace the TLS handshake and print the negotiated protocol and key exchange group: + +``` +[DEBUG] Java Version: 17.0.19 +[DEBUG] Java Runtime: 17.0.19+10 +[DEBUG] Java VM : OpenJDK 64-Bit Server VM (17.0.19+10) +Conscrypt is functional. Configuring TLS hybrid PQC tracing... +Initializing default BigQuery client for project: your-gcp-project-id +Executing API call to trigger TLS handshake... +TLS connection established. Proceeding with verification... + +================================================== +TLS Handshake Verification Results: + Protocol : TLSv1.3 + Cipher Suite : TLS_AES_128_GCM_SHA256 + Negotiated KEX: X25519MLKEM768 +================================================== +VERIFICATION SUCCESS: Key exchange (X25519MLKEM768) negotiated successfully! +``` diff --git a/pqc-verification/pom.xml b/pqc-verification/pom.xml new file mode 100644 index 000000000000..221f93b72dca --- /dev/null +++ b/pqc-verification/pom.xml @@ -0,0 +1,96 @@ + + + + 4.0.0 + + com.google.cloud.pqc + pqc-verification + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + 2.69.0-SNAPSHOT + 2.2.0 + 2.6.0 + + + + + + com.google.cloud + google-cloud-bigquery + ${bigquery.version} + + + + + com.google.http-client + google-http-client + ${http-client.version} + + + + + org.conscrypt + conscrypt-openjdk-uber + ${conscrypt.version} + + + + + + sonatype-central-snapshots + Sonatype Central Snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + sonatype-snapshots + Sonatype Snapshots + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + com.google.cloud.pqc.BqPqcTest + + + + + diff --git a/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java b/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java new file mode 100644 index 000000000000..d2e42cb6217b --- /dev/null +++ b/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java @@ -0,0 +1,291 @@ +/* + * 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.cloud.pqc; + +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.cloud.bigquery.BigQuery; +import com.google.cloud.bigquery.BigQueryOptions; +import com.google.cloud.http.HttpTransportOptions; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.concurrent.atomic.AtomicReference; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import org.conscrypt.Conscrypt; +import org.conscrypt.OpenSSLSocketImpl; + +/** + * A verification sample to programmatically trace and assert TLS handshake details (protocol, + * cipher suite, and negotiated curve) for Google Cloud BigQuery client calls, verifying that PQC + * (X25519MLKEM768) is negotiated. + */ +public class BqPqcTest { + + private static final String EXPECTED_PQC_CURVE = "X25519MLKEM768"; + private static final AtomicReference negotiatedCurve = new AtomicReference<>(); + private static final AtomicReference negotiatedProtocol = new AtomicReference<>(); + private static final AtomicReference negotiatedCipherSuite = new AtomicReference<>(); + + public static void main(String[] args) throws Exception { + System.out.println("[DEBUG] Java Version: " + System.getProperty("java.version")); + System.out.println("[DEBUG] Java Runtime: " + System.getProperty("java.runtime.version")); + System.out.println( + "[DEBUG] Java VM : " + + System.getProperty("java.vm.name") + + " (" + + System.getProperty("java.vm.version") + + ")"); + + String projectId = System.getProperty("project.id"); + if (projectId == null || projectId.isEmpty()) { + projectId = System.getenv("GOOGLE_CLOUD_PROJECT"); + } + if (projectId == null || projectId.isEmpty()) { + try { + projectId = BigQueryOptions.getDefaultInstance().getProjectId(); + } catch (Exception e) { + // Ignore if defaults are not configured + } + } + if (projectId == null || projectId.isEmpty()) { + System.err.println("Error: Google Cloud Project ID could not be resolved automatically."); + System.err.println( + "Please set the GOOGLE_CLOUD_PROJECT environment variable, or configure Application" + + " Default Credentials."); + System.exit(1); + } + + boolean usePqc = isConscryptFunctional(); + HttpTransportOptions transportOptions; + + if (usePqc) { + System.out.println("Conscrypt is functional. Configuring TLS hybrid PQC tracing..."); + // 1. Get the default HttpTransport from standard HttpTransportOptions + HttpTransportOptions defaultOptions = HttpTransportOptions.newBuilder().build(); + HttpTransport defaultTransport = defaultOptions.getHttpTransportFactory().create(); + + // 2. Reflectively extract the SSLSocketFactory configured by gax/core-http + SSLSocketFactory defaultFactory = + (SSLSocketFactory) getPrivateField(defaultTransport, "sslSocketFactory"); + if (defaultFactory == null) { + defaultFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + } + + // 3. Wrap it in our tracing socket factory + SSLSocketFactory tracingFactory = new TracingSSLSocketFactory(defaultFactory); + + // 4. Build a tracing transport using this factory and GAX PQC configuration + NetHttpTransport.Builder tracingBuilder = new NetHttpTransport.Builder(); + tracingBuilder.setSecurityProvider(Conscrypt.newProvider()); + tracingBuilder.setSslSocketConfigurator( + socket -> { + if (Conscrypt.isConscrypt(socket)) { + Conscrypt.setNamedGroups( + socket, + com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider + .DEFAULT_PQC_GROUPS); + } + }); + tracingBuilder.setSslSocketFactory(tracingFactory); + HttpTransport tracingTransport = tracingBuilder.build(); + + // 5. Configure BigQuery client to use this tracing transport + transportOptions = + HttpTransportOptions.newBuilder().setHttpTransportFactory(() -> tracingTransport).build(); + } else { + System.out.println( + "Conscrypt is not functional. Using default transport options (clean fallback check)..."); + transportOptions = HttpTransportOptions.newBuilder().build(); + } + + System.out.println("Initializing default BigQuery client for project: " + projectId); + BigQuery bigquery = + BigQueryOptions.newBuilder() + .setProjectId(projectId) + .setTransportOptions(transportOptions) + .setCredentials(com.google.cloud.NoCredentials.getInstance()) + .build() + .getService(); + + System.out.println("Executing API call to trigger TLS handshake..."); + try { + bigquery.listDatasets(); + } catch (com.google.cloud.bigquery.BigQueryException e) { + if (e.getCode() == 401 + || e.getMessage().contains("missing required authentication credential")) { + System.out.println("TLS connection established. Proceeding with verification..."); + } else { + System.err.println("API call failed with unexpected error code."); + e.printStackTrace(); + System.exit(1); + } + } catch (Exception e) { + System.err.println("Unexpected exception during connection."); + e.printStackTrace(); + System.exit(1); + } + + if (usePqc) { + // Wait a brief moment for asynchronous JSSE listener thread to execute + Thread.sleep(300); + + // Perform Programmatic Assertion on the Negotiated Curve + String curve = negotiatedCurve.get(); + String protocol = negotiatedProtocol.get(); + String cipherSuite = negotiatedCipherSuite.get(); + + System.out.println("\n=================================================="); + System.out.println("TLS Handshake Verification Results:"); + System.out.println(" Protocol : " + protocol); + System.out.println(" Cipher Suite : " + cipherSuite); + System.out.println(" Negotiated KEX: " + curve); + System.out.println("=================================================="); + + if (curve == null) { + System.err.println("ERROR: No TLS handshake was intercepted!"); + System.exit(1); + } + + String expectedCurve = "X25519MLKEM768"; + if (expectedCurve.equalsIgnoreCase(curve)) { + System.out.println( + "VERIFICATION SUCCESS: Key exchange (" + expectedCurve + ") negotiated successfully!"); + } else { + System.err.println( + "VERIFICATION FAILED: Expected Key Exchange " + + expectedCurve + + " but negotiated: " + + curve); + System.exit(1); + } + } else { + System.out.println("\n=================================================="); + System.out.println( + "VERIFICATION SUCCESS: Clean fallback to default JSSE provider verified successfully!"); + System.out.println("=================================================="); + } + } + + private static Object getPrivateField(Object obj, String fieldName) throws Exception { + Class clazz = obj.getClass(); + while (clazz != null) { + try { + java.lang.reflect.Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(obj); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + throw new NoSuchFieldException( + "Field " + fieldName + " not found in class hierarchy of " + obj.getClass()); + } + + private static class TracingSSLSocketFactory extends SSLSocketFactory { + private final SSLSocketFactory delegate; + + public TracingSSLSocketFactory(SSLSocketFactory delegate) { + this.delegate = delegate; + } + + private Socket wrap(Socket socket) { + System.out.println( + "[DEBUG] TracingSSLSocketFactory wrapped socket: " + + (socket == null ? "null" : socket.getClass().getName())); + if (socket instanceof SSLSocket) { + SSLSocket sslSocket = (SSLSocket) socket; + sslSocket.addHandshakeCompletedListener( + event -> { + try { + System.out.println("[DEBUG] HandshakeCompletedListener triggered asynchronously!"); + negotiatedCipherSuite.set(event.getCipherSuite()); + negotiatedProtocol.set(event.getSession().getProtocol()); + Socket rawSocket = event.getSocket(); + + String curve = null; + // Direct Conscrypt check since it is a direct dependency + if (rawSocket instanceof OpenSSLSocketImpl) { + curve = ((OpenSSLSocketImpl) rawSocket).getCurveNameForTesting(); + } + + if (curve != null) { + negotiatedCurve.set(curve); + } + } catch (Exception e) { + System.err.println("Failed to log TLS handshake: " + e.getMessage()); + } + }); + } + return socket; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) + throws IOException { + return wrap(delegate.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket() throws IOException { + return wrap(delegate.createSocket()); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return wrap(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException, UnknownHostException { + return wrap(delegate.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return wrap(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket( + InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return wrap(delegate.createSocket(address, port, localAddress, localPort)); + } + } + + private static boolean isConscryptFunctional() { + try { + org.conscrypt.Conscrypt.newProvider(); + return true; + } catch (Throwable t) { + return false; + } + } +} diff --git a/sdk-platform-java/gax-java/gax-httpjson/pom.xml b/sdk-platform-java/gax-java/gax-httpjson/pom.xml index de839fe6e074..d70dbfe4459b 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/pom.xml +++ b/sdk-platform-java/gax-java/gax-httpjson/pom.xml @@ -104,6 +104,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/InstantiatingHttpJsonChannelProvider.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java index 495fec1ed450..ea5506d2a2bd 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 @@ -51,6 +51,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; +import org.conscrypt.Conscrypt; /** * InstantiatingHttpJsonChannelProvider is a TransportChannelProvider which constructs a {@link @@ -67,6 +68,23 @@ @InternalExtensionOnly public final class InstantiatingHttpJsonChannelProvider implements TransportChannelProvider { + /** + * Default TLS 1.3 Post-Quantum Cryptography (PQC) named groups used when Conscrypt security + * provider is present. + * + *
    + *
  • {@code X25519MLKEM768}: Primary preferred group. Combines Curve25519 ECDHE with NIST FIPS + * 203 (ML-KEM-768) standard. + *
  • {@code SecP256r1MLKEM768}: Secondary preferred group. Combines NIST P-256 (SecP256r1) + * with NIST FIPS 203 (ML-KEM-768) for FIPS compliance. + *
  • {@code X25519Kyber768Draft00}: Legacy pre-FIPS draft fallback for endpoints deployed + * prior to FIPS 203 finalization. + *
  • {@code X25519}: Classical non-quantum key exchange fallback. + *
+ */ + public static final String[] DEFAULT_PQC_GROUPS = + new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519Kyber768Draft00", "X25519"}; + @VisibleForTesting static final Logger LOG = Logger.getLogger(InstantiatingHttpJsonChannelProvider.class.getName()); @@ -191,16 +209,33 @@ public TransportChannelProvider withCredentials(Credentials credentials) { } HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { - if (mtlsProvider == null) { - return null; - } - if (certificateBasedAccess.useMtlsClientCertificate()) { - KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); - if (mtlsKeyStore != null) { - return new NetHttpTransport.Builder().trustCertificates(null, mtlsKeyStore, "").build(); + try { + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); + builder.setSecurityProvider(Conscrypt.newProvider()); + builder.setSslSocketConfigurator( + socket -> { + if (Conscrypt.isConscrypt(socket)) { + Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); + } + }); + if (mtlsProvider != null && certificateBasedAccess.useMtlsClientCertificate()) { + KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); + if (mtlsKeyStore != null) { + builder.trustCertificates(null, mtlsKeyStore, ""); + } + } + return builder.build(); + } catch (Throwable t) { + LOG.log(Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); + NetHttpTransport.Builder fallbackBuilder = new NetHttpTransport.Builder(); + if (mtlsProvider != null && certificateBasedAccess.useMtlsClientCertificate()) { + KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); + if (mtlsKeyStore != null) { + fallbackBuilder.trustCertificates(null, mtlsKeyStore, ""); + } } + return fallbackBuilder.build(); } - return null; } private HttpJsonTransportChannel createChannel() throws IOException, GeneralSecurityException { @@ -364,7 +399,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/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 17ad9f2cbf2b..f29eb34190dd 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 @@ -32,6 +32,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +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; @@ -192,6 +193,44 @@ protected Object getMtlsObjectFromTransportChannel( .setHeaderProvider(Mockito.mock(HeaderProvider.class)) .setExecutor(Mockito.mock(Executor.class)) .build(); - return channelProvider.createHttpTransport(); + NetHttpTransport transport = (NetHttpTransport) channelProvider.createHttpTransport(); + return (transport != null && transport.isMtls()) ? transport : null; + } + + @Test + void testCreateHttpTransport_pqcConfigured() throws Exception { + boolean conscryptLoaded = false; + try { + org.conscrypt.Conscrypt.newProvider(); + conscryptLoaded = true; + } catch (Throwable t) { + // Conscrypt JNI cannot load on this test runner, skipping assertion + } + InstantiatingHttpJsonChannelProvider channelProvider = + InstantiatingHttpJsonChannelProvider.newBuilder() + .setEndpoint("localhost:8080") + .setHeaderProvider(Mockito.mock(HeaderProvider.class)) + .setExecutor(Mockito.mock(Executor.class)) + .build(); + NetHttpTransport transport = (NetHttpTransport) channelProvider.createHttpTransport(); + Object factory = getPrivateField(transport, "sslSocketFactory"); + if (conscryptLoaded) { + assertThat(factory).isNotNull(); + assertThat(factory.getClass().getName()) + .isEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); + Object configurator = getPrivateField(factory, "configurator"); + assertThat(configurator).isNotNull(); + } else { + if (factory != null) { + assertThat(factory.getClass().getName()) + .isNotEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); + } + } + } + + private static Object getPrivateField(Object obj, String fieldName) throws Exception { + java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(obj); } } 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..acc44f34cf7d 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 @@ -26,6 +26,7 @@ import com.google.api.gax.core.GaxProperties; import com.google.api.gax.httpjson.HttpHeadersUtils; import com.google.api.gax.httpjson.HttpJsonStatusCode; +import com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider; import com.google.api.gax.rpc.ApiClientHeaderProvider; import com.google.api.gax.rpc.EndpointContext; import com.google.api.gax.rpc.HeaderProvider; @@ -41,6 +42,7 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.util.Objects; +import org.conscrypt.Conscrypt; /** Class representing service options for those services that use HTTP as the transport layer. */ public class HttpTransportOptions implements TransportOptions { @@ -66,6 +68,27 @@ public HttpTransport create() { // Maybe not on App Engine } } + + // If Conscrypt is available on the classpath, instantiate it as the security provider + // and configure the HTTP client to enable Post-Quantum Cryptography (PQC) named groups + // by default. This ensures that client connections automatically benefit from + // quantum-resistant + // key exchange when Conscrypt is present. + try { + NetHttpTransport.Builder builder = + new NetHttpTransport.Builder().setSecurityProvider(Conscrypt.newProvider()); + builder.setSslSocketConfigurator( + socket -> { + if (Conscrypt.isConscrypt(socket)) { + Conscrypt.setNamedGroups( + socket, InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS); + } + }); + return builder.build(); + } catch (Throwable t) { + // Fallback to standard NetHttpTransport if Conscrypt is not available or failed to load + } + return new NetHttpTransport(); } } From 4c405ea1e1cf9a066f20bd0bacac81da2071751c Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:03:38 +0000 Subject: [PATCH 02/26] refactor(httpjson): restrict ITPqc to HttpJson PQC tests and remove pqc-verification sample module --- .../com/google/showcase/v1beta1/it/ITPqc.java | 185 +---------- pqc-verification/README.md | 83 ----- pqc-verification/pom.xml | 96 ------ .../java/com/google/cloud/pqc/BqPqcTest.java | 291 ------------------ 4 files changed, 9 insertions(+), 646 deletions(-) delete mode 100644 pqc-verification/README.md delete mode 100644 pqc-verification/pom.xml delete mode 100644 pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java index cf068c797712..1a69eeb147f5 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java @@ -22,27 +22,13 @@ import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.util.SslUtils; import com.google.api.gax.core.NoCredentialsProvider; -import com.google.api.gax.grpc.GrpcTransportChannel; import com.google.api.gax.httpjson.HttpJsonMetadata; import com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider; -import com.google.api.gax.rpc.FixedTransportChannelProvider; -import com.google.api.gax.rpc.TransportChannel; 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 io.grpc.Channel; -import io.grpc.ChannelCredentials; -import io.grpc.ClientCall; -import io.grpc.ClientInterceptor; -import io.grpc.ForwardingClientCall; -import io.grpc.ForwardingClientCallListener; -import io.grpc.Grpc; -import io.grpc.ManagedChannel; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.TlsChannelCredentials; import java.io.File; import java.io.InputStream; import java.nio.file.Files; @@ -54,15 +40,14 @@ import java.security.cert.CertificateFactory; import java.util.Collections; import java.util.List; -import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; /** - * Integration tests to verify Post-Quantum Cryptography (PQC) TLS negotiation for both gRPC and - * HTTP/JSON (REST) clients. + * 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 @@ -76,23 +61,9 @@ *

  • {@code x-showcase-tls-client-supported-groups}: The list of groups offered by the client. * * - *

    To enable PQC, Conscrypt must be available on the classpath. - * - *

      - *
    • For gRPC, the shaded Netty transport dynamically registers and uses Conscrypt natively if - * the Conscrypt library is available on the classpath. - *
    • For HTTP/JSON, the {@link NetHttpTransport} automatically registers Conscrypt as a security - * provider dynamically during transport construction. - *
    - * - * Consequently, these tests do not explicitly register Conscrypt in the global JVM provider list - * during setup. - * *

    Verification cases: * *

      - *
    1. {@code testGrpcPqc}: Verifies that gRPC (Netty-shaded) uses Conscrypt and successfully - * negotiates the hybrid post-quantum group {@code X25519MLKEM768}. *
    2. {@code testHttpJsonPqc}: Verifies that HTTP/JSON transport defaults to Conscrypt and * negotiates the hybrid post-quantum group {@code X25519MLKEM768}. *
    3. {@code testHttpJsonPqc_withExplicitSecurityProvider}: Verifies that overriding the @@ -104,7 +75,6 @@ public class ITPqc { // TLS response header names from Showcase server private static final String TLS_GROUP_HEADER = "x-showcase-tls-group"; - private static final String TLS_CIPHER_HEADER = "x-showcase-tls-cipher"; private static final String TLS_SUPPORTED_GROUPS_HEADER = "x-showcase-tls-client-supported-groups"; @@ -135,59 +105,6 @@ static void setUp() { .isTrue(); } - @Test - void testGrpcPqc() throws Exception { - - // Create channel credentials trusting the custom CA - ChannelCredentials creds = - TlsChannelCredentials.newBuilder().trustManager(new File(DEFAULT_CA_CERT_PATH)).build(); - - ManagedChannel channel = Grpc.newChannelBuilder(SECURE_ENDPOINT, creds).build(); - try { - TransportChannel transportChannel = GrpcTransportChannel.create(channel); - - GrpcHeaderCapturingInterceptor interceptor = new GrpcHeaderCapturingInterceptor(); - - EchoSettings settings = - EchoSettings.newBuilder() - .setCredentialsProvider(NoCredentialsProvider.create()) - .setTransportChannelProvider(FixedTransportChannelProvider.create(transportChannel)) - .build(); - - // Add interceptor to capture headers - ManagedChannel interceptedChannel = new InterceptedManagedChannel(channel, interceptor); - TransportChannel interceptedTransportChannel = - GrpcTransportChannel.create(interceptedChannel); - - settings = - settings.toBuilder() - .setTransportChannelProvider( - FixedTransportChannelProvider.create(interceptedTransportChannel)) - .build(); - - try (EchoClient client = EchoClient.create(settings)) { - EchoResponse response = - client.echo(EchoRequest.newBuilder().setContent("pqc-grpc-test").build()); - assertThat(response.getContent()).isEqualTo("pqc-grpc-test"); - - Metadata capturedHeaders = interceptor.getCapturedHeaders(); - assertThat(capturedHeaders).isNotNull(); - - Metadata.Key groupKey = - Metadata.Key.of(TLS_GROUP_HEADER, Metadata.ASCII_STRING_MARSHALLER); - Metadata.Key supportedGroupsKey = - Metadata.Key.of(TLS_SUPPORTED_GROUPS_HEADER, Metadata.ASCII_STRING_MARSHALLER); - - String expectedGroup = isConscryptFunctional() ? "X25519MLKEM768" : "X25519"; - assertThat(capturedHeaders.get(groupKey)).isEqualTo(expectedGroup); - assertThat(capturedHeaders.get(supportedGroupsKey)).isNotNull(); - } - } finally { - channel.shutdown(); - channel.awaitTermination(10, TimeUnit.SECONDS); - } - } - @Test void testHttpJsonPqc() throws Exception { @@ -210,7 +127,13 @@ void testHttpJsonPqc() throws Exception { builder.setSslSocketFactory(sslContext.getSocketFactory()); if (conscryptProvider != null) { - com.google.api.gax.httpjson.ConscryptPqcConfiguratorHelper.configure(builder); + builder.setSslSocketConfigurator( + socket -> { + if (org.conscrypt.Conscrypt.isConscrypt(socket)) { + org.conscrypt.Conscrypt.setNamedGroups( + socket, InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS); + } + }); } else { builder.setSslSocketConfigurator( socket -> { @@ -310,96 +233,6 @@ void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception { } } - /** - * Captures initial TLS response headers (e.g. x-showcase-tls-group) from the gRPC stream. This is - * required because showcase TLS headers are sent as initial headers rather than trailing metadata - * (trailers), which means the shared utility GrpcCapturingClientInterceptor cannot be used (as it - * only intercepts trailers). - */ - private static class GrpcHeaderCapturingInterceptor implements ClientInterceptor { - private Metadata capturedHeaders; - - @Override - public ClientCall interceptCall( - MethodDescriptor method, io.grpc.CallOptions callOptions, Channel next) { - return new ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions)) { - @Override - public void start(Listener responseListener, Metadata headers) { - super.start( - new ForwardingClientCallListener.SimpleForwardingClientCallListener( - responseListener) { - @Override - public void onHeaders(Metadata headers) { - capturedHeaders = headers; - super.onHeaders(headers); - } - }, - headers); - } - }; - } - - public Metadata getCapturedHeaders() { - return capturedHeaders; - } - } - - /** - * Helper class to wrap a standard ManagedChannel with gRPC client interceptors. Since EchoClient - * requires a ManagedChannel (which handles shutdown and awaitTermination lifecycles), but - * ClientInterceptors.intercept() only returns a generic Channel, this class bridges the two by - * forwarding call creation to the intercepted channel, and routing lifecycle calls to the base - * channel. - */ - private static class InterceptedManagedChannel extends ManagedChannel { - private final ManagedChannel delegate; - private final Channel intercepted; - - InterceptedManagedChannel(ManagedChannel delegate, ClientInterceptor... interceptors) { - this.delegate = delegate; - this.intercepted = io.grpc.ClientInterceptors.intercept(delegate, interceptors); - } - - @Override - public ClientCall newCall( - MethodDescriptor methodDescriptor, io.grpc.CallOptions callOptions) { - return intercepted.newCall(methodDescriptor, callOptions); - } - - @Override - public String authority() { - return delegate.authority(); - } - - @Override - public ManagedChannel shutdown() { - delegate.shutdown(); - return this; - } - - @Override - public boolean isShutdown() { - return delegate.isShutdown(); - } - - @Override - public boolean isTerminated() { - return delegate.isTerminated(); - } - - @Override - public ManagedChannel shutdownNow() { - delegate.shutdownNow(); - return this; - } - - @Override - public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { - return delegate.awaitTermination(timeout, unit); - } - } - private static String getSingleHeaderString(HttpJsonMetadata metadata, String name) { Object valueObj = metadata.getHeaders().get(name); if (valueObj instanceof List) { diff --git a/pqc-verification/README.md b/pqc-verification/README.md deleted file mode 100644 index 2231238180ba..000000000000 --- a/pqc-verification/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# GAPIC Post-Quantum Cryptography (PQC) Support & Verification - -This directory contains verification tools and samples to test, trace, and verify Post-Quantum Cryptography (PQC) support in Google Cloud Java client libraries, covering both gRPC and HttpJson (REST) transports. - ---- - -## 1. Prerequisites & Dependencies - -### Java Version Compatibility & JDK 8 Support -* **Build Environment**: JDK 11+ is required if compiling Conscrypt from source. JDK 17+ or JDK 21+ is recommended for modern development. -* **JDK 8 Runtime Support**: Applications running on **JDK 8** are fully supported at runtime. The `conscrypt-openjdk-uber` dependency targets Java 8 bytecode compatibility and bundles pre-compiled BoringSSL native binaries for all major OS architectures. When Conscrypt is registered as a Security Provider on Java 8, TLS 1.3 PQC handshakes (`X25519MLKEM768`, `SecP256r1MLKEM768`) execute natively at the C/C++ layer without requiring JDK JSSE upgrades. - -### Core Production Dependencies -PQC support is built natively into the core transport libraries: -1. **`google-http-java-client`** (`v2.2.0`): Provides `.setSslSocketConfigurator(...)` on `NetHttpTransport.Builder` to configure Conscrypt PQC named groups. -2. **`gax-httpjson`** (`GAX`): Configures `NetHttpTransport.Builder` with Conscrypt security provider and `InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS` by default. -3. **`google-cloud-core-http`**: Enables Conscrypt security provider and `DEFAULT_PQC_GROUPS` by default for handwritten client transports. - ---- - -## 2. Running Showcase Integration Tests (Local TLS Server) - -The `ITPqc` test suite runs integration tests against a local secure **GAPIC Showcase** server to verify end-to-end PQC handshake negotiation. - -### Step 2.1: Build Showcase Server -Clone the showcase server and checkout the PQC TLS support branch: -```shell -git clone https://github.com/googleapis/gapic-showcase.git -cd gapic-showcase -git checkout feat-pqc-tls -go build ./cmd/gapic-showcase -``` - -### Step 2.2: Run the Showcase Server with Auto-TLS -Start the Showcase server in Auto-TLS mode to generate a local CA certificate: - -```shell -./gapic-showcase run \ - --port 7470 \ - --tls \ - --ca-cert-output-file /tmp/showcase-ca.pem -``` - -### Step 2.3: Execute Integration Tests -Run the Showcase PQC integration test suite: -```shell -mvn test -pl java-showcase/gapic-showcase -Dtest=ITPqc -Dshowcase.ca.cert.path=/tmp/showcase-ca.pem -``` - ---- - -## 3. Standalone BigQuery PQC Verification Sample - -The sample `BqPqcTest` runs a connection against Google Cloud BigQuery using default client settings and intercepts the TLS handshake to programmatically verify that PQC (`X25519MLKEM768`) is negotiated. - -### Run the Sample -You can run the sample directly via Maven from the repository root: - -```shell -# Run using exec-maven-plugin -mvn clean compile exec:java -f pqc-verification/pom.xml -``` - -### Expected Output -The program will automatically trace the TLS handshake and print the negotiated protocol and key exchange group: - -``` -[DEBUG] Java Version: 17.0.19 -[DEBUG] Java Runtime: 17.0.19+10 -[DEBUG] Java VM : OpenJDK 64-Bit Server VM (17.0.19+10) -Conscrypt is functional. Configuring TLS hybrid PQC tracing... -Initializing default BigQuery client for project: your-gcp-project-id -Executing API call to trigger TLS handshake... -TLS connection established. Proceeding with verification... - -================================================== -TLS Handshake Verification Results: - Protocol : TLSv1.3 - Cipher Suite : TLS_AES_128_GCM_SHA256 - Negotiated KEX: X25519MLKEM768 -================================================== -VERIFICATION SUCCESS: Key exchange (X25519MLKEM768) negotiated successfully! -``` diff --git a/pqc-verification/pom.xml b/pqc-verification/pom.xml deleted file mode 100644 index 221f93b72dca..000000000000 --- a/pqc-verification/pom.xml +++ /dev/null @@ -1,96 +0,0 @@ - - - - 4.0.0 - - com.google.cloud.pqc - pqc-verification - 1.0-SNAPSHOT - - - 17 - 17 - UTF-8 - 2.69.0-SNAPSHOT - 2.2.0 - 2.6.0 - - - - - - com.google.cloud - google-cloud-bigquery - ${bigquery.version} - - - - - com.google.http-client - google-http-client - ${http-client.version} - - - - - org.conscrypt - conscrypt-openjdk-uber - ${conscrypt.version} - - - - - - sonatype-central-snapshots - Sonatype Central Snapshots - https://central.sonatype.com/repository/maven-snapshots/ - - false - - - true - - - - sonatype-snapshots - Sonatype Snapshots - https://oss.sonatype.org/content/repositories/snapshots - - false - - - true - - - - - - - - - org.codehaus.mojo - exec-maven-plugin - 3.1.0 - - com.google.cloud.pqc.BqPqcTest - - - - - diff --git a/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java b/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java deleted file mode 100644 index d2e42cb6217b..000000000000 --- a/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * 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.cloud.pqc; - -import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.cloud.bigquery.BigQuery; -import com.google.cloud.bigquery.BigQueryOptions; -import com.google.cloud.http.HttpTransportOptions; -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.net.UnknownHostException; -import java.util.concurrent.atomic.AtomicReference; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import org.conscrypt.Conscrypt; -import org.conscrypt.OpenSSLSocketImpl; - -/** - * A verification sample to programmatically trace and assert TLS handshake details (protocol, - * cipher suite, and negotiated curve) for Google Cloud BigQuery client calls, verifying that PQC - * (X25519MLKEM768) is negotiated. - */ -public class BqPqcTest { - - private static final String EXPECTED_PQC_CURVE = "X25519MLKEM768"; - private static final AtomicReference negotiatedCurve = new AtomicReference<>(); - private static final AtomicReference negotiatedProtocol = new AtomicReference<>(); - private static final AtomicReference negotiatedCipherSuite = new AtomicReference<>(); - - public static void main(String[] args) throws Exception { - System.out.println("[DEBUG] Java Version: " + System.getProperty("java.version")); - System.out.println("[DEBUG] Java Runtime: " + System.getProperty("java.runtime.version")); - System.out.println( - "[DEBUG] Java VM : " - + System.getProperty("java.vm.name") - + " (" - + System.getProperty("java.vm.version") - + ")"); - - String projectId = System.getProperty("project.id"); - if (projectId == null || projectId.isEmpty()) { - projectId = System.getenv("GOOGLE_CLOUD_PROJECT"); - } - if (projectId == null || projectId.isEmpty()) { - try { - projectId = BigQueryOptions.getDefaultInstance().getProjectId(); - } catch (Exception e) { - // Ignore if defaults are not configured - } - } - if (projectId == null || projectId.isEmpty()) { - System.err.println("Error: Google Cloud Project ID could not be resolved automatically."); - System.err.println( - "Please set the GOOGLE_CLOUD_PROJECT environment variable, or configure Application" - + " Default Credentials."); - System.exit(1); - } - - boolean usePqc = isConscryptFunctional(); - HttpTransportOptions transportOptions; - - if (usePqc) { - System.out.println("Conscrypt is functional. Configuring TLS hybrid PQC tracing..."); - // 1. Get the default HttpTransport from standard HttpTransportOptions - HttpTransportOptions defaultOptions = HttpTransportOptions.newBuilder().build(); - HttpTransport defaultTransport = defaultOptions.getHttpTransportFactory().create(); - - // 2. Reflectively extract the SSLSocketFactory configured by gax/core-http - SSLSocketFactory defaultFactory = - (SSLSocketFactory) getPrivateField(defaultTransport, "sslSocketFactory"); - if (defaultFactory == null) { - defaultFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); - } - - // 3. Wrap it in our tracing socket factory - SSLSocketFactory tracingFactory = new TracingSSLSocketFactory(defaultFactory); - - // 4. Build a tracing transport using this factory and GAX PQC configuration - NetHttpTransport.Builder tracingBuilder = new NetHttpTransport.Builder(); - tracingBuilder.setSecurityProvider(Conscrypt.newProvider()); - tracingBuilder.setSslSocketConfigurator( - socket -> { - if (Conscrypt.isConscrypt(socket)) { - Conscrypt.setNamedGroups( - socket, - com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider - .DEFAULT_PQC_GROUPS); - } - }); - tracingBuilder.setSslSocketFactory(tracingFactory); - HttpTransport tracingTransport = tracingBuilder.build(); - - // 5. Configure BigQuery client to use this tracing transport - transportOptions = - HttpTransportOptions.newBuilder().setHttpTransportFactory(() -> tracingTransport).build(); - } else { - System.out.println( - "Conscrypt is not functional. Using default transport options (clean fallback check)..."); - transportOptions = HttpTransportOptions.newBuilder().build(); - } - - System.out.println("Initializing default BigQuery client for project: " + projectId); - BigQuery bigquery = - BigQueryOptions.newBuilder() - .setProjectId(projectId) - .setTransportOptions(transportOptions) - .setCredentials(com.google.cloud.NoCredentials.getInstance()) - .build() - .getService(); - - System.out.println("Executing API call to trigger TLS handshake..."); - try { - bigquery.listDatasets(); - } catch (com.google.cloud.bigquery.BigQueryException e) { - if (e.getCode() == 401 - || e.getMessage().contains("missing required authentication credential")) { - System.out.println("TLS connection established. Proceeding with verification..."); - } else { - System.err.println("API call failed with unexpected error code."); - e.printStackTrace(); - System.exit(1); - } - } catch (Exception e) { - System.err.println("Unexpected exception during connection."); - e.printStackTrace(); - System.exit(1); - } - - if (usePqc) { - // Wait a brief moment for asynchronous JSSE listener thread to execute - Thread.sleep(300); - - // Perform Programmatic Assertion on the Negotiated Curve - String curve = negotiatedCurve.get(); - String protocol = negotiatedProtocol.get(); - String cipherSuite = negotiatedCipherSuite.get(); - - System.out.println("\n=================================================="); - System.out.println("TLS Handshake Verification Results:"); - System.out.println(" Protocol : " + protocol); - System.out.println(" Cipher Suite : " + cipherSuite); - System.out.println(" Negotiated KEX: " + curve); - System.out.println("=================================================="); - - if (curve == null) { - System.err.println("ERROR: No TLS handshake was intercepted!"); - System.exit(1); - } - - String expectedCurve = "X25519MLKEM768"; - if (expectedCurve.equalsIgnoreCase(curve)) { - System.out.println( - "VERIFICATION SUCCESS: Key exchange (" + expectedCurve + ") negotiated successfully!"); - } else { - System.err.println( - "VERIFICATION FAILED: Expected Key Exchange " - + expectedCurve - + " but negotiated: " - + curve); - System.exit(1); - } - } else { - System.out.println("\n=================================================="); - System.out.println( - "VERIFICATION SUCCESS: Clean fallback to default JSSE provider verified successfully!"); - System.out.println("=================================================="); - } - } - - private static Object getPrivateField(Object obj, String fieldName) throws Exception { - Class clazz = obj.getClass(); - while (clazz != null) { - try { - java.lang.reflect.Field field = clazz.getDeclaredField(fieldName); - field.setAccessible(true); - return field.get(obj); - } catch (NoSuchFieldException e) { - clazz = clazz.getSuperclass(); - } - } - throw new NoSuchFieldException( - "Field " + fieldName + " not found in class hierarchy of " + obj.getClass()); - } - - private static class TracingSSLSocketFactory extends SSLSocketFactory { - private final SSLSocketFactory delegate; - - public TracingSSLSocketFactory(SSLSocketFactory delegate) { - this.delegate = delegate; - } - - private Socket wrap(Socket socket) { - System.out.println( - "[DEBUG] TracingSSLSocketFactory wrapped socket: " - + (socket == null ? "null" : socket.getClass().getName())); - if (socket instanceof SSLSocket) { - SSLSocket sslSocket = (SSLSocket) socket; - sslSocket.addHandshakeCompletedListener( - event -> { - try { - System.out.println("[DEBUG] HandshakeCompletedListener triggered asynchronously!"); - negotiatedCipherSuite.set(event.getCipherSuite()); - negotiatedProtocol.set(event.getSession().getProtocol()); - Socket rawSocket = event.getSocket(); - - String curve = null; - // Direct Conscrypt check since it is a direct dependency - if (rawSocket instanceof OpenSSLSocketImpl) { - curve = ((OpenSSLSocketImpl) rawSocket).getCurveNameForTesting(); - } - - if (curve != null) { - negotiatedCurve.set(curve); - } - } catch (Exception e) { - System.err.println("Failed to log TLS handshake: " + e.getMessage()); - } - }); - } - return socket; - } - - @Override - public String[] getDefaultCipherSuites() { - return delegate.getDefaultCipherSuites(); - } - - @Override - public String[] getSupportedCipherSuites() { - return delegate.getSupportedCipherSuites(); - } - - @Override - public Socket createSocket(Socket s, String host, int port, boolean autoClose) - throws IOException { - return wrap(delegate.createSocket(s, host, port, autoClose)); - } - - @Override - public Socket createSocket() throws IOException { - return wrap(delegate.createSocket()); - } - - @Override - public Socket createSocket(String host, int port) throws IOException, UnknownHostException { - return wrap(delegate.createSocket(host, port)); - } - - @Override - public Socket createSocket(String host, int port, InetAddress localHost, int localPort) - throws IOException, UnknownHostException { - return wrap(delegate.createSocket(host, port, localHost, localPort)); - } - - @Override - public Socket createSocket(InetAddress host, int port) throws IOException { - return wrap(delegate.createSocket(host, port)); - } - - @Override - public Socket createSocket( - InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { - return wrap(delegate.createSocket(address, port, localAddress, localPort)); - } - } - - private static boolean isConscryptFunctional() { - try { - org.conscrypt.Conscrypt.newProvider(); - return true; - } catch (Throwable t) { - return false; - } - } -} From 0d7f936e82644ca9e1713d128bddce77067f4ea0 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:09:03 +0000 Subject: [PATCH 03/26] refactor(httpjson): remove X25519Kyber768Draft00 from PQC groups and streamline Conscrypt try-fallback --- .../InstantiatingHttpJsonChannelProvider.java | 40 +++++++++---------- ...tantiatingHttpJsonChannelProviderTest.java | 25 ++++++------ .../cloud/http/HttpTransportOptions.java | 24 ++++++----- 3 files changed, 46 insertions(+), 43 deletions(-) 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 ea5506d2a2bd..815128fa08e0 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 @@ -83,7 +83,7 @@ public final class InstantiatingHttpJsonChannelProvider implements TransportChan * */ public static final String[] DEFAULT_PQC_GROUPS = - new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519Kyber768Draft00", "X25519"}; + new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519"}; @VisibleForTesting static final Logger LOG = Logger.getLogger(InstantiatingHttpJsonChannelProvider.class.getName()); @@ -209,33 +209,31 @@ public TransportChannelProvider withCredentials(Credentials credentials) { } HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); try { - NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); builder.setSecurityProvider(Conscrypt.newProvider()); - builder.setSslSocketConfigurator( - socket -> { + } catch (Throwable t) { + LOG.log(Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); + } + + builder.setSslSocketConfigurator( + socket -> { + try { if (Conscrypt.isConscrypt(socket)) { Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); } - }); - if (mtlsProvider != null && certificateBasedAccess.useMtlsClientCertificate()) { - KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); - if (mtlsKeyStore != null) { - builder.trustCertificates(null, mtlsKeyStore, ""); - } - } - return builder.build(); - } catch (Throwable t) { - LOG.log(Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); - NetHttpTransport.Builder fallbackBuilder = new NetHttpTransport.Builder(); - if (mtlsProvider != null && certificateBasedAccess.useMtlsClientCertificate()) { - KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); - if (mtlsKeyStore != null) { - fallbackBuilder.trustCertificates(null, mtlsKeyStore, ""); - } + } catch (Throwable t) { + // Conscrypt not available or socket is standard JDK TLS socket + } + }); + + if (mtlsProvider != null && certificateBasedAccess.useMtlsClientCertificate()) { + KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); + if (mtlsKeyStore != null) { + builder.trustCertificates(null, mtlsKeyStore, ""); } - return fallbackBuilder.build(); } + return builder.build(); } private HttpJsonTransportChannel createChannel() throws IOException, GeneralSecurityException { 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 f29eb34190dd..4c76941175cd 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 @@ -214,18 +214,19 @@ void testCreateHttpTransport_pqcConfigured() throws Exception { .build(); NetHttpTransport transport = (NetHttpTransport) channelProvider.createHttpTransport(); Object factory = getPrivateField(transport, "sslSocketFactory"); - if (conscryptLoaded) { - assertThat(factory).isNotNull(); - assertThat(factory.getClass().getName()) - .isEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); - Object configurator = getPrivateField(factory, "configurator"); - assertThat(configurator).isNotNull(); - } else { - if (factory != null) { - assertThat(factory.getClass().getName()) - .isNotEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); - } - } + assertThat(factory).isNotNull(); + assertThat(factory.getClass().getName()) + .isEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); + Object configurator = getPrivateField(factory, "configurator"); + assertThat(configurator).isNotNull(); + } + + @Test + void testDefaultPqcGroups_containsExpectedGroups() { + assertThat(InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS) + .asList() + .containsExactly("X25519MLKEM768", "SecP256r1MLKEM768", "X25519") + .inOrder(); } private static Object getPrivateField(Object obj, String fieldName) throws Exception { 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 acc44f34cf7d..f5a300424713 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 @@ -74,22 +74,26 @@ public HttpTransport create() { // by default. This ensures that client connections automatically benefit from // quantum-resistant // key exchange when Conscrypt is present. + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); try { - NetHttpTransport.Builder builder = - new NetHttpTransport.Builder().setSecurityProvider(Conscrypt.newProvider()); - builder.setSslSocketConfigurator( - socket -> { + builder.setSecurityProvider(Conscrypt.newProvider()); + } catch (Throwable t) { + // Conscrypt native libraries not available, fallback to standard JDK TLS + } + + builder.setSslSocketConfigurator( + socket -> { + try { if (Conscrypt.isConscrypt(socket)) { Conscrypt.setNamedGroups( socket, InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS); } - }); - return builder.build(); - } catch (Throwable t) { - // Fallback to standard NetHttpTransport if Conscrypt is not available or failed to load - } + } catch (Throwable t) { + // Conscrypt not available or socket is standard JDK TLS socket + } + }); - return new NetHttpTransport(); + return builder.build(); } } From aa2c841521e892f775831671c8bacb4f34f197f2 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:17:44 +0000 Subject: [PATCH 04/26] refactor(httpjson): group setSecurityProvider and setSslSocketConfigurator in try block and remove draft Javadoc --- .../InstantiatingHttpJsonChannelProvider.java | 19 ++++++------------- ...tantiatingHttpJsonChannelProviderTest.java | 17 ++++++++++++----- .../cloud/http/HttpTransportOptions.java | 17 ++++++----------- 3 files changed, 24 insertions(+), 29 deletions(-) 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 815128fa08e0..f4f1a4ec7c3f 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 @@ -77,8 +77,6 @@ public final class InstantiatingHttpJsonChannelProvider implements TransportChan * 203 (ML-KEM-768) standard. *
    4. {@code SecP256r1MLKEM768}: Secondary preferred group. Combines NIST P-256 (SecP256r1) * with NIST FIPS 203 (ML-KEM-768) for FIPS compliance. - *
    5. {@code X25519Kyber768Draft00}: Legacy pre-FIPS draft fallback for endpoints deployed - * prior to FIPS 203 finalization. *
    6. {@code X25519}: Classical non-quantum key exchange fallback. * */ @@ -212,20 +210,15 @@ HttpTransport createHttpTransport() throws IOException, GeneralSecurityException NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); try { builder.setSecurityProvider(Conscrypt.newProvider()); - } catch (Throwable t) { - LOG.log(Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); - } - - builder.setSslSocketConfigurator( - socket -> { - try { + builder.setSslSocketConfigurator( + socket -> { if (Conscrypt.isConscrypt(socket)) { Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); } - } catch (Throwable t) { - // Conscrypt not available or socket is standard JDK TLS socket - } - }); + }); + } catch (Throwable t) { + LOG.log(Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); + } if (mtlsProvider != null && certificateBasedAccess.useMtlsClientCertificate()) { KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); 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 4c76941175cd..d8bbe70bf4c5 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 @@ -214,11 +214,18 @@ void testCreateHttpTransport_pqcConfigured() throws Exception { .build(); NetHttpTransport transport = (NetHttpTransport) channelProvider.createHttpTransport(); Object factory = getPrivateField(transport, "sslSocketFactory"); - assertThat(factory).isNotNull(); - assertThat(factory.getClass().getName()) - .isEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); - Object configurator = getPrivateField(factory, "configurator"); - assertThat(configurator).isNotNull(); + if (conscryptLoaded) { + assertThat(factory).isNotNull(); + assertThat(factory.getClass().getName()) + .isEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); + Object configurator = getPrivateField(factory, "configurator"); + assertThat(configurator).isNotNull(); + } else { + if (factory != null) { + assertThat(factory.getClass().getName()) + .isNotEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); + } + } } @Test 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 f5a300424713..89ca0032e91f 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 @@ -77,21 +77,16 @@ public HttpTransport create() { NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); try { builder.setSecurityProvider(Conscrypt.newProvider()); - } catch (Throwable t) { - // Conscrypt native libraries not available, fallback to standard JDK TLS - } - - builder.setSslSocketConfigurator( - socket -> { - try { + builder.setSslSocketConfigurator( + socket -> { if (Conscrypt.isConscrypt(socket)) { Conscrypt.setNamedGroups( socket, InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS); } - } catch (Throwable t) { - // Conscrypt not available or socket is standard JDK TLS socket - } - }); + }); + } catch (Throwable t) { + // Conscrypt native libraries not available, fallback to standard JDK TLS + } return builder.build(); } From 2fb7f15ed83dc8dce93f52f93b360d30d8a82dbe Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:23:50 +0000 Subject: [PATCH 05/26] docs(pqc): add detailed explanatory comments above Conscrypt try-catch blocks in GAX and Core HTTP --- .../InstantiatingHttpJsonChannelProvider.java | 11 +++++++++++ .../google/cloud/http/HttpTransportOptions.java | 17 ++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) 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 f4f1a4ec7c3f..dcdd9dc6cb33 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 @@ -208,6 +208,17 @@ public TransportChannelProvider withCredentials(Credentials credentials) { HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); + // Attempt to register Conscrypt as the Security Provider for HTTP/JSON connections to enable + // Post-Quantum Cryptography (PQC) hybrid key exchange (e.g. X25519MLKEM768) by default. + // + // Both setSecurityProvider and setSslSocketConfigurator are configured together inside the + // try block so that socket named group configuration is only applied when Conscrypt + // initialization + // succeeds. + // + // Catching Throwable ensures that if Conscrypt JNI native libraries are not present on the + // classpath or fail to load on the target host architecture, transport creation gracefully + // falls back to standard JDK TLS JSSE provider without interrupting application execution. try { builder.setSecurityProvider(Conscrypt.newProvider()); builder.setSslSocketConfigurator( 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 89ca0032e91f..28a412e4b318 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 @@ -69,12 +69,19 @@ public HttpTransport create() { } } - // If Conscrypt is available on the classpath, instantiate it as the security provider - // and configure the HTTP client to enable Post-Quantum Cryptography (PQC) named groups - // by default. This ensures that client connections automatically benefit from - // quantum-resistant - // key exchange when Conscrypt is present. NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); + // Attempt to register Conscrypt as the Security Provider for HTTP client connections to + // enable + // Post-Quantum Cryptography (PQC) hybrid key exchange (e.g. X25519MLKEM768) by default. + // + // Both setSecurityProvider and setSslSocketConfigurator are configured together inside the + // try block so that socket named group configuration is only applied when Conscrypt + // initialization + // succeeds. + // + // Catching Throwable ensures that if Conscrypt JNI native libraries are not present on the + // classpath or fail to load on the target host architecture, transport creation gracefully + // falls back to standard JDK TLS JSSE provider without interrupting application execution. try { builder.setSecurityProvider(Conscrypt.newProvider()); builder.setSslSocketConfigurator( From dde825446357b12dc448ce80f16a6f3f90a086a9 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:27:40 +0000 Subject: [PATCH 06/26] docs(pqc): add Level.FINE logger to HttpTransportOptions when Conscrypt is unavailable --- .../java/com/google/cloud/http/HttpTransportOptions.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 28a412e4b318..54c7a0acdc25 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 @@ -42,6 +42,8 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; import org.conscrypt.Conscrypt; /** Class representing service options for those services that use HTTP as the transport layer. */ @@ -56,6 +58,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 @@ -92,7 +95,8 @@ public HttpTransport create() { } }); } catch (Throwable t) { - // Conscrypt native libraries not available, fallback to standard JDK TLS + LOG.log( + Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); } return builder.build(); From 8ba0498c765a33e698c053c5ba94a17d89bddd96 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:30:08 +0000 Subject: [PATCH 07/26] test(httpjson): remove reflection helper from InstantiatingHttpJsonChannelProviderTest --- ...tantiatingHttpJsonChannelProviderTest.java | 29 ++----------------- 1 file changed, 2 insertions(+), 27 deletions(-) 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 d8bbe70bf4c5..e6fcbc7b2bf5 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 @@ -198,14 +198,7 @@ protected Object getMtlsObjectFromTransportChannel( } @Test - void testCreateHttpTransport_pqcConfigured() throws Exception { - boolean conscryptLoaded = false; - try { - org.conscrypt.Conscrypt.newProvider(); - conscryptLoaded = true; - } catch (Throwable t) { - // Conscrypt JNI cannot load on this test runner, skipping assertion - } + void testCreateHttpTransport_returnsValidTransport() throws Exception { InstantiatingHttpJsonChannelProvider channelProvider = InstantiatingHttpJsonChannelProvider.newBuilder() .setEndpoint("localhost:8080") @@ -213,19 +206,7 @@ void testCreateHttpTransport_pqcConfigured() throws Exception { .setExecutor(Mockito.mock(Executor.class)) .build(); NetHttpTransport transport = (NetHttpTransport) channelProvider.createHttpTransport(); - Object factory = getPrivateField(transport, "sslSocketFactory"); - if (conscryptLoaded) { - assertThat(factory).isNotNull(); - assertThat(factory.getClass().getName()) - .isEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); - Object configurator = getPrivateField(factory, "configurator"); - assertThat(configurator).isNotNull(); - } else { - if (factory != null) { - assertThat(factory.getClass().getName()) - .isNotEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); - } - } + assertThat(transport).isNotNull(); } @Test @@ -235,10 +216,4 @@ void testDefaultPqcGroups_containsExpectedGroups() { .containsExactly("X25519MLKEM768", "SecP256r1MLKEM768", "X25519") .inOrder(); } - - private static Object getPrivateField(Object obj, String fieldName) throws Exception { - java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return field.get(obj); - } } From 4b38082d9a0c8630b73e5c843ae6ed54d95b2930 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:36:28 +0000 Subject: [PATCH 08/26] test(pqc): add unit tests for PQC Conscrypt transport configuration in GAX and Core HTTP --- ...tantiatingHttpJsonChannelProviderTest.java | 23 +++++++++++- .../cloud/http/HttpTransportOptionsTest.java | 36 +++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) 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 e6fcbc7b2bf5..35458c1e0f12 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 @@ -198,7 +198,14 @@ protected Object getMtlsObjectFromTransportChannel( } @Test - void testCreateHttpTransport_returnsValidTransport() throws Exception { + void testCreateHttpTransport_pqcConfigured() throws Exception { + boolean conscryptAvailable = false; + try { + org.conscrypt.Conscrypt.newProvider(); + conscryptAvailable = org.conscrypt.Conscrypt.isAvailable(); + } catch (Throwable t) { + // Conscrypt JNI native shared library is not available on this host environment + } InstantiatingHttpJsonChannelProvider channelProvider = InstantiatingHttpJsonChannelProvider.newBuilder() .setEndpoint("localhost:8080") @@ -207,6 +214,14 @@ void testCreateHttpTransport_returnsValidTransport() throws Exception { .build(); NetHttpTransport transport = (NetHttpTransport) channelProvider.createHttpTransport(); assertThat(transport).isNotNull(); + if (conscryptAvailable) { + Object factory = getPrivateField(transport, "sslSocketFactory"); + assertThat(factory).isNotNull(); + assertThat(factory.getClass().getName()) + .isEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); + Object configurator = getPrivateField(factory, "configurator"); + assertThat(configurator).isNotNull(); + } } @Test @@ -216,4 +231,10 @@ void testDefaultPqcGroups_containsExpectedGroups() { .containsExactly("X25519MLKEM768", "SecP256r1MLKEM768", "X25519") .inOrder(); } + + private static Object getPrivateField(Object obj, String fieldName) throws Exception { + java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(obj); + } } diff --git a/sdk-platform-java/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java b/sdk-platform-java/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java index 1697a0c43f41..421316fd465d 100644 --- a/sdk-platform-java/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java +++ b/sdk-platform-java/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java @@ -118,6 +118,30 @@ void testBuilder() { assertEquals(-1, DEFAULT_OPTIONS.getReadTimeout()); } + @Test + void testDefaultHttpTransportFactory_createPqcConfiguredTransport() throws Exception { + boolean conscryptAvailable = false; + try { + org.conscrypt.Conscrypt.newProvider(); + conscryptAvailable = org.conscrypt.Conscrypt.isAvailable(); + } catch (Throwable t) { + // Conscrypt JNI native shared library is not available on this host environment + } + DefaultHttpTransportFactory factory = new DefaultHttpTransportFactory(); + HttpTransport transport = factory.create(); + assertTrue(transport instanceof com.google.api.client.http.javanet.NetHttpTransport); + if (conscryptAvailable) { + java.lang.reflect.Field field = + com.google.api.client.http.javanet.NetHttpTransport.class.getDeclaredField( + "sslSocketFactory"); + field.setAccessible(true); + Object sslSocketFactory = field.get(transport); + assertEquals( + "com.google.api.client.http.javanet.ConfigurableSSLSocketFactory", + sslSocketFactory.getClass().getName()); + } + } + @Test void testBaseEquals() { assertEquals(OPTIONS, OPTIONS_COPY); @@ -166,7 +190,9 @@ void testHttpRequestInitializer_defaultUniverseDomainSettings_customCredentials( UnauthenticatedException.class, () -> httpRequestInitializer.initialize(defaultHttpRequest)); assertEquals( - "The configured universe domain (googleapis.com) does not match the universe domain found in the credentials (random.com). If you haven't configured the universe domain explicitly, `googleapis.com` is the default.", + "The configured universe domain (googleapis.com) does not match the universe domain found" + + " in the credentials (random.com). If you haven't configured the universe domain" + + " explicitly, `googleapis.com` is the default.", exception.getCause().getMessage()); } @@ -181,7 +207,9 @@ void testHttpRequestInitializer_customUniverseDomainSettings_defaultCredentials( UnauthenticatedException.class, () -> httpRequestInitializer.initialize(defaultHttpRequest)); assertEquals( - "The configured universe domain (random.com) does not match the universe domain found in the credentials (googleapis.com). If you haven't configured the universe domain explicitly, `googleapis.com` is the default.", + "The configured universe domain (random.com) does not match the universe domain found in" + + " the credentials (googleapis.com). If you haven't configured the universe domain" + + " explicitly, `googleapis.com` is the default.", exception.getCause().getMessage()); } @@ -219,7 +247,9 @@ void testHttpRequestInitializer_customUniverseDomainSettings_noCredentials() { UnauthenticatedException.class, () -> httpRequestInitializer.initialize(defaultHttpRequest)); assertEquals( - "The configured universe domain (random.com) does not match the universe domain found in the credentials (googleapis.com). If you haven't configured the universe domain explicitly, `googleapis.com` is the default.", + "The configured universe domain (random.com) does not match the universe domain found in" + + " the credentials (googleapis.com). If you haven't configured the universe domain" + + " explicitly, `googleapis.com` is the default.", exception.getCause().getMessage()); } From 02c18d180e78dc9613abfa86902ec754e5c994b4 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:38:52 +0000 Subject: [PATCH 09/26] test(pqc): clean up reflection tests from InstantiatingHttpJsonChannelProviderTest and HttpTransportOptionsTest --- ...tantiatingHttpJsonChannelProviderTest.java | 23 +----------------- .../cloud/http/HttpTransportOptionsTest.java | 24 ------------------- 2 files changed, 1 insertion(+), 46 deletions(-) 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 35458c1e0f12..e6fcbc7b2bf5 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 @@ -198,14 +198,7 @@ protected Object getMtlsObjectFromTransportChannel( } @Test - void testCreateHttpTransport_pqcConfigured() throws Exception { - boolean conscryptAvailable = false; - try { - org.conscrypt.Conscrypt.newProvider(); - conscryptAvailable = org.conscrypt.Conscrypt.isAvailable(); - } catch (Throwable t) { - // Conscrypt JNI native shared library is not available on this host environment - } + void testCreateHttpTransport_returnsValidTransport() throws Exception { InstantiatingHttpJsonChannelProvider channelProvider = InstantiatingHttpJsonChannelProvider.newBuilder() .setEndpoint("localhost:8080") @@ -214,14 +207,6 @@ void testCreateHttpTransport_pqcConfigured() throws Exception { .build(); NetHttpTransport transport = (NetHttpTransport) channelProvider.createHttpTransport(); assertThat(transport).isNotNull(); - if (conscryptAvailable) { - Object factory = getPrivateField(transport, "sslSocketFactory"); - assertThat(factory).isNotNull(); - assertThat(factory.getClass().getName()) - .isEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); - Object configurator = getPrivateField(factory, "configurator"); - assertThat(configurator).isNotNull(); - } } @Test @@ -231,10 +216,4 @@ void testDefaultPqcGroups_containsExpectedGroups() { .containsExactly("X25519MLKEM768", "SecP256r1MLKEM768", "X25519") .inOrder(); } - - private static Object getPrivateField(Object obj, String fieldName) throws Exception { - java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return field.get(obj); - } } diff --git a/sdk-platform-java/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java b/sdk-platform-java/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java index 421316fd465d..b523a2e9af60 100644 --- a/sdk-platform-java/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java +++ b/sdk-platform-java/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java @@ -118,30 +118,6 @@ void testBuilder() { assertEquals(-1, DEFAULT_OPTIONS.getReadTimeout()); } - @Test - void testDefaultHttpTransportFactory_createPqcConfiguredTransport() throws Exception { - boolean conscryptAvailable = false; - try { - org.conscrypt.Conscrypt.newProvider(); - conscryptAvailable = org.conscrypt.Conscrypt.isAvailable(); - } catch (Throwable t) { - // Conscrypt JNI native shared library is not available on this host environment - } - DefaultHttpTransportFactory factory = new DefaultHttpTransportFactory(); - HttpTransport transport = factory.create(); - assertTrue(transport instanceof com.google.api.client.http.javanet.NetHttpTransport); - if (conscryptAvailable) { - java.lang.reflect.Field field = - com.google.api.client.http.javanet.NetHttpTransport.class.getDeclaredField( - "sslSocketFactory"); - field.setAccessible(true); - Object sslSocketFactory = field.get(transport); - assertEquals( - "com.google.api.client.http.javanet.ConfigurableSSLSocketFactory", - sslSocketFactory.getClass().getName()); - } - } - @Test void testBaseEquals() { assertEquals(OPTIONS, OPTIONS_COPY); From 3619e36bbb8caf2abd542849e5ff22d7d4b795c9 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:41:37 +0000 Subject: [PATCH 10/26] refactor(pqc): fallback to fresh NetHttpTransport.Builder on Conscrypt exception and restore HttpTransportOptionsTest to main --- .../InstantiatingHttpJsonChannelProvider.java | 29 ++++++++++------- .../cloud/http/HttpTransportOptions.java | 32 +++++++++---------- .../cloud/http/HttpTransportOptionsTest.java | 12 ++----- 3 files changed, 36 insertions(+), 37 deletions(-) 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 dcdd9dc6cb33..16b5b8fbbe5a 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 @@ -207,37 +207,42 @@ public TransportChannelProvider withCredentials(Credentials credentials) { } HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { - NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); // Attempt to register Conscrypt as the Security Provider for HTTP/JSON connections to enable // Post-Quantum Cryptography (PQC) hybrid key exchange (e.g. X25519MLKEM768) by default. // // Both setSecurityProvider and setSslSocketConfigurator are configured together inside the // try block so that socket named group configuration is only applied when Conscrypt - // initialization - // succeeds. + // initialization succeeds. // // Catching Throwable ensures that if Conscrypt JNI native libraries are not present on the // classpath or fail to load on the target host architecture, transport creation gracefully - // falls back to standard JDK TLS JSSE provider without interrupting application execution. + // falls back to standard JDK TLS JSSE provider using a fresh NetHttpTransport.Builder instance. try { - builder.setSecurityProvider(Conscrypt.newProvider()); - builder.setSslSocketConfigurator( - socket -> { - if (Conscrypt.isConscrypt(socket)) { - Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); - } - }); + NetHttpTransport.Builder builder = + new NetHttpTransport.Builder() + .setSecurityProvider(Conscrypt.newProvider()) + .setSslSocketConfigurator( + socket -> { + if (Conscrypt.isConscrypt(socket)) { + Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); + } + }); + return configureMtls(builder).build(); } catch (Throwable t) { LOG.log(Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); + return configureMtls(new NetHttpTransport.Builder()).build(); } + } + private NetHttpTransport.Builder configureMtls(NetHttpTransport.Builder builder) + throws IOException, GeneralSecurityException { if (mtlsProvider != null && certificateBasedAccess.useMtlsClientCertificate()) { KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); if (mtlsKeyStore != null) { builder.trustCertificates(null, mtlsKeyStore, ""); } } - return builder.build(); + return builder; } private HttpJsonTransportChannel createChannel() throws IOException, GeneralSecurityException { 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 54c7a0acdc25..0dec5b474099 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 @@ -72,34 +72,34 @@ public HttpTransport create() { } } - NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); // Attempt to register Conscrypt as the Security Provider for HTTP client connections to - // enable - // Post-Quantum Cryptography (PQC) hybrid key exchange (e.g. X25519MLKEM768) by default. + // enable Post-Quantum Cryptography (PQC) hybrid key exchange (e.g. X25519MLKEM768) by + // default. // // Both setSecurityProvider and setSslSocketConfigurator are configured together inside the // try block so that socket named group configuration is only applied when Conscrypt - // initialization - // succeeds. + // initialization succeeds. // // Catching Throwable ensures that if Conscrypt JNI native libraries are not present on the // classpath or fail to load on the target host architecture, transport creation gracefully - // falls back to standard JDK TLS JSSE provider without interrupting application execution. + // falls back to standard JDK TLS JSSE provider using a fresh NetHttpTransport.Builder + // instance. try { - builder.setSecurityProvider(Conscrypt.newProvider()); - builder.setSslSocketConfigurator( - socket -> { - if (Conscrypt.isConscrypt(socket)) { - Conscrypt.setNamedGroups( - socket, InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS); - } - }); + return new NetHttpTransport.Builder() + .setSecurityProvider(Conscrypt.newProvider()) + .setSslSocketConfigurator( + socket -> { + if (Conscrypt.isConscrypt(socket)) { + Conscrypt.setNamedGroups( + socket, InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS); + } + }) + .build(); } catch (Throwable t) { LOG.log( Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); + return new NetHttpTransport.Builder().build(); } - - return builder.build(); } } diff --git a/sdk-platform-java/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java b/sdk-platform-java/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java index b523a2e9af60..1697a0c43f41 100644 --- a/sdk-platform-java/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java +++ b/sdk-platform-java/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java @@ -166,9 +166,7 @@ void testHttpRequestInitializer_defaultUniverseDomainSettings_customCredentials( UnauthenticatedException.class, () -> httpRequestInitializer.initialize(defaultHttpRequest)); assertEquals( - "The configured universe domain (googleapis.com) does not match the universe domain found" - + " in the credentials (random.com). If you haven't configured the universe domain" - + " explicitly, `googleapis.com` is the default.", + "The configured universe domain (googleapis.com) does not match the universe domain found in the credentials (random.com). If you haven't configured the universe domain explicitly, `googleapis.com` is the default.", exception.getCause().getMessage()); } @@ -183,9 +181,7 @@ void testHttpRequestInitializer_customUniverseDomainSettings_defaultCredentials( UnauthenticatedException.class, () -> httpRequestInitializer.initialize(defaultHttpRequest)); assertEquals( - "The configured universe domain (random.com) does not match the universe domain found in" - + " the credentials (googleapis.com). If you haven't configured the universe domain" - + " explicitly, `googleapis.com` is the default.", + "The configured universe domain (random.com) does not match the universe domain found in the credentials (googleapis.com). If you haven't configured the universe domain explicitly, `googleapis.com` is the default.", exception.getCause().getMessage()); } @@ -223,9 +219,7 @@ void testHttpRequestInitializer_customUniverseDomainSettings_noCredentials() { UnauthenticatedException.class, () -> httpRequestInitializer.initialize(defaultHttpRequest)); assertEquals( - "The configured universe domain (random.com) does not match the universe domain found in" - + " the credentials (googleapis.com). If you haven't configured the universe domain" - + " explicitly, `googleapis.com` is the default.", + "The configured universe domain (random.com) does not match the universe domain found in the credentials (googleapis.com). If you haven't configured the universe domain explicitly, `googleapis.com` is the default.", exception.getCause().getMessage()); } From 7a56583608741d9bdd5e0223c1bbb9b7d499e5e2 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:45:57 +0000 Subject: [PATCH 11/26] refactor(showcase): streamline ITPqc transport setup and remove ConscryptPqcConfiguratorHelper --- .../com/google/showcase/v1beta1/it/ITPqc.java | 44 ++++--------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java index 1a69eeb147f5..87e2b13b9648 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java @@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertWithMessage; import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.util.SslUtils; import com.google.api.gax.core.NoCredentialsProvider; import com.google.api.gax.httpjson.HttpJsonMetadata; import com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider; @@ -42,6 +41,7 @@ import java.util.List; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; +import org.conscrypt.Conscrypt; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -108,47 +108,21 @@ static void setUp() { @Test void testHttpJsonPqc() throws Exception { - Provider conscryptProvider = null; - try { - conscryptProvider = org.conscrypt.Conscrypt.newProvider(); - } catch (Throwable t) { - // Conscrypt JNI is not available on this platform/runner - } - NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); - if (conscryptProvider != null) { - builder.setSecurityProvider(conscryptProvider); - } - - SSLContext sslContext = SslUtils.getTlsSslContext(conscryptProvider); - TrustManagerFactory tmf = SslUtils.getDefaultTrustManagerFactory(conscryptProvider); - tmf.init(loadCaCert(DEFAULT_CA_CERT_PATH)); - sslContext.init(null, tmf.getTrustManagers(), null); - builder.setSslSocketFactory(sslContext.getSocketFactory()); - - if (conscryptProvider != null) { + try { + builder.setSecurityProvider(Conscrypt.newProvider()); builder.setSslSocketConfigurator( socket -> { - if (org.conscrypt.Conscrypt.isConscrypt(socket)) { - org.conscrypt.Conscrypt.setNamedGroups( + if (Conscrypt.isConscrypt(socket)) { + Conscrypt.setNamedGroups( socket, InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS); } }); - } else { - builder.setSslSocketConfigurator( - socket -> { - try { - javax.net.ssl.SSLParameters params = socket.getSSLParameters(); - java.lang.reflect.Method method = - params.getClass().getMethod("setNamedGroups", String[].class); - method.invoke(params, (Object) new String[] {"X25519"}); - socket.setSSLParameters(params); - } catch (Exception e) { - // Ignore if method not supported on this JDK version - } - }); + } catch (Throwable t) { + // Conscrypt JNI is not available on this platform/runner } + builder.trustCertificates(null, loadCaCert(DEFAULT_CA_CERT_PATH), ""); NetHttpTransport transport = builder.build(); HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor(); @@ -259,7 +233,7 @@ private static KeyStore loadCaCert(String certPath) throws Exception { private static boolean isConscryptFunctional() { try { - org.conscrypt.Conscrypt.newProvider(); + Conscrypt.newProvider(); return true; } catch (Throwable t) { return false; From 3b10d47dacfc1c4d7015a034baafc4eee4e61eda Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:50:01 +0000 Subject: [PATCH 12/26] refactor(showcase): remove isConscryptFunctional fallback and restrict classical non-PQC test case --- .../com/google/showcase/v1beta1/it/ITPqc.java | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java index 87e2b13b9648..95274862e566 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java @@ -40,6 +40,7 @@ import java.util.Collections; import java.util.List; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; import javax.net.ssl.TrustManagerFactory; import org.conscrypt.Conscrypt; import org.junit.jupiter.api.BeforeAll; @@ -149,8 +150,7 @@ void testHttpJsonPqc() throws Exception { assertThat(capturedHeaders).isNotNull(); String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER); - String expectedGroup = isConscryptFunctional() ? "X25519MLKEM768" : "X25519"; - assertThat(negotiatedGroup).isEqualTo(expectedGroup); + assertThat(negotiatedGroup).isEqualTo(EXPECTED_TLS_GROUP); String supportedGroups = getSingleHeaderString(capturedHeaders, TLS_SUPPORTED_GROUPS_HEADER); assertThat(supportedGroups).isNotNull(); @@ -171,9 +171,22 @@ void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception { tmf.init(loadCaCert(DEFAULT_CA_CERT_PATH)); sslContext.init(null, tmf.getTrustManagers(), null); - // Build NetHttpTransport using the SunJSSE socket factory + // Build NetHttpTransport using SunJSSE socket factory and explicitly restrict named groups to + // classical curves (no ML-KEM) NetHttpTransport transport = - new NetHttpTransport.Builder().setSslSocketFactory(sslContext.getSocketFactory()).build(); + new NetHttpTransport.Builder() + .setSslSocketFactory(sslContext.getSocketFactory()) + .setSslSocketConfigurator( + socket -> { + try { + SSLParameters params = socket.getSSLParameters(); + params.setNamedGroups(new String[] {"X25519", "SecP256r1"}); + socket.setSSLParameters(params); + } catch (Exception e) { + // Ignore on JDK versions where setNamedGroups is unsupported + } + }) + .build(); HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor(); @@ -200,9 +213,8 @@ void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception { assertThat(capturedHeaders).isNotNull(); String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER); - // Under SunJSSE (JDK default), PQC curves are unsupported, so it falls back to a classical - // curve (either X25519 or CurveP256 depending on JDK / Go negotiation) - assertThat(negotiatedGroup).isAnyOf("X25519", "CurveP256"); + // Under classical non-PQC configuration, negotiated group is a classical curve + assertThat(negotiatedGroup).isAnyOf("X25519", "SecP256r1", "CurveP256"); assertThat(negotiatedGroup).isNotEqualTo(EXPECTED_TLS_GROUP); } } @@ -230,13 +242,4 @@ private static KeyStore loadCaCert(String certPath) throws Exception { } return trustStore; } - - private static boolean isConscryptFunctional() { - try { - Conscrypt.newProvider(); - return true; - } catch (Throwable t) { - return false; - } - } } From 5f7b1c4e679e77771537ff45a2999a69a9453ab1 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:55:39 +0000 Subject: [PATCH 13/26] refactor(showcase): address PR review comments for ITPqc.java and add helper Javadocs --- .../com/google/showcase/v1beta1/it/ITPqc.java | 67 ++++++++++++------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java index 95274862e566..dd6fcd23453c 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java @@ -80,10 +80,15 @@ public class ITPqc { "x-showcase-tls-client-supported-groups"; // Expected TLS parameters - private static final String EXPECTED_TLS_GROUP = "X25519MLKEM768"; + 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) { @@ -109,22 +114,18 @@ static void setUp() { @Test void testHttpJsonPqc() throws Exception { - NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); - try { - builder.setSecurityProvider(Conscrypt.newProvider()); - builder.setSslSocketConfigurator( - socket -> { - if (Conscrypt.isConscrypt(socket)) { - Conscrypt.setNamedGroups( - socket, InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS); - } - }); - } catch (Throwable t) { - // Conscrypt JNI is not available on this platform/runner - } - - builder.trustCertificates(null, loadCaCert(DEFAULT_CA_CERT_PATH), ""); - NetHttpTransport transport = builder.build(); + NetHttpTransport transport = + new NetHttpTransport.Builder() + .setSecurityProvider(Conscrypt.newProvider()) + .setSslSocketConfigurator( + socket -> { + if (Conscrypt.isConscrypt(socket)) { + Conscrypt.setNamedGroups( + socket, InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS); + } + }) + .trustCertificates(null, loadCaCert(DEFAULT_CA_CERT_PATH), "") + .build(); HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor(); @@ -150,7 +151,7 @@ void testHttpJsonPqc() throws Exception { assertThat(capturedHeaders).isNotNull(); String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER); - assertThat(negotiatedGroup).isEqualTo(EXPECTED_TLS_GROUP); + assertThat(negotiatedGroup).isEqualTo(EXPECTED_PQC_GROUP); String supportedGroups = getSingleHeaderString(capturedHeaders, TLS_SUPPORTED_GROUPS_HEADER); assertThat(supportedGroups).isNotNull(); @@ -158,7 +159,7 @@ void testHttpJsonPqc() throws Exception { } @Test - void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception { + void testHttpJsonPqc_withExplicitSecurityProviderNoPqcGroups() throws Exception { // Explicitly use SunJSSE (JDK default) instead of Conscrypt Provider sunJsseProvider = Security.getProvider("SunJSSE"); assertThat(sunJsseProvider).isNotNull(); @@ -171,8 +172,10 @@ void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception { tmf.init(loadCaCert(DEFAULT_CA_CERT_PATH)); sslContext.init(null, tmf.getTrustManagers(), null); - // Build NetHttpTransport using SunJSSE socket factory and explicitly restrict named groups to - // classical curves (no ML-KEM) + // This test verifies behavior for environments where PQC is not enabled or supported. When + // future JDK versions (e.g. JDK 27+) enable PQC by default in standard JDK JSSE, explicitly + // configuring classical named groups ensures that non-PQC classical TLS connections can still + // be established. NetHttpTransport transport = new NetHttpTransport.Builder() .setSslSocketFactory(sslContext.getSocketFactory()) @@ -183,7 +186,11 @@ void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception { params.setNamedGroups(new String[] {"X25519", "SecP256r1"}); socket.setSSLParameters(params); } catch (Exception e) { - // Ignore on JDK versions where setNamedGroups is unsupported + // For JDK 8-19, SSLParameters.setNamedGroups() does not exist, so JSSE + // naturally + // defaults to classical algorithms, which is expected. Hardcoding classical + // algorithms via setNamedGroups is primarily for JDK 20+ when PQC becomes + // default. } }) .build(); @@ -215,10 +222,17 @@ void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception { 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_TLS_GROUP); + 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) { @@ -232,6 +246,13 @@ private static String getSingleHeaderString(HttpJsonMetadata metadata, String na 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); From d20a1972beaca739ea54253a5225756ec54909c4 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 19:57:04 +0000 Subject: [PATCH 14/26] docs(showcase): clarify rationale for classical named groups configuration in ITPqc --- .../com/google/showcase/v1beta1/it/ITPqc.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java index dd6fcd23453c..410e17026d72 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java @@ -172,10 +172,10 @@ void testHttpJsonPqc_withExplicitSecurityProviderNoPqcGroups() throws Exception tmf.init(loadCaCert(DEFAULT_CA_CERT_PATH)); sslContext.init(null, tmf.getTrustManagers(), null); - // This test verifies behavior for environments where PQC is not enabled or supported. When - // future JDK versions (e.g. JDK 27+) enable PQC by default in standard JDK JSSE, explicitly - // configuring classical named groups ensures that non-PQC classical TLS connections can still - // be established. + // This test verifies client transport behavior when PQC algorithms are not offered. + // Future Java versions (e.g. JDK 27+) will enable PQC (ML-KEM) by default in standard JDK JSSE. + // Explicitly setting named groups to classical algorithms ensures that this test reliably + // tests the non-PQC classical TLS connection path regardless of underlying JDK defaults. NetHttpTransport transport = new NetHttpTransport.Builder() .setSslSocketFactory(sslContext.getSocketFactory()) @@ -186,11 +186,11 @@ void testHttpJsonPqc_withExplicitSecurityProviderNoPqcGroups() throws Exception params.setNamedGroups(new String[] {"X25519", "SecP256r1"}); socket.setSSLParameters(params); } catch (Exception e) { - // For JDK 8-19, SSLParameters.setNamedGroups() does not exist, so JSSE + // For JDK 8-19, SSLParameters.setNamedGroups() is unsupported, and JSSE // naturally - // defaults to classical algorithms, which is expected. Hardcoding classical - // algorithms via setNamedGroups is primarily for JDK 20+ when PQC becomes - // default. + // defaults to classical algorithms. Setting classical named groups is primarily + // for + // JDK 20+ when PQC becomes the default in standard JSSE. } }) .build(); From caa15fe5c25c1d9cedbf8c7f2a66b47cace876e8 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 20:20:37 +0000 Subject: [PATCH 15/26] refactor(http): decouple PQC constants and add defensive lambda error handling --- .../InstantiatingHttpJsonChannelProvider.java | 29 ++++++++++++------- .../cloud/http/HttpTransportOptions.java | 23 ++++++++++----- 2 files changed, 34 insertions(+), 18 deletions(-) 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 16b5b8fbbe5a..f53eb14fd08c 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 @@ -217,21 +217,28 @@ HttpTransport createHttpTransport() throws IOException, GeneralSecurityException // Catching Throwable ensures that if Conscrypt JNI native libraries are not present on the // classpath or fail to load on the target host architecture, transport creation gracefully // falls back to standard JDK TLS JSSE provider using a fresh NetHttpTransport.Builder instance. + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); try { - NetHttpTransport.Builder builder = - new NetHttpTransport.Builder() - .setSecurityProvider(Conscrypt.newProvider()) - .setSslSocketConfigurator( - socket -> { - if (Conscrypt.isConscrypt(socket)) { - Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); - } - }); - return configureMtls(builder).build(); + builder + .setSecurityProvider(Conscrypt.newProvider()) + .setSslSocketConfigurator( + socket -> { + if (Conscrypt.isConscrypt(socket)) { + try { + Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); + } catch (Throwable t) { + // Catch runtime socket configuration errors (e.g. version mismatch or JNI + // error) + // so individual API calls do not fail if Conscrypt named group setup fails. + LOG.log(Level.WARNING, "Failed to set PQC named groups on Conscrypt socket", t); + } + } + }); } catch (Throwable t) { LOG.log(Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); - return configureMtls(new NetHttpTransport.Builder()).build(); + builder = new NetHttpTransport.Builder(); } + return configureMtls(builder).build(); } private NetHttpTransport.Builder configureMtls(NetHttpTransport.Builder builder) 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 0dec5b474099..17ab85f81565 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 @@ -26,7 +26,6 @@ import com.google.api.gax.core.GaxProperties; import com.google.api.gax.httpjson.HttpHeadersUtils; import com.google.api.gax.httpjson.HttpJsonStatusCode; -import com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider; import com.google.api.gax.rpc.ApiClientHeaderProvider; import com.google.api.gax.rpc.EndpointContext; import com.google.api.gax.rpc.HeaderProvider; @@ -59,6 +58,8 @@ public class HttpTransportOptions implements TransportOptions { public static class DefaultHttpTransportFactory implements HttpTransportFactory { private static final Logger LOG = Logger.getLogger(DefaultHttpTransportFactory.class.getName()); + private static final String[] PQC_GROUPS = + new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519"}; private static final HttpTransportFactory INSTANCE = new DefaultHttpTransportFactory(); @Override @@ -84,22 +85,30 @@ public HttpTransport create() { // classpath or fail to load on the target host architecture, transport creation gracefully // falls back to standard JDK TLS JSSE provider using a fresh NetHttpTransport.Builder // instance. + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); try { - return new NetHttpTransport.Builder() + builder .setSecurityProvider(Conscrypt.newProvider()) .setSslSocketConfigurator( socket -> { if (Conscrypt.isConscrypt(socket)) { - Conscrypt.setNamedGroups( - socket, InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS); + try { + Conscrypt.setNamedGroups(socket, PQC_GROUPS); + } catch (Throwable t) { + // Catch runtime socket configuration errors (e.g. version mismatch or JNI + // error) + // so individual API calls do not fail if Conscrypt named group setup fails. + LOG.log( + Level.WARNING, "Failed to set PQC named groups on Conscrypt socket", t); + } } - }) - .build(); + }); } catch (Throwable t) { LOG.log( Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); - return new NetHttpTransport.Builder().build(); + builder = new NetHttpTransport.Builder(); } + return builder.build(); } } From 41b15b0c898e54dac971a2c824306a28ab43cb81 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 20:46:01 +0000 Subject: [PATCH 16/26] fix(http): update log level to FINE and clarify fallback message for socket configurator --- .../httpjson/InstantiatingHttpJsonChannelProvider.java | 8 ++++++-- .../java/com/google/cloud/http/HttpTransportOptions.java | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) 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 f53eb14fd08c..60e2789ae463 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 @@ -229,8 +229,12 @@ HttpTransport createHttpTransport() throws IOException, GeneralSecurityException } catch (Throwable t) { // Catch runtime socket configuration errors (e.g. version mismatch or JNI // error) - // so individual API calls do not fail if Conscrypt named group setup fails. - LOG.log(Level.WARNING, "Failed to set PQC named groups on Conscrypt socket", t); + // and fall back to Conscrypt's default TLS groups without failing the request. + LOG.log( + Level.FINE, + "Failed to set PQC named groups on Conscrypt socket. Falling back to" + + " Conscrypt default TLS groups.", + t); } } }); 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 17ab85f81565..0969ffe45811 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 @@ -97,9 +97,13 @@ public HttpTransport create() { } catch (Throwable t) { // Catch runtime socket configuration errors (e.g. version mismatch or JNI // error) - // so individual API calls do not fail if Conscrypt named group setup fails. + // and fall back to Conscrypt's default TLS groups without failing the + // request. LOG.log( - Level.WARNING, "Failed to set PQC named groups on Conscrypt socket", t); + Level.FINE, + "Failed to set PQC named groups on Conscrypt socket. Falling back to" + + " Conscrypt default TLS groups.", + t); } } }); From f9aedfbd5dd1ee0440f121fc953f84f2218cb968 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 20:56:36 +0000 Subject: [PATCH 17/26] perf(http): lazily initialize and cache Conscrypt security provider instance --- .../InstantiatingHttpJsonChannelProvider.java | 31 +++++++++-------- .../cloud/http/HttpTransportOptions.java | 33 ++++++++++--------- 2 files changed, 36 insertions(+), 28 deletions(-) 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 60e2789ae463..0c5f82239909 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 @@ -45,6 +45,7 @@ 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; @@ -83,6 +84,20 @@ public final class InstantiatingHttpJsonChannelProvider implements TransportChan public static final String[] DEFAULT_PQC_GROUPS = new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519"}; + private static class ConscryptProviderHolder { + private static final Provider INSTANCE = createProvider(); + + private static Provider createProvider() { + try { + return Conscrypt.newProvider(); + } catch (Throwable t) { + LOG.log( + Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); + return null; + } + } + } + @VisibleForTesting static final Logger LOG = Logger.getLogger(InstantiatingHttpJsonChannelProvider.class.getName()); @@ -209,18 +224,11 @@ public TransportChannelProvider withCredentials(Credentials credentials) { HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { // Attempt to register Conscrypt as the Security Provider for HTTP/JSON connections to enable // Post-Quantum Cryptography (PQC) hybrid key exchange (e.g. X25519MLKEM768) by default. - // - // Both setSecurityProvider and setSslSocketConfigurator are configured together inside the - // try block so that socket named group configuration is only applied when Conscrypt - // initialization succeeds. - // - // Catching Throwable ensures that if Conscrypt JNI native libraries are not present on the - // classpath or fail to load on the target host architecture, transport creation gracefully - // falls back to standard JDK TLS JSSE provider using a fresh NetHttpTransport.Builder instance. NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); - try { + Provider conscryptProvider = ConscryptProviderHolder.INSTANCE; + if (conscryptProvider != null) { builder - .setSecurityProvider(Conscrypt.newProvider()) + .setSecurityProvider(conscryptProvider) .setSslSocketConfigurator( socket -> { if (Conscrypt.isConscrypt(socket)) { @@ -238,9 +246,6 @@ HttpTransport createHttpTransport() throws IOException, GeneralSecurityException } } }); - } catch (Throwable t) { - LOG.log(Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); - builder = new NetHttpTransport.Builder(); } return configureMtls(builder).build(); } 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 0969ffe45811..16e8660e9bfb 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 @@ -40,6 +40,7 @@ import com.google.cloud.TransportOptions; import java.io.IOException; import java.io.ObjectInputStream; +import java.security.Provider; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; @@ -62,6 +63,20 @@ public static class DefaultHttpTransportFactory implements HttpTransportFactory new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519"}; private static final HttpTransportFactory INSTANCE = new DefaultHttpTransportFactory(); + private static class ConscryptProviderHolder { + private static final Provider INSTANCE = createProvider(); + + private static Provider createProvider() { + try { + return Conscrypt.newProvider(); + } catch (Throwable t) { + LOG.log( + Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); + return null; + } + } + } + @Override public HttpTransport create() { // Consider App Engine Standard @@ -76,19 +91,11 @@ public HttpTransport create() { // Attempt to register Conscrypt as the Security Provider for HTTP client connections to // enable Post-Quantum Cryptography (PQC) hybrid key exchange (e.g. X25519MLKEM768) by // default. - // - // Both setSecurityProvider and setSslSocketConfigurator are configured together inside the - // try block so that socket named group configuration is only applied when Conscrypt - // initialization succeeds. - // - // Catching Throwable ensures that if Conscrypt JNI native libraries are not present on the - // classpath or fail to load on the target host architecture, transport creation gracefully - // falls back to standard JDK TLS JSSE provider using a fresh NetHttpTransport.Builder - // instance. NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); - try { + Provider conscryptProvider = ConscryptProviderHolder.INSTANCE; + if (conscryptProvider != null) { builder - .setSecurityProvider(Conscrypt.newProvider()) + .setSecurityProvider(conscryptProvider) .setSslSocketConfigurator( socket -> { if (Conscrypt.isConscrypt(socket)) { @@ -107,10 +114,6 @@ public HttpTransport create() { } } }); - } catch (Throwable t) { - LOG.log( - Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); - builder = new NetHttpTransport.Builder(); } return builder.build(); } From d32c73981e6155c51f231a9974aaf79c661a7704 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 21:04:50 +0000 Subject: [PATCH 18/26] refactor(http): use early returns in createHttpTransport and upgrade Conscrypt log level to WARNING --- .../InstantiatingHttpJsonChannelProvider.java | 44 +++++++++-------- .../cloud/http/HttpTransportOptions.java | 48 ++++++++++--------- 2 files changed, 49 insertions(+), 43 deletions(-) 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 0c5f82239909..5a9640ff8501 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 @@ -92,7 +92,7 @@ private static Provider createProvider() { return Conscrypt.newProvider(); } catch (Throwable t) { LOG.log( - Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); + Level.WARNING, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); return null; } } @@ -226,27 +226,29 @@ HttpTransport createHttpTransport() throws IOException, GeneralSecurityException // Post-Quantum Cryptography (PQC) hybrid key exchange (e.g. X25519MLKEM768) by default. NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); Provider conscryptProvider = ConscryptProviderHolder.INSTANCE; - if (conscryptProvider != null) { - builder - .setSecurityProvider(conscryptProvider) - .setSslSocketConfigurator( - socket -> { - if (Conscrypt.isConscrypt(socket)) { - try { - Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); - } catch (Throwable t) { - // Catch runtime socket configuration errors (e.g. version mismatch or JNI - // error) - // and fall back to Conscrypt's default TLS groups without failing the request. - LOG.log( - Level.FINE, - "Failed to set PQC named groups on Conscrypt socket. Falling back to" - + " Conscrypt default TLS groups.", - t); - } - } - }); + if (conscryptProvider == null) { + return configureMtls(builder).build(); } + builder + .setSecurityProvider(conscryptProvider) + .setSslSocketConfigurator( + socket -> { + if (!Conscrypt.isConscrypt(socket)) { + return; + } + try { + Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); + } catch (Throwable t) { + // Catch runtime socket configuration errors (e.g. version mismatch or JNI + // error) + // and fall back to Conscrypt's default TLS groups without failing the request. + LOG.log( + Level.WARNING, + "Failed to set PQC named groups on Conscrypt socket. Falling back to" + + " Conscrypt default TLS groups.", + t); + } + }); return configureMtls(builder).build(); } 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 16e8660e9bfb..4c82b94a61a4 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 @@ -71,7 +71,9 @@ private static Provider createProvider() { return Conscrypt.newProvider(); } catch (Throwable t) { LOG.log( - Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); + Level.WARNING, + "Conscrypt native libraries not available. Falling back to JDK TLS.", + t); return null; } } @@ -93,28 +95,30 @@ public HttpTransport create() { // default. NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); Provider conscryptProvider = ConscryptProviderHolder.INSTANCE; - if (conscryptProvider != null) { - builder - .setSecurityProvider(conscryptProvider) - .setSslSocketConfigurator( - socket -> { - if (Conscrypt.isConscrypt(socket)) { - try { - Conscrypt.setNamedGroups(socket, PQC_GROUPS); - } catch (Throwable t) { - // Catch runtime socket configuration errors (e.g. version mismatch or JNI - // error) - // and fall back to Conscrypt's default TLS groups without failing the - // request. - LOG.log( - Level.FINE, - "Failed to set PQC named groups on Conscrypt socket. Falling back to" - + " Conscrypt default TLS groups.", - t); - } - } - }); + if (conscryptProvider == null) { + return builder.build(); } + builder + .setSecurityProvider(conscryptProvider) + .setSslSocketConfigurator( + socket -> { + if (!Conscrypt.isConscrypt(socket)) { + return; + } + try { + Conscrypt.setNamedGroups(socket, PQC_GROUPS); + } catch (Throwable t) { + // Catch runtime socket configuration errors (e.g. version mismatch or JNI + // error) + // and fall back to Conscrypt's default TLS groups without failing the + // request. + LOG.log( + Level.WARNING, + "Failed to set PQC named groups on Conscrypt socket. Falling back to" + + " Conscrypt default TLS groups.", + t); + } + }); return builder.build(); } } From 336452398f915e241d5dd01bb07e676d17f21b76 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 21:10:09 +0000 Subject: [PATCH 19/26] docs(http): document architectural tradeoff decision for graceful Conscrypt PQC fallback --- .../InstantiatingHttpJsonChannelProvider.java | 11 +++++++++-- .../com/google/cloud/http/HttpTransportOptions.java | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) 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 5a9640ff8501..066d43e8268a 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 @@ -224,6 +224,14 @@ public TransportChannelProvider withCredentials(Credentials credentials) { HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { // Attempt to register Conscrypt as the Security Provider for HTTP/JSON connections to enable // Post-Quantum Cryptography (PQC) hybrid key exchange (e.g. X25519MLKEM768) by default. + // + // Tradeoff Decision: We intentionally catch errors and gracefully fall back to JDK TLS or + // Conscrypt defaults rather than failing fast. Because Conscrypt depends on native JNI + // libraries that may not be available or compatible across all user environments (e.g. + // non-x86 architectures, musl libc, custom runtimes), failing fast would cause breaking + // outages for existing customers upgrading the SDK. We accept the tradeoff of a potential + // silent fallback to classical TLS (logged at WARNING level) in exchange for maintaining high + // availability and backward compatibility across all client environments. NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); Provider conscryptProvider = ConscryptProviderHolder.INSTANCE; if (conscryptProvider == null) { @@ -239,8 +247,7 @@ HttpTransport createHttpTransport() throws IOException, GeneralSecurityException try { Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); } catch (Throwable t) { - // Catch runtime socket configuration errors (e.g. version mismatch or JNI - // error) + // Catch runtime socket configuration errors (e.g. version mismatch or JNI error) // and fall back to Conscrypt's default TLS groups without failing the request. LOG.log( Level.WARNING, 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 4c82b94a61a4..6fe29699ddf6 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 @@ -93,6 +93,14 @@ public HttpTransport create() { // Attempt to register Conscrypt as the Security Provider for HTTP client connections to // enable Post-Quantum Cryptography (PQC) hybrid key exchange (e.g. X25519MLKEM768) by // default. + // + // Tradeoff Decision: We intentionally catch errors and gracefully fall back to JDK TLS or + // Conscrypt defaults rather than failing fast. Because Conscrypt depends on native JNI + // libraries that may not be available or compatible across all user environments (e.g. + // non-x86 architectures, musl libc, custom runtimes), failing fast would cause breaking + // outages for existing customers upgrading the SDK. We accept the tradeoff of a potential + // silent fallback to classical TLS (logged at WARNING level) in exchange for maintaining high + // availability and backward compatibility across all client environments. NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); Provider conscryptProvider = ConscryptProviderHolder.INSTANCE; if (conscryptProvider == null) { From 8b14718685cc2f5a09319ad87495c27f230c4f89 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 21:18:10 +0000 Subject: [PATCH 20/26] refactor(http): centralize Conscrypt PQC transport logic in HttpJsonTransportUtils --- .../gax/httpjson/HttpJsonTransportUtils.java | 136 ++++++++++++++++++ .../InstantiatingHttpJsonChannelProvider.java | 54 +------ .../cloud/http/HttpTransportOptions.java | 62 +------- 3 files changed, 140 insertions(+), 112 deletions(-) create mode 100644 sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java new file mode 100644 index 000000000000..5e14695054a4 --- /dev/null +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java @@ -0,0 +1,136 @@ +/* + * 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 with Post-Quantum + * Cryptography (PQC) Conscrypt support. + */ +@InternalApi +public class HttpJsonTransportUtils { + + private static final Logger LOG = Logger.getLogger(HttpJsonTransportUtils.class.getName()); + + /** + * Default TLS 1.3 Post-Quantum Cryptography (PQC) named groups configured when Conscrypt security + * provider is present: + * + *
        + *
      • {@code X25519MLKEM768}: Primary preferred hybrid key exchange algorithm combining + * Curve25519 ECDHE with NIST FIPS 203 (ML-KEM-768 / Kyber768), selected as the primary + * group per Google Cloud PQC guidelines. + *
      • {@code SecP256r1MLKEM768}: Secondary preferred hybrid key exchange algorithm combining + * NIST P-256 (SecP256r1) with NIST FIPS 203 (ML-KEM-768) for environments requiring + * FIPS-compliant elliptic curves. + *
      • {@code X25519}: Classical non-quantum key exchange fallback to ensure compatibility with + * standard TLS 1.3 endpoints if ML-KEM is not negotiated. + *
      + */ + public static final String[] DEFAULT_PQC_GROUPS = + new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519"}; + + /** + * Lazy Initialization-on-Demand Holder Idiom (Singleton pattern). + * + *

      This static nested class defers Conscrypt {@link Provider} creation until first access and + * caches the resulting instance (or {@code null} if Conscrypt native JNI libraries fail to load). + * + *

      Design rationale: + * + *

        + *
      • Thread Safety: Leverages JVM class-loading guarantees to initialize the singleton + * thread-safely without requiring synchronized blocks or locks. + *
      • Performance: Avoids re-instantiating {@code Conscrypt.newProvider()} JNI + * allocations on every transport creation. + *
      • Single Log Output: If Conscrypt JNI is not available on the target architecture, + * the failure is logged at WARNING level exactly once during class initialization and + * caches {@code null}, preventing repeated JNI error logging on subsequent calls. + *
      + */ + 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; + } + } + } + + /** + * Creates a {@link NetHttpTransport.Builder} pre-configured with Conscrypt security provider and + * PQC named groups if Conscrypt is available. + * + *

      Tradeoff Decision: We intentionally catch errors and gracefully fall back to JDK TLS or + * Conscrypt defaults rather than failing fast. Because Conscrypt depends on native JNI libraries + * that may not be available or compatible across all user environments (e.g. non-x86 + * architectures, musl libc, custom runtimes), failing fast would cause breaking outages for + * existing customers upgrading the SDK. We accept the tradeoff of a potential silent fallback to + * classical TLS (logged at WARNING level) in exchange for maintaining high availability and + * backward compatibility across all client environments. + */ + public static NetHttpTransport.Builder createPqcHttpTransportBuilder() { + NetHttpTransport.Builder builder = new NetHttpTransport.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 (Throwable t) { + LOG.log( + Level.WARNING, + "Failed to set PQC named groups on Conscrypt socket. Falling back to" + + " Conscrypt default TLS groups.", + t); + } + }); + } + + private HttpJsonTransportUtils() {} +} 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 066d43e8268a..33ac2d274230 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 @@ -45,14 +45,12 @@ 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.annotation.Nullable; -import org.conscrypt.Conscrypt; /** * InstantiatingHttpJsonChannelProvider is a TransportChannelProvider which constructs a {@link @@ -81,22 +79,7 @@ public final class InstantiatingHttpJsonChannelProvider implements TransportChan *

    7. {@code X25519}: Classical non-quantum key exchange fallback. * */ - public static final String[] DEFAULT_PQC_GROUPS = - new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519"}; - - 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; - } - } - } + public static final String[] DEFAULT_PQC_GROUPS = HttpJsonTransportUtils.DEFAULT_PQC_GROUPS; @VisibleForTesting static final Logger LOG = Logger.getLogger(InstantiatingHttpJsonChannelProvider.class.getName()); @@ -222,40 +205,7 @@ public TransportChannelProvider withCredentials(Credentials credentials) { } HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { - // Attempt to register Conscrypt as the Security Provider for HTTP/JSON connections to enable - // Post-Quantum Cryptography (PQC) hybrid key exchange (e.g. X25519MLKEM768) by default. - // - // Tradeoff Decision: We intentionally catch errors and gracefully fall back to JDK TLS or - // Conscrypt defaults rather than failing fast. Because Conscrypt depends on native JNI - // libraries that may not be available or compatible across all user environments (e.g. - // non-x86 architectures, musl libc, custom runtimes), failing fast would cause breaking - // outages for existing customers upgrading the SDK. We accept the tradeoff of a potential - // silent fallback to classical TLS (logged at WARNING level) in exchange for maintaining high - // availability and backward compatibility across all client environments. - NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); - Provider conscryptProvider = ConscryptProviderHolder.INSTANCE; - if (conscryptProvider == null) { - return configureMtls(builder).build(); - } - builder - .setSecurityProvider(conscryptProvider) - .setSslSocketConfigurator( - socket -> { - if (!Conscrypt.isConscrypt(socket)) { - return; - } - try { - Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); - } catch (Throwable t) { - // Catch runtime socket configuration errors (e.g. version mismatch or JNI error) - // and fall back to Conscrypt's default TLS groups without failing the request. - LOG.log( - Level.WARNING, - "Failed to set PQC named groups on Conscrypt socket. Falling back to" - + " Conscrypt default TLS groups.", - t); - } - }); + NetHttpTransport.Builder builder = HttpJsonTransportUtils.createPqcHttpTransportBuilder(); return configureMtls(builder).build(); } 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 6fe29699ddf6..3edc865af95d 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 @@ -22,10 +22,10 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpTransport; -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.HttpJsonStatusCode; +import com.google.api.gax.httpjson.HttpJsonTransportUtils; import com.google.api.gax.rpc.ApiClientHeaderProvider; import com.google.api.gax.rpc.EndpointContext; import com.google.api.gax.rpc.HeaderProvider; @@ -40,11 +40,8 @@ import com.google.cloud.TransportOptions; import java.io.IOException; import java.io.ObjectInputStream; -import java.security.Provider; import java.util.Objects; -import java.util.logging.Level; import java.util.logging.Logger; -import org.conscrypt.Conscrypt; /** Class representing service options for those services that use HTTP as the transport layer. */ public class HttpTransportOptions implements TransportOptions { @@ -59,26 +56,8 @@ public class HttpTransportOptions implements TransportOptions { public static class DefaultHttpTransportFactory implements HttpTransportFactory { private static final Logger LOG = Logger.getLogger(DefaultHttpTransportFactory.class.getName()); - private static final String[] PQC_GROUPS = - new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519"}; private static final HttpTransportFactory INSTANCE = new DefaultHttpTransportFactory(); - 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; - } - } - } - @Override public HttpTransport create() { // Consider App Engine Standard @@ -90,44 +69,7 @@ public HttpTransport create() { } } - // Attempt to register Conscrypt as the Security Provider for HTTP client connections to - // enable Post-Quantum Cryptography (PQC) hybrid key exchange (e.g. X25519MLKEM768) by - // default. - // - // Tradeoff Decision: We intentionally catch errors and gracefully fall back to JDK TLS or - // Conscrypt defaults rather than failing fast. Because Conscrypt depends on native JNI - // libraries that may not be available or compatible across all user environments (e.g. - // non-x86 architectures, musl libc, custom runtimes), failing fast would cause breaking - // outages for existing customers upgrading the SDK. We accept the tradeoff of a potential - // silent fallback to classical TLS (logged at WARNING level) in exchange for maintaining high - // availability and backward compatibility across all client environments. - NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); - Provider conscryptProvider = ConscryptProviderHolder.INSTANCE; - if (conscryptProvider == null) { - return builder.build(); - } - builder - .setSecurityProvider(conscryptProvider) - .setSslSocketConfigurator( - socket -> { - if (!Conscrypt.isConscrypt(socket)) { - return; - } - try { - Conscrypt.setNamedGroups(socket, PQC_GROUPS); - } catch (Throwable t) { - // Catch runtime socket configuration errors (e.g. version mismatch or JNI - // error) - // and fall back to Conscrypt's default TLS groups without failing the - // request. - LOG.log( - Level.WARNING, - "Failed to set PQC named groups on Conscrypt socket. Falling back to" - + " Conscrypt default TLS groups.", - t); - } - }); - return builder.build(); + return HttpJsonTransportUtils.createPqcHttpTransportBuilder().build(); } } From 7cf7b845c0c901f68bdc61d3db584ee5c54f37b7 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 21:21:57 +0000 Subject: [PATCH 21/26] docs(http): update Conscrypt PQC Javadoc comments and method naming --- .../gax/httpjson/HttpJsonTransportUtils.java | 22 +++++++------------ .../InstantiatingHttpJsonChannelProvider.java | 2 +- .../cloud/http/HttpTransportOptions.java | 2 +- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java index 5e14695054a4..e03c710efc78 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java @@ -51,8 +51,7 @@ public class HttpJsonTransportUtils { * *
        *
      • {@code X25519MLKEM768}: Primary preferred hybrid key exchange algorithm combining - * Curve25519 ECDHE with NIST FIPS 203 (ML-KEM-768 / Kyber768), selected as the primary - * group per Google Cloud PQC guidelines. + * Curve25519 ECDHE with NIST FIPS 203 (ML-KEM-768 / Kyber768). *
      • {@code SecP256r1MLKEM768}: Secondary preferred hybrid key exchange algorithm combining * NIST P-256 (SecP256r1) with NIST FIPS 203 (ML-KEM-768) for environments requiring * FIPS-compliant elliptic curves. @@ -69,8 +68,6 @@ public class HttpJsonTransportUtils { *

        This static nested class defers Conscrypt {@link Provider} creation until first access and * caches the resulting instance (or {@code null} if Conscrypt native JNI libraries fail to load). * - *

        Design rationale: - * *

          *
        • Thread Safety: Leverages JVM class-loading guarantees to initialize the singleton * thread-safely without requiring synchronized blocks or locks. @@ -96,18 +93,15 @@ private static Provider createProvider() { } /** - * Creates a {@link NetHttpTransport.Builder} pre-configured with Conscrypt security provider and - * PQC named groups if Conscrypt is available. + * Creates a {@link NetHttpTransport.Builder} pre-configured with Conscrypt as the security + * provider by default. * - *

          Tradeoff Decision: We intentionally catch errors and gracefully fall back to JDK TLS or - * Conscrypt defaults rather than failing fast. Because Conscrypt depends on native JNI libraries - * that may not be available or compatible across all user environments (e.g. non-x86 - * architectures, musl libc, custom runtimes), failing fast would cause breaking outages for - * existing customers upgrading the SDK. We accept the tradeoff of a potential silent fallback to - * classical TLS (logged at WARNING level) in exchange for maintaining high availability and - * backward compatibility across all client environments. + *

          Note: Conscrypt native JNI libraries may not be available or compatible across all + * environments. If Conscrypt is not available, transport creation gracefully falls back to the + * default JDK TLS provider. Users can customize the {@link NetHttpTransport.Builder} or security + * provider if needed. */ - public static NetHttpTransport.Builder createPqcHttpTransportBuilder() { + public static NetHttpTransport.Builder createConscryptHttpTransportBuilder() { NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); Provider conscryptProvider = ConscryptProviderHolder.INSTANCE; if (conscryptProvider == null) { 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 33ac2d274230..7b3fff50f6ff 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 @@ -205,7 +205,7 @@ public TransportChannelProvider withCredentials(Credentials credentials) { } HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { - NetHttpTransport.Builder builder = HttpJsonTransportUtils.createPqcHttpTransportBuilder(); + NetHttpTransport.Builder builder = HttpJsonTransportUtils.createConscryptHttpTransportBuilder(); return configureMtls(builder).build(); } 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 3edc865af95d..31222d114494 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 @@ -69,7 +69,7 @@ public HttpTransport create() { } } - return HttpJsonTransportUtils.createPqcHttpTransportBuilder().build(); + return HttpJsonTransportUtils.createConscryptHttpTransportBuilder().build(); } } From 4dd41085c4053e1ef3a5f9c262fff72c95a6bbb8 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 21:33:06 +0000 Subject: [PATCH 22/26] refactor(http): remove duplicate DEFAULT_PQC_GROUPS and simplify Conscrypt Javadoc --- .../gax/httpjson/HttpJsonTransportUtils.java | 20 +++++-------------- .../InstantiatingHttpJsonChannelProvider.java | 14 ------------- ...tantiatingHttpJsonChannelProviderTest.java | 2 +- 3 files changed, 6 insertions(+), 30 deletions(-) diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java index e03c710efc78..756286f90011 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java @@ -63,20 +63,10 @@ public class HttpJsonTransportUtils { new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519"}; /** - * Lazy Initialization-on-Demand Holder Idiom (Singleton pattern). + * Lazy initialization holder for Conscrypt {@link Provider}. * - *

          This static nested class defers Conscrypt {@link Provider} creation until first access and - * caches the resulting instance (or {@code null} if Conscrypt native JNI libraries fail to load). - * - *

            - *
          • Thread Safety: Leverages JVM class-loading guarantees to initialize the singleton - * thread-safely without requiring synchronized blocks or locks. - *
          • Performance: Avoids re-instantiating {@code Conscrypt.newProvider()} JNI - * allocations on every transport creation. - *
          • Single Log Output: If Conscrypt JNI is not available on the target architecture, - * the failure is logged at WARNING level exactly once during class initialization and - * caches {@code null}, preventing repeated JNI error logging on subsequent calls. - *
          + *

          Caches the provider instance (or {@code null} if native libraries fail to load) to avoid + * repeated JNI allocations and error logging. */ private static class ConscryptProviderHolder { private static final Provider INSTANCE = createProvider(); @@ -98,8 +88,8 @@ private static Provider createProvider() { * *

          Note: Conscrypt native JNI libraries may not be available or compatible across all * environments. If Conscrypt is not available, transport creation gracefully falls back to the - * default JDK TLS provider. Users can customize the {@link NetHttpTransport.Builder} or security - * provider if needed. + * default JDK TLS provider. Users can customize the {@link NetHttpTransport.Builder} to use a + * different security provider. */ public static NetHttpTransport.Builder createConscryptHttpTransportBuilder() { NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); 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 7b3fff50f6ff..b31904d64a6a 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 @@ -67,20 +67,6 @@ @InternalExtensionOnly public final class InstantiatingHttpJsonChannelProvider implements TransportChannelProvider { - /** - * Default TLS 1.3 Post-Quantum Cryptography (PQC) named groups used when Conscrypt security - * provider is present. - * - *

            - *
          • {@code X25519MLKEM768}: Primary preferred group. Combines Curve25519 ECDHE with NIST FIPS - * 203 (ML-KEM-768) standard. - *
          • {@code SecP256r1MLKEM768}: Secondary preferred group. Combines NIST P-256 (SecP256r1) - * with NIST FIPS 203 (ML-KEM-768) for FIPS compliance. - *
          • {@code X25519}: Classical non-quantum key exchange fallback. - *
          - */ - public static final String[] DEFAULT_PQC_GROUPS = HttpJsonTransportUtils.DEFAULT_PQC_GROUPS; - @VisibleForTesting static final Logger LOG = Logger.getLogger(InstantiatingHttpJsonChannelProvider.class.getName()); 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 e6fcbc7b2bf5..255628759ad7 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 @@ -211,7 +211,7 @@ void testCreateHttpTransport_returnsValidTransport() throws Exception { @Test void testDefaultPqcGroups_containsExpectedGroups() { - assertThat(InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS) + assertThat(HttpJsonTransportUtils.DEFAULT_PQC_GROUPS) .asList() .containsExactly("X25519MLKEM768", "SecP256r1MLKEM768", "X25519") .inOrder(); From f35ea4183b4d3fec4a607af461b9f5b442b475e0 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 21:37:27 +0000 Subject: [PATCH 23/26] docs(http): move environment fallback explanation to ConscryptProviderHolder and comment catch block --- .../gax/httpjson/HttpJsonTransportUtils.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java index 756286f90011..11ecd807be0f 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java @@ -65,8 +65,11 @@ public class HttpJsonTransportUtils { /** * Lazy initialization holder for Conscrypt {@link Provider}. * - *

          Caches the provider instance (or {@code null} if native libraries fail to load) to avoid - * repeated JNI allocations and error logging. + *

          Note: Conscrypt native JNI libraries may not be available or compatible across all + * environment runtimes (e.g. non-x86 architectures, musl libc, or constrained environments). + * Caching {@code null} on failure allows us to attempt using Conscrypt as the default security + * provider without causing breaking compatibility issues for customers on special environments + * when upgrading our SDK. */ private static class ConscryptProviderHolder { private static final Provider INSTANCE = createProvider(); @@ -84,12 +87,8 @@ private static Provider createProvider() { /** * Creates a {@link NetHttpTransport.Builder} pre-configured with Conscrypt as the security - * provider by default. - * - *

          Note: Conscrypt native JNI libraries may not be available or compatible across all - * environments. If Conscrypt is not available, transport creation gracefully falls back to the - * default JDK TLS provider. Users can customize the {@link NetHttpTransport.Builder} to use a - * different security provider. + * provider by default if Conscrypt is available. Users can customize the {@link + * NetHttpTransport.Builder} to use a different security provider. */ public static NetHttpTransport.Builder createConscryptHttpTransportBuilder() { NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); @@ -107,6 +106,9 @@ public static NetHttpTransport.Builder createConscryptHttpTransportBuilder() { try { Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); } catch (Throwable t) { + // Catch runtime socket configuration errors (e.g. version mismatch or unexpected + // socket implementation) to gracefully fall back to Conscrypt's default TLS groups + // without failing transport creation. LOG.log( Level.WARNING, "Failed to set PQC named groups on Conscrypt socket. Falling back to" From 056d3eb411128c6fa19bb7170f4556cec7729748 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 21:41:35 +0000 Subject: [PATCH 24/26] docs(http): clarify caching rationale vs JDK TLS fallback in ConscryptProviderHolder Javadoc --- .../api/gax/httpjson/HttpJsonTransportUtils.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java index 11ecd807be0f..ed46d9ad581c 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonTransportUtils.java @@ -65,11 +65,12 @@ public class HttpJsonTransportUtils { /** * Lazy initialization holder for Conscrypt {@link Provider}. * - *

          Note: Conscrypt native JNI libraries may not be available or compatible across all - * environment runtimes (e.g. non-x86 architectures, musl libc, or constrained environments). - * Caching {@code null} on failure allows us to attempt using Conscrypt as the default security - * provider without causing breaking compatibility issues for customers on special environments - * when upgrading our SDK. + *

          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(); From 1d082b6ddaeef55d3a151086c7e215989d66af8a Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 21:45:04 +0000 Subject: [PATCH 25/26] refactor(showcase): rename ITPqc integration test to ITPostQuantumCryptography --- .../v1beta1/it/{ITPqc.java => ITPostQuantumCryptography.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/{ITPqc.java => ITPostQuantumCryptography.java} (99%) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPostQuantumCryptography.java similarity index 99% rename from java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java rename to java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPostQuantumCryptography.java index 410e17026d72..5a4526289954 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPostQuantumCryptography.java @@ -72,7 +72,7 @@ * back gracefully to classical key exchange ({@code X25519}) instead of crashing. *

    */ -public class ITPqc { +public class ITPostQuantumCryptography { // TLS response header names from Showcase server private static final String TLS_GROUP_HEADER = "x-showcase-tls-group"; From 1a0882974c75f4c0447617c302c27e3acdb21c0f Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 21 Jul 2026 21:48:44 +0000 Subject: [PATCH 26/26] ci(showcase): replace hardcoded sleep 2 with deterministic Bash /dev/tcp readiness polling loop --- .github/workflows/showcase.yaml | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/showcase.yaml b/.github/workflows/showcase.yaml index d3ac46522c72..6c1f135d0703 100644 --- a/.github/workflows/showcase.yaml +++ b/.github/workflows/showcase.yaml @@ -65,7 +65,18 @@ jobs: ./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 & - sleep 2 + # 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 @@ -174,7 +185,18 @@ jobs: ./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 & - sleep 2 + # 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