org.junit.jupiter
junit-jupiter-engine
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..f83a33bc5ebc
--- /dev/null
+++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java
@@ -0,0 +1,426 @@
+/*
+ * 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:
+ *
+ *
+ * - {@code x-showcase-tls-group}: The negotiated key exchange named group (e.g.
+ * X25519MLKEM768).
+ *
- {@code x-showcase-tls-version}: The TLS version negotiated (e.g. TLS 1.3).
+ *
- {@code x-showcase-tls-cipher}: The negotiated cipher suite (e.g. TLS_AES_128_GCM_SHA256).
+ *
- {@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:
+ *
+ *
+ * - {@code testGrpcPqc}: Verifies that gRPC (Netty-shaded) uses Conscrypt and successfully
+ * negotiates the hybrid post-quantum group {@code X25519MLKEM768}.
+ *
- {@code testHttpJsonPqc}: Verifies that HTTP/JSON transport defaults to Conscrypt and
+ * negotiates the hybrid post-quantum group {@code X25519MLKEM768}.
+ *
- {@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(
+ new com.google.api.client.http.javanet.JreNamedGroupsSslSocketConfigurator(
+ new String[] {"X25519"}));
+ }
+
+ 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/java-showcase/pom.xml b/java-showcase/pom.xml
index c90fb72c78fb..532ca3a02e8e 100644
--- a/java-showcase/pom.xml
+++ b/java-showcase/pom.xml
@@ -29,8 +29,28 @@
true
+
+
+ sonatype-snapshots
+ https://central.sonatype.com/repository/maven-snapshots/
+
+ true
+
+
+ false
+
+
+
+
+
+ io.grpc
+ grpc-bom
+ 1.83.0-SNAPSHOT
+ pom
+ import
+
com.google.cloud
google-cloud-shared-dependencies
diff --git a/pom.xml b/pom.xml
index 6471f9af7af0..e97a138d0aef 100644
--- a/pom.xml
+++ b/pom.xml
@@ -344,4 +344,40 @@
+
+
+
+ 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
+
+
+
+ sonatype-s01-snapshots
+ Sonatype S01 Snapshots
+ https://s01.oss.sonatype.org/content/repositories/snapshots
+
+ false
+
+
+ true
+
+
+
\ No newline at end of file
diff --git a/pqc-verification/README.md b/pqc-verification/README.md
new file mode 100644
index 000000000000..704777a65304
--- /dev/null
+++ b/pqc-verification/README.md
@@ -0,0 +1,100 @@
+# 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
+* To perform PQC handshakes, JDK 11+ is required for compiling Conscrypt. JDK 17+ or JDK 21+ is highly recommended.
+* Conscrypt acts as the security provider providing hybrid group `X25519MLKEM768`.
+
+### Core Snapshot Artifacts
+The PQC verification depends on local SNAPSHOT builds of libraries containing our PQC enhancements:
+1. **`google-http-java-client`** (`pqc-support-conscrypt` branch): Enforces and wraps standard HTTP connections to prefer Conscrypt PQC sockets.
+2. **`gRPC-Java`** (`1.83.0-SNAPSHOT`): Enables Netty 4.2 support which negotiates hybrid key exchange by default.
+
+---
+
+## 2. Setting Up Showcase (Local TLS Server)
+
+The `ITPqc` test suite runs integration tests against the local secure **GAPIC Showcase** server.
+
+### Step 2.1: Download & Build Showcase with TLS Support
+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 (Recommended)
+Start the Showcase server in Auto-TLS mode. This automatically generates a CA certificate in-memory at startup and saves it to the target directory:
+
+```shell
+# Run on secure port 7470
+./gapic-showcase run \
+ --port 7470 \
+ --tls \
+ --ca-cert-output-file /tmp/showcase-ca.pem
+```
+
+*Note: The helper script `build-with-local-http-client.sh` will automatically launch and clean up this Showcase server if it finds the `gapic-showcase` repository cloned next to the `google-cloud-java` monorepo.*
+If running manually, execute the tests using:
+`mvn test -pl java-showcase/gapic-showcase -Dtest=ITPqc -Dshowcase.ca.cert.path=/tmp/showcase-ca.pem`
+
+---
+
+## 3. Running Local Verification Tests
+
+Use the helper script `build-with-local-http-client.sh` to automatically build/install `google-http-java-client` as a local snapshot, compile the monorepo, and execute Showcase PQC integration tests:
+
+```shell
+# Set path to the google-http-java-client repository
+export HTTP_CLIENT_DIR=~/IdeaProjects/google-http-java-client
+
+# Run the verification script
+./build-with-local-http-client.sh
+```
+
+If successful, you will see `BUILD SUCCESS` and both `testGrpcPqc` and `testHttpJsonPqc` passing.
+
+---
+
+## 4. Standalone BigQuery PQC Verification Sample
+
+The class `BqPqcTest` runs a live connection to Google Cloud BigQuery using the default client settings. Since the PQC changes are enabled by default in the underlying HTTP client transport, this sample does not require any custom PQC configurations.
+
+### Run the Sample
+You can run the sample directly from your IDE, or via Maven. The project ID is resolved automatically via Application Default Credentials (ADC), and TLS/SSL handshake tracing is configured programmatically:
+
+```shell
+cd pqc-verification
+
+# Run using exec-maven-plugin
+mvn clean compile exec:java
+```
+
+### Expected Output
+The program will automatically intercept the TLS handshake and assert on the negotiated curve. If successful, you will see a validation summary at the end of execution:
+
+```
+[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)
+Initializing default BigQuery client for project: your-gcp-project-id
+Listing datasets using default BigQuery Client...
+- my_dataset1
+- my_dataset2
+Success! BigQuery datasets retrieved successfully.
+
+==================================================
+TLS Handshake Verification Results:
+ Protocol : TLSv1.3
+ Cipher Suite : TLS_AES_128_GCM_SHA256
+ Negotiated KEX: X25519MLKEM768
+==================================================
+VERIFICATION SUCCESS: PQC Hybrid key exchange negotiated successfully!
+```
diff --git a/pqc-verification/pom.xml b/pqc-verification/pom.xml
new file mode 100644
index 000000000000..8b56f265be0d
--- /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.1.2-SNAPSHOT
+ 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..bf618dbb8373
--- /dev/null
+++ b/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java
@@ -0,0 +1,279 @@
+/*
+ * 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.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
+ HttpTransport tracingTransport =
+ new NetHttpTransport.Builder().setSslSocketFactory(tracingFactory).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/gapic-generator-java-pom-parent/pom.xml b/sdk-platform-java/gapic-generator-java-pom-parent/pom.xml
index 76fff743425f..87c03f7310e6 100644
--- a/sdk-platform-java/gapic-generator-java-pom-parent/pom.xml
+++ b/sdk-platform-java/gapic-generator-java-pom-parent/pom.xml
@@ -28,7 +28,8 @@
consistent across modules in this repository -->
1.3.2
1.81.0
- 2.1.1
+ 2.1.2-SNAPSHOT
+ 2.6.0
2.13.2
33.5.0-jre
4.33.2
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/ConscryptPqcConfiguratorHelper.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ConscryptPqcConfiguratorHelper.java
new file mode 100644
index 000000000000..135b8d4a6328
--- /dev/null
+++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ConscryptPqcConfiguratorHelper.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.google.api.gax.httpjson;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.http.javanet.SslSocketConfigurator;
+import com.google.api.core.InternalApi;
+import javax.net.ssl.SSLSocket;
+import org.conscrypt.Conscrypt;
+
+@InternalApi
+public final class ConscryptPqcConfiguratorHelper {
+ private static final String[] DEFAULT_PQC_GROUPS =
+ new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519Kyber768Draft00", "X25519"};
+
+ public static void configure(NetHttpTransport.Builder builder) {
+ builder.setSslSocketConfigurator(
+ new SslSocketConfigurator() {
+ @Override
+ public void configure(SSLSocket socket) {
+ if (Conscrypt.isConscrypt(socket)) {
+ Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS);
+ }
+ }
+ });
+ }
+}
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..4fdb9a5d5781 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
@@ -191,16 +192,20 @@ public TransportChannelProvider withCredentials(Credentials credentials) {
}
HttpTransport createHttpTransport() throws IOException, GeneralSecurityException {
- if (mtlsProvider == null) {
- return null;
+ NetHttpTransport.Builder builder = new NetHttpTransport.Builder();
+ try {
+ builder.setSecurityProvider(Conscrypt.newProvider());
+ ConscryptPqcConfiguratorHelper.configure(builder);
+ } catch (Throwable t) {
+ LOG.log(Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t);
}
- if (certificateBasedAccess.useMtlsClientCertificate()) {
+ if (mtlsProvider != null && certificateBasedAccess.useMtlsClientCertificate()) {
KeyStore mtlsKeyStore = mtlsProvider.getKeyStore();
if (mtlsKeyStore != null) {
- return new NetHttpTransport.Builder().trustCertificates(null, mtlsKeyStore, "").build();
+ builder.trustCertificates(null, mtlsKeyStore, "");
}
}
- return null;
+ return builder.build();
}
private HttpJsonTransportChannel createChannel() throws IOException, GeneralSecurityException {
@@ -364,7 +369,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..f4cc4e92e4bd 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,41 @@ 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.getClass().getName())
+ .isEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory");
+ Object configurator = getPrivateField(factory, "configurator");
+ assertThat(configurator).isNotNull();
+ } else {
+ 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/gax-java/pom.xml b/sdk-platform-java/gax-java/pom.xml
index d7f9f6841fb6..6cfc2bb49cb0 100644
--- a/sdk-platform-java/gax-java/pom.xml
+++ b/sdk-platform-java/gax-java/pom.xml
@@ -181,6 +181,11 @@
${awaitility.version}
test
+