diff --git a/build-with-local-http-client.sh b/build-with-local-http-client.sh new file mode 100755 index 000000000000..f5293924273b --- /dev/null +++ b/build-with-local-http-client.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# +# 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. +# + +set -e + +# Find the directory of this script (root of google-cloud-java) +MONOREPO_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PARENT_DIR="$(cd "${MONOREPO_DIR}/.." && pwd)" + +# Path to the google-http-java-client repository +HTTP_CLIENT_DIR="${HTTP_CLIENT_DIR:-${PARENT_DIR}/google-http-java-client}" +HTTP_CLIENT_BRANCH="${HTTP_CLIENT_BRANCH:-pqc-support-conscrypt}" + + +HTTP_CLIENT_VERSION="${HTTP_CLIENT_VERSION:-2.1.2-SNAPSHOT}" + +echo "=========================================================================" +echo "Building and installing google-http-java-client snapshot..." +echo "Using path: ${HTTP_CLIENT_DIR}" +echo "=========================================================================" + +if [ ! -d "${HTTP_CLIENT_DIR}" ]; then + echo "Error: google-http-java-client directory not found at: ${HTTP_CLIENT_DIR}" + echo "You can specify its location by setting the HTTP_CLIENT_DIR environment variable." + exit 1 +fi + +# Check if the snapshot jar is already built in the local maven repository +M2_JAR_PATH="${HOME}/.m2/repository/com/google/http-client/google-http-client/${HTTP_CLIENT_VERSION}/google-http-client-${HTTP_CLIENT_VERSION}.jar" + +if [ -f "${M2_JAR_PATH}" ] && [ "${FORCE_REBUILD}" != "true" ]; then + echo "Found existing google-http-client snapshot at ${M2_JAR_PATH}." + echo "Skipping build. (To force rebuild, run with FORCE_REBUILD=true)" +else + # Store current directory and build http client + pushd "${HTTP_CLIENT_DIR}" + echo "Switching to branch ${HTTP_CLIENT_BRANCH} in google-http-java-client..." + git checkout "${HTTP_CLIENT_BRANCH}" + + echo "Running maven install..." + mvn clean install -pl google-http-client -am -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dclirr.skip=true + popd +fi + +echo "=========================================================================" +echo "Building and verifying gapic-showcase with PQC in google-cloud-java..." +echo "=========================================================================" + +# Run the showcase tests +mvn test -pl java-showcase/gapic-showcase -Dtest=ITPqc diff --git a/java-showcase/gapic-showcase/pom.xml b/java-showcase/gapic-showcase/pom.xml index e36caea4a8a5..5346f1d268e3 100644 --- a/java-showcase/gapic-showcase/pom.xml +++ b/java-showcase/gapic-showcase/pom.xml @@ -136,6 +136,12 @@ + + org.conscrypt + conscrypt-openjdk-uber + 2.6-alpha5 + test + 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..ec79e288c2a5 --- /dev/null +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java @@ -0,0 +1,398 @@ +/* + * 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 static com.google.showcase.v1beta1.it.util.TestClientInitializer.DEFAULT_GRPC_ENDPOINT; +import static com.google.showcase.v1beta1.it.util.TestClientInitializer.DEFAULT_HTTPJSON_ENDPOINT; + +import com.google.api.client.http.javanet.NetHttpTransport; +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_VERSION_HEADER = "x-showcase-tls-version"; + 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 EXPECTED_TLS_VERSION = "TLS 1.3"; + private static final String EXPECTED_TLS_CIPHER = "TLS_AES_128_GCM_SHA256"; + + private static final String DEFAULT_CA_CERT_PATH = "target/showcase-ca.pem"; + + @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(DEFAULT_GRPC_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 versionKey = + Metadata.Key.of(TLS_VERSION_HEADER, Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key cipherKey = + Metadata.Key.of(TLS_CIPHER_HEADER, Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key supportedGroupsKey = + Metadata.Key.of(TLS_SUPPORTED_GROUPS_HEADER, Metadata.ASCII_STRING_MARSHALLER); + + assertThat(capturedHeaders.get(groupKey)).isEqualTo(EXPECTED_TLS_GROUP); + assertThat(capturedHeaders.get(versionKey)).isEqualTo(EXPECTED_TLS_VERSION); + assertThat(capturedHeaders.get(cipherKey)).isEqualTo(EXPECTED_TLS_CIPHER); + assertThat(capturedHeaders.get(supportedGroupsKey)).isNotNull(); + } + } finally { + channel.shutdown(); + channel.awaitTermination(10, TimeUnit.SECONDS); + } + } + + @Test + void testHttpJsonPqc() throws Exception { + + // Build NetHttpTransport trusting the CA cert + NetHttpTransport transport = + new NetHttpTransport.Builder().trustCertificates(loadCaCert(DEFAULT_CA_CERT_PATH)).build(); + + HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor(); + + InstantiatingHttpJsonChannelProvider transportChannelProvider = + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport(transport) + .setEndpoint(DEFAULT_HTTPJSON_ENDPOINT.replace("http://", "https://")) + .setInterceptorProvider(() -> Collections.singletonList(interceptor)) + .build(); + + EchoSettings settings = + EchoSettings.newHttpJsonBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider(transportChannelProvider) + .build(); + + try (EchoClient client = EchoClient.create(settings)) { + EchoResponse response = + client.echo(EchoRequest.newBuilder().setContent("pqc-httpjson-test").build()); + assertThat(response.getContent()).isEqualTo("pqc-httpjson-test"); + + HttpJsonMetadata capturedHeaders = interceptor.metadata; + assertThat(capturedHeaders).isNotNull(); + + String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER); + assertThat(negotiatedGroup).isEqualTo(EXPECTED_TLS_GROUP); + + String tlsVersion = getSingleHeaderString(capturedHeaders, TLS_VERSION_HEADER); + assertThat(tlsVersion).isEqualTo(EXPECTED_TLS_VERSION); + + String tlsCipher = getSingleHeaderString(capturedHeaders, TLS_CIPHER_HEADER); + assertThat(tlsCipher).isEqualTo(EXPECTED_TLS_CIPHER); + + 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(DEFAULT_HTTPJSON_ENDPOINT.replace("http://", "https://")) + .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 classical + // X25519 + assertThat(negotiatedGroup).isEqualTo("X25519"); + + String tlsVersion = getSingleHeaderString(capturedHeaders, TLS_VERSION_HEADER); + assertThat(tlsVersion).isEqualTo("TLS 1.3"); + + String tlsCipher = getSingleHeaderString(capturedHeaders, TLS_CIPHER_HEADER); + assertThat(tlsCipher).isEqualTo("TLS_AES_128_GCM_SHA256"); + } + } + + /** + * 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; + } +} diff --git a/java-showcase/pom.xml b/java-showcase/pom.xml index 73ba577a62d1..394beae5f088 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/pqc-verification/README.md b/pqc-verification/README.md new file mode 100644 index 000000000000..b012af6447b6 --- /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: Generate TLS Certificates +Generate self-signed testing certificates using `openssl` (saved to `~/pqc-certs`): +```shell +mkdir -p ~/pqc-certs +openssl req -x509 -newkey rsa:4096 -keyout ~/pqc-certs/server.key -out ~/pqc-certs/server.crt -sha256 -days 365 -nodes -subj "/CN=localhost" +openssl x509 -outform pem -in ~/pqc-certs/server.crt -out ~/pqc-certs/ca.crt +``` + +### Step 2.3: Run the Showcase Server +Start the Showcase server in TLS mode using the generated certificate: +```shell +# Run on secure port 7470 +./gapic-showcase run \ + --tls-cert ~/pqc-certs/server.crt \ + --tls-key ~/pqc-certs/server.key \ + --port 7470 +``` + +--- + +## 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 Tracing Sample + +The class `BqPqcTest` runs a live connection to Google Cloud BigQuery, intercepts TLS sockets, and traces the negotiated curve/groups to verify `X25519MLKEM768` is used. + +### Run with Maven +To execute the BigQuery trace sample: + +```shell +cd pqc-verification + +# Run using exec-maven-plugin +mvn clean compile exec:java -Dproject.id="your-gcp-project-id" +``` + +### Expected Output +If Conscrypt is configured correctly and your environment supports PQC, you will see output tracing the handshake: +``` +[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) +[DEBUG] Conscrypt Version: 2.6.0 +Registered Conscrypt provider at position 1. +Initializing BigQuery client for project: your-gcp-project-id +Listing datasets using BigQuery Client with TLS tracing... +[TLS TRACE] Handshake Completed + Protocol : TLSv1.3 + CipherSuite: TLS_AES_128_GCM_SHA256 + Curve Name : X25519MLKEM768 (via Conscrypt OpenSSLSocketImpl.getCurveNameForTesting) + Is PQC? : YES (Hybrid Post-Quantum) +- my_dataset1 +- my_dataset2 +``` diff --git a/pqc-verification/pom.xml b/pqc-verification/pom.xml new file mode 100644 index 000000000000..83042ec76bcf --- /dev/null +++ b/pqc-verification/pom.xml @@ -0,0 +1,71 @@ + + + + 4.0.0 + + com.google.cloud.pqc + pqc-verification + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + 2.68.0-SNAPSHOT + 2.1.2-SNAPSHOT + 2.6-alpha5 + + + + + + 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} + + + + + + + + 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..97fdee4cd102 --- /dev/null +++ b/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java @@ -0,0 +1,218 @@ +/* + * 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.auth.http.HttpTransportFactory; +import com.google.cloud.bigquery.BigQuery; +import com.google.cloud.bigquery.BigQueryOptions; +import com.google.cloud.bigquery.Dataset; +import com.google.cloud.http.HttpTransportOptions; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.Security; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import org.conscrypt.Conscrypt; +import org.conscrypt.OpenSSLSocketImpl; + +/** + * A reproduction sample to trace TLS handshake details (protocol, cipher suite, and negotiated + * curve) for Google Cloud BigQuery client calls, verifying that PQC (X25519MLKEM768) is negotiated. + * + *

This code requires Conscrypt on the classpath to enable and detect PQC algorithms. + */ +public class BqPqcTest { + + 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") + + ")"); + try { + System.out.println("[DEBUG] Conscrypt Version: " + Conscrypt.version()); + Security.addProvider(Conscrypt.newProvider()); + System.out.println("Registered Conscrypt provider."); + } catch (Throwable t) { + System.out.println("[DEBUG] Failed to register or get Conscrypt version: " + t.getMessage()); + } + + // 1. Build custom HttpTransportFactory with Tracing SSLSocketFactory + HttpTransportFactory transportFactory = new TracingHttpTransportFactory(); + + HttpTransportOptions transportOptions = + HttpTransportOptions.newBuilder().setHttpTransportFactory(transportFactory).build(); + + // 3. Initialize BigQuery client + String projectId = System.getProperty("project.id", "lawrence-test-project-2"); + System.out.println("Initializing BigQuery client for project: " + projectId); + + BigQuery bigquery = + BigQueryOptions.newBuilder() + .setProjectId(projectId) + .setTransportOptions(transportOptions) + .build() + .getService(); + + // 4. Trigger a call to list datasets + System.out.println("Listing datasets using BigQuery Client with TLS tracing..."); + try { + for (Dataset dataset : bigquery.listDatasets().iterateAll()) { + System.out.println("- " + dataset.getDatasetId().getDataset()); + } + } catch (Exception e) { + System.err.println("API call failed: " + e.getMessage()); + e.printStackTrace(); + } + } + + private static void logHandshakeDetails( + String protocol, + String cipherSuite, + String curve, + String methodUsed, + String socketClassName) { + System.out.println("[TLS TRACE] Handshake Completed"); + System.out.println(" Protocol : " + protocol); + System.out.println(" CipherSuite: " + cipherSuite); + if (curve != null) { + System.out.println(" Curve Name : " + curve + " (via Conscrypt " + methodUsed + ")"); + boolean isPqc = + curve.equalsIgnoreCase("X25519MLKEM768") + || curve.toLowerCase().contains("mlkem") + || curve.toLowerCase().contains("kyber"); + System.out.println( + " Is PQC? : " + (isPqc ? "YES (Hybrid Post-Quantum)" : "NO (Classical)")); + } else { + System.out.println(" Curve Name : Unknown"); + System.out.println(" Socket Class: " + socketClassName); + System.out.println(" Is PQC? : UNKNOWN (Use Conscrypt to detect)"); + } + } + + private static class TracingHttpTransportFactory implements HttpTransportFactory { + @Override + public HttpTransport create() { + try { + // Build a standard Conscrypt-backed TLS context + SSLContext sslContext = SSLContext.getInstance("TLS", "Conscrypt"); + sslContext.init(null, null, null); + SSLSocketFactory conscryptFactory = sslContext.getSocketFactory(); + + // Wrap it in the tracing factory so we can log handshake outcomes + SSLSocketFactory tracingFactory = new TracingSSLSocketFactory(conscryptFactory); + + // NetHttpTransport automatically wraps our tracing factory to enforce PQC named groups + return new NetHttpTransport.Builder().setSslSocketFactory(tracingFactory).build(); + } catch (Exception e) { + throw new RuntimeException("Failed to create Conscrypt-enforced tracing transport", e); + } + } + } + + /** + * A wrapper SSLSocketFactory that registers a handshake completion listener to intercept and + * print TLS metadata (protocol, cipher suite, and negotiated group/curve) for developer + * visibility and testing. + */ + private static class TracingSSLSocketFactory extends SSLSocketFactory { + private final SSLSocketFactory delegate; + + public TracingSSLSocketFactory(SSLSocketFactory delegate) { + this.delegate = delegate; + } + + private Socket wrap(Socket socket) { + if (socket instanceof SSLSocket) { + SSLSocket sslSocket = (SSLSocket) socket; + sslSocket.addHandshakeCompletedListener( + event -> { + try { + String cipherSuite = event.getCipherSuite(); + String protocol = event.getSession().getProtocol(); + Socket rawSocket = event.getSocket(); + + String curve = null; + String methodUsed = ""; + + if (rawSocket instanceof OpenSSLSocketImpl) { + curve = ((OpenSSLSocketImpl) rawSocket).getCurveNameForTesting(); + methodUsed = "OpenSSLSocketImpl.getCurveNameForTesting"; + } + + logHandshakeDetails( + protocol, cipherSuite, curve, methodUsed, rawSocket.getClass().getName()); + } 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)); + } + } +} 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 2135cdb03c8d..9b823c6185d3 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,7 @@ consistent across modules in this repository --> 1.3.2 1.81.0 - 2.1.1 + 2.1.2-SNAPSHOT 2.13.2 33.5.0-jre 4.33.2