From dd9eb5f680fc830acf0d7258a38d4d96668cc17a Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Mon, 22 Jun 2026 19:45:40 +0000 Subject: [PATCH 01/10] test(showcase): add PQC integration tests for gRPC and HttpJson TAG=agy CONV=385b9ab5-874c-4c9a-b331-66dab51fef61 --- java-showcase/gapic-showcase/pom.xml | 6 + .../com/google/showcase/v1beta1/it/ITPqc.java | 418 ++++++++++++++++++ java-showcase/pom.xml | 20 + 3 files changed, 444 insertions(+) create mode 100644 java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java diff --git a/java-showcase/gapic-showcase/pom.xml b/java-showcase/gapic-showcase/pom.xml index e36caea4a8a5..35632eeaf38f 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-alpha2 + 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..c1754f07deb5 --- /dev/null +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java @@ -0,0 +1,418 @@ +/* + * 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 org.junit.jupiter.api.Assumptions.assumeTrue; + +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.ApiMethodDescriptor; +import com.google.api.gax.httpjson.ForwardingHttpJsonClientCall; +import com.google.api.gax.httpjson.ForwardingHttpJsonClientCallListener; +import com.google.api.gax.httpjson.HttpJsonCallOptions; +import com.google.api.gax.httpjson.HttpJsonChannel; +import com.google.api.gax.httpjson.HttpJsonClientCall; +import com.google.api.gax.httpjson.HttpJsonClientInterceptor; +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 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.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +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; + +public class ITPqc { + + private static String caCertPath; + private static String grpcEndpoint; + private static String httpjsonEndpoint; + private static boolean hasCert; + + @BeforeAll + static void setUp() { + caCertPath = System.getProperty("showcase.ca.cert", "../../certs/ca.crt"); + grpcEndpoint = System.getProperty("showcase.secure-grpc.endpoint", "localhost:7470"); + httpjsonEndpoint = + System.getProperty("showcase.secure-httpjson.endpoint", "https://localhost:7470"); + + File certFile = new File(caCertPath); + hasCert = certFile.exists() && certFile.isFile(); + + // Register Conscrypt provider if available and not already registered + if (hasCert) { + try { + if (Security.getProvider("Conscrypt") == null) { + Security.insertProviderAt(Conscrypt.newProvider(), 1); + } + } catch (Throwable t) { + System.err.println("Failed to register Conscrypt provider: " + t.getMessage()); + } + } + } + + @Test + void testGrpcPqc() throws Exception { + assumeTrue(hasCert, "CA Certificate not found at " + caCertPath + ". Skipping gRPC PQC test."); + + // Create channel credentials trusting the custom CA + ChannelCredentials creds = + TlsChannelCredentials.newBuilder().trustManager(new File(caCertPath)).build(); + + ManagedChannel channel = Grpc.newChannelBuilder(grpcEndpoint, creds).build(); + 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("x-showcase-tls-group", Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key versionKey = + Metadata.Key.of("x-showcase-tls-version", Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key cipherKey = + Metadata.Key.of("x-showcase-tls-cipher", Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key supportedGroupsKey = + Metadata.Key.of( + "x-showcase-tls-client-supported-groups", Metadata.ASCII_STRING_MARSHALLER); + + assertThat(capturedHeaders.get(groupKey)).isEqualTo("X25519MLKEM768"); + assertThat(capturedHeaders.get(versionKey)).isEqualTo("TLS 1.3"); + assertThat(capturedHeaders.get(cipherKey)).isEqualTo("TLS_AES_128_GCM_SHA256"); + assertThat(capturedHeaders.get(supportedGroupsKey)).isNotNull(); + } finally { + channel.shutdown(); + channel.awaitTermination(10, TimeUnit.SECONDS); + } + } + + @Test + void testHttpJsonPqc() throws Exception { + assumeTrue( + hasCert, "CA Certificate not found at " + caCertPath + ". Skipping HttpJson PQC test."); + + // Build custom SSLContext trusting the CA cert + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + try (InputStream is = Files.newInputStream(Paths.get(caCertPath))) { + Certificate cert = cf.generateCertificate(is); + trustStore.setCertificateEntry("showcase-ca", cert); + } + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(trustStore); + + // Force Conscrypt TLS context + SSLContext sslContext = SSLContext.getInstance("TLS", Conscrypt.newProvider()); + sslContext.init(null, tmf.getTrustManagers(), null); + + // Wrap socket factory to enforce PQC named groups + javax.net.ssl.SSLSocketFactory pqcFactory = + new PqcEnforcingSSLSocketFactory( + sslContext.getSocketFactory(), new String[] {"X25519MLKEM768"}); + + NetHttpTransport transport = + new NetHttpTransport.Builder().setSslSocketFactory(pqcFactory).build(); + + HttpJsonHeaderCapturingInterceptor interceptor = new HttpJsonHeaderCapturingInterceptor(); + + InstantiatingHttpJsonChannelProvider transportChannelProvider = + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport(transport) + .setEndpoint(httpjsonEndpoint) + .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.getCapturedHeaders(); + assertThat(capturedHeaders).isNotNull(); + + String negotiatedGroup = getSingleHeaderString(capturedHeaders, "x-showcase-tls-group"); + assertThat(negotiatedGroup).isEqualTo("X25519MLKEM768"); + + String tlsVersion = getSingleHeaderString(capturedHeaders, "x-showcase-tls-version"); + assertThat(tlsVersion).isEqualTo("TLS 1.3"); + + String tlsCipher = getSingleHeaderString(capturedHeaders, "x-showcase-tls-cipher"); + assertThat(tlsCipher).isEqualTo("TLS_AES_128_GCM_SHA256"); + + String supportedGroups = + getSingleHeaderString(capturedHeaders, "x-showcase-tls-client-supported-groups"); + assertThat(supportedGroups).isNotNull(); + } + } + + 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; + } + } + + private static class HttpJsonHeaderCapturingInterceptor implements HttpJsonClientInterceptor { + private HttpJsonMetadata capturedHeaders; + + @Override + public HttpJsonClientCall interceptCall( + ApiMethodDescriptor method, + HttpJsonCallOptions callOptions, + HttpJsonChannel next) { + return new ForwardingHttpJsonClientCall.SimpleForwardingHttpJsonClientCall( + next.newCall(method, callOptions)) { + @Override + public void start( + HttpJsonClientCall.Listener responseListener, HttpJsonMetadata requestHeaders) { + super.start( + new ForwardingHttpJsonClientCallListener.SimpleForwardingHttpJsonClientCallListener< + RespT>(responseListener) { + @Override + public void onHeaders(HttpJsonMetadata responseHeaders) { + capturedHeaders = responseHeaders; + super.onHeaders(responseHeaders); + } + }, + requestHeaders); + } + }; + } + + public HttpJsonMetadata getCapturedHeaders() { + return capturedHeaders; + } + } + + 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 java.util.List) { + java.util.List list = (java.util.List) valueObj; + if (!list.isEmpty()) { + return String.valueOf(list.get(0)); + } + } else if (valueObj != null) { + return String.valueOf(valueObj); + } + return null; + } + + private static void trySetNamedGroups(javax.net.ssl.SSLParameters params, String[] groups) { + try { + java.lang.reflect.Method setNamedGroupsMethod = + javax.net.ssl.SSLParameters.class.getMethod("setNamedGroups", String[].class); + setNamedGroupsMethod.invoke(params, (Object) groups); + } catch (Exception e) { + System.err.println("Failed to set named groups via reflection: " + e.getMessage()); + } + } + + private static class PqcEnforcingSSLSocketFactory extends javax.net.ssl.SSLSocketFactory { + private final javax.net.ssl.SSLSocketFactory delegate; + private final String[] groups; + + PqcEnforcingSSLSocketFactory(javax.net.ssl.SSLSocketFactory delegate, String[] groups) { + this.delegate = delegate; + this.groups = groups; + } + + private java.net.Socket configure(java.net.Socket socket) { + if (socket instanceof javax.net.ssl.SSLSocket) { + javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) socket; + javax.net.ssl.SSLParameters params = sslSocket.getSSLParameters(); + trySetNamedGroups(params, groups); + sslSocket.setSSLParameters(params); + } + return socket; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public java.net.Socket createSocket(java.net.Socket s, String host, int port, boolean autoClose) + throws java.io.IOException { + return configure(delegate.createSocket(s, host, port, autoClose)); + } + + @Override + public java.net.Socket createSocket() throws java.io.IOException { + return configure(delegate.createSocket()); + } + + @Override + public java.net.Socket createSocket(String host, int port) + throws java.io.IOException, java.net.UnknownHostException { + return configure(delegate.createSocket(host, port)); + } + + @Override + public java.net.Socket createSocket( + String host, int port, java.net.InetAddress localHost, int localPort) + throws java.io.IOException, java.net.UnknownHostException { + return configure(delegate.createSocket(host, port, localHost, localPort)); + } + + @Override + public java.net.Socket createSocket(java.net.InetAddress host, int port) + throws java.io.IOException { + return configure(delegate.createSocket(host, port)); + } + + @Override + public java.net.Socket createSocket( + java.net.InetAddress address, int port, java.net.InetAddress localAddress, int localPort) + throws java.io.IOException { + return configure(delegate.createSocket(address, port, localAddress, localPort)); + } + } +} 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 From 6cdd6692c8d596134ef71e386111db94a07803f8 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 30 Jun 2026 15:34:35 +0000 Subject: [PATCH 02/10] test(showcase): simplify PQC HTTP/JSON tests by using new native NetHttpTransport integration. Upgraded Conscrypt to 2.6-alpha5 to run tests. TAG=agy CONV=385b9ab5-874c-4c9a-b331-66dab51fef61 --- java-showcase/gapic-showcase/pom.xml | 2 +- .../com/google/showcase/v1beta1/it/ITPqc.java | 98 +------------------ .../gapic-generator-java-pom-parent/pom.xml | 2 +- 3 files changed, 7 insertions(+), 95 deletions(-) diff --git a/java-showcase/gapic-showcase/pom.xml b/java-showcase/gapic-showcase/pom.xml index 35632eeaf38f..5346f1d268e3 100644 --- a/java-showcase/gapic-showcase/pom.xml +++ b/java-showcase/gapic-showcase/pom.xml @@ -139,7 +139,7 @@ org.conscrypt conscrypt-openjdk-uber - 2.6-alpha2 + 2.6-alpha5 test 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 c1754f07deb5..89f65bb88492 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 @@ -58,8 +58,6 @@ import java.security.cert.CertificateFactory; import java.util.Collections; import java.util.concurrent.TimeUnit; -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; @@ -155,7 +153,7 @@ void testHttpJsonPqc() throws Exception { assumeTrue( hasCert, "CA Certificate not found at " + caCertPath + ". Skipping HttpJson PQC test."); - // Build custom SSLContext trusting the CA cert + // Build custom TrustManager trusting the CA cert KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore.load(null, null); CertificateFactory cf = CertificateFactory.getInstance("X.509"); @@ -163,21 +161,12 @@ void testHttpJsonPqc() throws Exception { Certificate cert = cf.generateCertificate(is); trustStore.setCertificateEntry("showcase-ca", cert); } - TrustManagerFactory tmf = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(trustStore); - - // Force Conscrypt TLS context - SSLContext sslContext = SSLContext.getInstance("TLS", Conscrypt.newProvider()); - sslContext.init(null, tmf.getTrustManagers(), null); - - // Wrap socket factory to enforce PQC named groups - javax.net.ssl.SSLSocketFactory pqcFactory = - new PqcEnforcingSSLSocketFactory( - sslContext.getSocketFactory(), new String[] {"X25519MLKEM768"}); + // Since Conscrypt was registered at position 1 in setUp(), + // trustCertificates will resolve SSLContext using Conscrypt, + // and NetHttpTransport will automatically wrap the socket factory to enforce PQC. NetHttpTransport transport = - new NetHttpTransport.Builder().setSslSocketFactory(pqcFactory).build(); + new NetHttpTransport.Builder().trustCertificates(trustStore).build(); HttpJsonHeaderCapturingInterceptor interceptor = new HttpJsonHeaderCapturingInterceptor(); @@ -338,81 +327,4 @@ private static String getSingleHeaderString(HttpJsonMetadata metadata, String na } return null; } - - private static void trySetNamedGroups(javax.net.ssl.SSLParameters params, String[] groups) { - try { - java.lang.reflect.Method setNamedGroupsMethod = - javax.net.ssl.SSLParameters.class.getMethod("setNamedGroups", String[].class); - setNamedGroupsMethod.invoke(params, (Object) groups); - } catch (Exception e) { - System.err.println("Failed to set named groups via reflection: " + e.getMessage()); - } - } - - private static class PqcEnforcingSSLSocketFactory extends javax.net.ssl.SSLSocketFactory { - private final javax.net.ssl.SSLSocketFactory delegate; - private final String[] groups; - - PqcEnforcingSSLSocketFactory(javax.net.ssl.SSLSocketFactory delegate, String[] groups) { - this.delegate = delegate; - this.groups = groups; - } - - private java.net.Socket configure(java.net.Socket socket) { - if (socket instanceof javax.net.ssl.SSLSocket) { - javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) socket; - javax.net.ssl.SSLParameters params = sslSocket.getSSLParameters(); - trySetNamedGroups(params, groups); - sslSocket.setSSLParameters(params); - } - return socket; - } - - @Override - public String[] getDefaultCipherSuites() { - return delegate.getDefaultCipherSuites(); - } - - @Override - public String[] getSupportedCipherSuites() { - return delegate.getSupportedCipherSuites(); - } - - @Override - public java.net.Socket createSocket(java.net.Socket s, String host, int port, boolean autoClose) - throws java.io.IOException { - return configure(delegate.createSocket(s, host, port, autoClose)); - } - - @Override - public java.net.Socket createSocket() throws java.io.IOException { - return configure(delegate.createSocket()); - } - - @Override - public java.net.Socket createSocket(String host, int port) - throws java.io.IOException, java.net.UnknownHostException { - return configure(delegate.createSocket(host, port)); - } - - @Override - public java.net.Socket createSocket( - String host, int port, java.net.InetAddress localHost, int localPort) - throws java.io.IOException, java.net.UnknownHostException { - return configure(delegate.createSocket(host, port, localHost, localPort)); - } - - @Override - public java.net.Socket createSocket(java.net.InetAddress host, int port) - throws java.io.IOException { - return configure(delegate.createSocket(host, port)); - } - - @Override - public java.net.Socket createSocket( - java.net.InetAddress address, int port, java.net.InetAddress localAddress, int localPort) - throws java.io.IOException { - return configure(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 From 6ec09e4388e0679ec6de1ec184a141900461e668 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 30 Jun 2026 15:51:19 +0000 Subject: [PATCH 03/10] chore: add build-with-local-http-client.sh script to compile both repos and run tests TAG=agy CONV=385b9ab5-874c-4c9a-b331-66dab51fef61 --- build-with-local-http-client.sh | 82 +++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100755 build-with-local-http-client.sh diff --git a/build-with-local-http-client.sh b/build-with-local-http-client.sh new file mode 100755 index 000000000000..4df2fa3c4079 --- /dev/null +++ b/build-with-local-http-client.sh @@ -0,0 +1,82 @@ +#!/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}" + +# Use JDK 17 by default for compiling and formatting (required for Spotify fmt plugin) +# If SDKMAN is installed, try using its JDK 17 +if [ -d "$HOME/.sdkman/candidates/java/17.0.19-tem" ]; then + export JAVA_HOME="$HOME/.sdkman/candidates/java/17.0.19-tem" +elif [ -d "/usr/lib/jvm/java-17-openjdk-amd64" ]; then + export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64" +fi + +if [ -n "$JAVA_HOME" ]; then + echo "Using JAVA_HOME: $JAVA_HOME" + export PATH="$JAVA_HOME/bin:$PATH" +else + echo "WARNING: JAVA_HOME for JDK 17 was not found. Using default java: $(java -version 2>&1 | head -n 1)" +fi + +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 + +# 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 -DskipTests +popd + +echo "=========================================================================" +echo "Building and verifying gapic-showcase with PQC in google-cloud-java..." +echo "=========================================================================" + +# We need Java 21+ to run the showcase tests because of Conscrypt TLS requirements, +# but if the user has custom JDK, we will respect it. +# Let's try to locate JDK 21 for showcase run if it exists, or just use the active JDK. +if [ -d "$HOME/.sdkman/candidates/java/17.0.19-tem" ]; then + # JDK 17 also works for running show-case tests if Conscrypt loads successfully + export JAVA_HOME="$HOME/.sdkman/candidates/java/17.0.19-tem" +elif [ -d "/usr/lib/jvm/java-21-openjdk-amd64" ]; then + export JAVA_HOME="/usr/lib/jvm/java-21-openjdk-amd64" +fi + +if [ -n "$JAVA_HOME" ]; then + export PATH="$JAVA_HOME/bin:$PATH" +fi + +# Run the showcase tests +mvn test -pl java-showcase/gapic-showcase -Dtest=ITPqc -Dshowcase.ca.cert="${HOME}/pqc-certs/ca.crt" From e62d44098e747154a25f48e1fb296fa6303fd13a Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 30 Jun 2026 15:56:24 +0000 Subject: [PATCH 04/10] chore: skip javadoc and clirr in build script to prevent build issues --- build-with-local-http-client.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-with-local-http-client.sh b/build-with-local-http-client.sh index 4df2fa3c4079..7070158f8118 100755 --- a/build-with-local-http-client.sh +++ b/build-with-local-http-client.sh @@ -57,7 +57,7 @@ pushd "${HTTP_CLIENT_DIR}" git checkout "${HTTP_CLIENT_BRANCH}" echo "Running maven install..." - mvn clean install -DskipTests + mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dclirr.skip=true popd echo "=========================================================================" From edcd15ea1c2d3cf145fc83bb259e821a7e58014e Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 30 Jun 2026 16:02:10 +0000 Subject: [PATCH 05/10] chore: update build script to skip test compilation entirely for faster builds --- build-with-local-http-client.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-with-local-http-client.sh b/build-with-local-http-client.sh index 7070158f8118..ffb81d42979c 100755 --- a/build-with-local-http-client.sh +++ b/build-with-local-http-client.sh @@ -57,7 +57,7 @@ pushd "${HTTP_CLIENT_DIR}" git checkout "${HTTP_CLIENT_BRANCH}" echo "Running maven install..." - mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dclirr.skip=true + mvn clean install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dclirr.skip=true popd echo "=========================================================================" From d54b30942d135e29927779a549b125b87f9ca804 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 30 Jun 2026 16:03:17 +0000 Subject: [PATCH 06/10] chore: skip http-client rebuild in build script if local m2 snapshot exists --- build-with-local-http-client.sh | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/build-with-local-http-client.sh b/build-with-local-http-client.sh index ffb81d42979c..f62a1e30201c 100755 --- a/build-with-local-http-client.sh +++ b/build-with-local-http-client.sh @@ -51,14 +51,22 @@ if [ ! -d "${HTTP_CLIENT_DIR}" ]; then exit 1 fi -# 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 -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dclirr.skip=true -popd +# 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/2.1.2-SNAPSHOT/google-http-client-2.1.2-SNAPSHOT.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 -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..." From 945a336b00bb8beba69c6220a729f931879b62f2 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 30 Jun 2026 16:05:56 +0000 Subject: [PATCH 07/10] test: configure jdk.tls.namedGroups in ITPqc to enable PQC for HttpJson --- .../src/test/java/com/google/showcase/v1beta1/it/ITPqc.java | 3 +++ 1 file changed, 3 insertions(+) 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 89f65bb88492..b3b74c0788b8 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 @@ -79,6 +79,9 @@ static void setUp() { File certFile = new File(caCertPath); hasCert = certFile.exists() && certFile.isFile(); + // Force Conscrypt and OpenJDK to prefer X25519MLKEM768 for TLS 1.3 + System.setProperty("jdk.tls.namedGroups", "X25519MLKEM768,X25519,secp256r1"); + // Register Conscrypt provider if available and not already registered if (hasCert) { try { From 360cf6fbab4095209de2ecad42ea876f33a99c0e Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 30 Jun 2026 19:21:43 +0000 Subject: [PATCH 08/10] feat: add pqc-verification module with BigQuery sample and setup README TAG=agy CONV=385b9ab5-874c-4c9a-b331-66dab51fef61 --- pqc-verification/README.md | 100 ++++++ pqc-verification/pom.xml | 71 +++++ .../java/com/google/cloud/pqc/BqPqcTest.java | 286 ++++++++++++++++++ 3 files changed, 457 insertions(+) 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/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..48234a6c47f0 --- /dev/null +++ b/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java @@ -0,0 +1,286 @@ +/* + * 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 org.conscrypt.Conscrypt; +import org.conscrypt.OpenSSLSocketImpl; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * 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 { + // 1. Try to register Conscrypt provider + registerConscrypt(); + 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()); + } catch (Throwable t) { + System.out.println("[DEBUG] Failed to get Conscrypt version: " + t.getMessage()); + } + + // Force Conscrypt and OpenJDK to prefer X25519MLKEM768 for TLS 1.3 + System.setProperty("jdk.tls.namedGroups", "X25519MLKEM768,X25519,secp256r1"); + + // 2. 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 registerConscrypt() { + try { + java.security.Provider provider = Conscrypt.newProvider(); + if (Security.getProvider("Conscrypt") == null) { + Security.insertProviderAt(provider, 1); + System.out.println("Registered Conscrypt provider at position 1."); + } + } catch (Throwable t) { + System.err.println("Failed to register Conscrypt: " + t.getMessage()); + t.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 { + // Strictly use Conscrypt provider for TLS context + SSLContext sslContext = SSLContext.getInstance("TLS", "Conscrypt"); + sslContext.init(null, null, null); + + SSLSocketFactory baseFactory = sslContext.getSocketFactory(); + SSLSocketFactory pqcFactory = new PqcEnforcingSSLSocketFactory( + baseFactory, new String[] {"X25519MLKEM768", "X25519"}); + SSLSocketFactory tracingFactory = new TracingSSLSocketFactory(pqcFactory); + + return new NetHttpTransport.Builder().setSslSocketFactory(tracingFactory).build(); + } catch (Exception e) { + throw new RuntimeException("Failed to create Conscrypt-enforced tracing transport", e); + } + } + } + + 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)); + } + } + + private static void trySetNamedGroups(SSLSocket sslSocket, String[] groups) { + try { + Conscrypt.setNamedGroups(sslSocket, groups); + } catch (Exception e) { + System.err.println("[TLS TRACE] Failed to set named groups: " + e.getMessage()); + e.printStackTrace(); + } + } + + private static class PqcEnforcingSSLSocketFactory extends SSLSocketFactory { + private final SSLSocketFactory delegate; + private final String[] groups; + + PqcEnforcingSSLSocketFactory(SSLSocketFactory delegate, String[] groups) { + this.delegate = delegate; + this.groups = groups; + } + + private Socket configure(Socket socket) { + if (socket instanceof SSLSocket) { + trySetNamedGroups((SSLSocket) socket, groups); + } + 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 configure(delegate.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket() throws IOException { + return configure(delegate.createSocket()); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return configure(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException, UnknownHostException { + return configure(delegate.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return configure(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket( + InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return configure(delegate.createSocket(address, port, localAddress, localPort)); + } + } +} From c00f8a58afa05252a274f57a19138e20f1ff7a46 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 30 Jun 2026 19:58:30 +0000 Subject: [PATCH 09/10] test(showcase): add test for explicit security provider override in HttpJson PQC and use doNotValidateCertificate TAG=agy CONV=385b9ab5-874c-4c9a-b331-66dab51fef61 --- build-with-local-http-client.sh | 32 +---- .../com/google/showcase/v1beta1/it/ITPqc.java | 121 +++++++++--------- 2 files changed, 64 insertions(+), 89 deletions(-) diff --git a/build-with-local-http-client.sh b/build-with-local-http-client.sh index f62a1e30201c..48ac87ca12ad 100755 --- a/build-with-local-http-client.sh +++ b/build-with-local-http-client.sh @@ -25,20 +25,6 @@ PARENT_DIR="$(cd "${MONOREPO_DIR}/.." && pwd)" HTTP_CLIENT_DIR="${HTTP_CLIENT_DIR:-${PARENT_DIR}/google-http-java-client}" HTTP_CLIENT_BRANCH="${HTTP_CLIENT_BRANCH:-pqc-support-conscrypt}" -# Use JDK 17 by default for compiling and formatting (required for Spotify fmt plugin) -# If SDKMAN is installed, try using its JDK 17 -if [ -d "$HOME/.sdkman/candidates/java/17.0.19-tem" ]; then - export JAVA_HOME="$HOME/.sdkman/candidates/java/17.0.19-tem" -elif [ -d "/usr/lib/jvm/java-17-openjdk-amd64" ]; then - export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64" -fi - -if [ -n "$JAVA_HOME" ]; then - echo "Using JAVA_HOME: $JAVA_HOME" - export PATH="$JAVA_HOME/bin:$PATH" -else - echo "WARNING: JAVA_HOME for JDK 17 was not found. Using default java: $(java -version 2>&1 | head -n 1)" -fi echo "=========================================================================" echo "Building and installing google-http-java-client snapshot..." @@ -64,7 +50,7 @@ else git checkout "${HTTP_CLIENT_BRANCH}" echo "Running maven install..." - mvn clean install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dclirr.skip=true + mvn clean install -pl google-http-client -am -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dclirr.skip=true popd fi @@ -72,19 +58,5 @@ echo "=========================================================================" echo "Building and verifying gapic-showcase with PQC in google-cloud-java..." echo "=========================================================================" -# We need Java 21+ to run the showcase tests because of Conscrypt TLS requirements, -# but if the user has custom JDK, we will respect it. -# Let's try to locate JDK 21 for showcase run if it exists, or just use the active JDK. -if [ -d "$HOME/.sdkman/candidates/java/17.0.19-tem" ]; then - # JDK 17 also works for running show-case tests if Conscrypt loads successfully - export JAVA_HOME="$HOME/.sdkman/candidates/java/17.0.19-tem" -elif [ -d "/usr/lib/jvm/java-21-openjdk-amd64" ]; then - export JAVA_HOME="/usr/lib/jvm/java-21-openjdk-amd64" -fi - -if [ -n "$JAVA_HOME" ]; then - export PATH="$JAVA_HOME/bin:$PATH" -fi - # Run the showcase tests -mvn test -pl java-showcase/gapic-showcase -Dtest=ITPqc -Dshowcase.ca.cert="${HOME}/pqc-certs/ca.crt" +mvn test -pl java-showcase/gapic-showcase -Dtest=ITPqc 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 b3b74c0788b8..422669a45db6 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 @@ -17,7 +17,6 @@ package com.google.showcase.v1beta1.it; import static com.google.common.truth.Truth.assertThat; -import static org.junit.jupiter.api.Assumptions.assumeTrue; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.gax.core.NoCredentialsProvider; @@ -38,24 +37,19 @@ import com.google.showcase.v1beta1.EchoResponse; import com.google.showcase.v1beta1.EchoSettings; 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.Security; -import java.security.cert.Certificate; -import java.security.cert.CertificateFactory; +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import java.security.Provider; import java.util.Collections; import java.util.concurrent.TimeUnit; import org.conscrypt.Conscrypt; @@ -64,45 +58,23 @@ public class ITPqc { - private static String caCertPath; - private static String grpcEndpoint; - private static String httpjsonEndpoint; - private static boolean hasCert; + private static final String GRPC_ENDPOINT = "localhost:7470"; + private static final String HTTPJSON_ENDPOINT = "https://localhost:7470"; @BeforeAll static void setUp() { - caCertPath = System.getProperty("showcase.ca.cert", "../../certs/ca.crt"); - grpcEndpoint = System.getProperty("showcase.secure-grpc.endpoint", "localhost:7470"); - httpjsonEndpoint = - System.getProperty("showcase.secure-httpjson.endpoint", "https://localhost:7470"); - - File certFile = new File(caCertPath); - hasCert = certFile.exists() && certFile.isFile(); - // Force Conscrypt and OpenJDK to prefer X25519MLKEM768 for TLS 1.3 System.setProperty("jdk.tls.namedGroups", "X25519MLKEM768,X25519,secp256r1"); - - // Register Conscrypt provider if available and not already registered - if (hasCert) { - try { - if (Security.getProvider("Conscrypt") == null) { - Security.insertProviderAt(Conscrypt.newProvider(), 1); - } - } catch (Throwable t) { - System.err.println("Failed to register Conscrypt provider: " + t.getMessage()); - } - } } @Test void testGrpcPqc() throws Exception { - assumeTrue(hasCert, "CA Certificate not found at " + caCertPath + ". Skipping gRPC PQC test."); + // Build insecure Netty SslContext to bypass certificate validation for testing + SslContext sslContext = GrpcSslContexts.configure( + SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)).build(); - // Create channel credentials trusting the custom CA - ChannelCredentials creds = - TlsChannelCredentials.newBuilder().trustManager(new File(caCertPath)).build(); - - ManagedChannel channel = Grpc.newChannelBuilder(grpcEndpoint, creds).build(); + ManagedChannel channel = + NettyChannelBuilder.forTarget(GRPC_ENDPOINT).sslContext(sslContext).build(); TransportChannel transportChannel = GrpcTransportChannel.create(channel); GrpcHeaderCapturingInterceptor interceptor = new GrpcHeaderCapturingInterceptor(); @@ -153,30 +125,15 @@ void testGrpcPqc() throws Exception { @Test void testHttpJsonPqc() throws Exception { - assumeTrue( - hasCert, "CA Certificate not found at " + caCertPath + ". Skipping HttpJson PQC test."); - - // Build custom TrustManager trusting the CA cert - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load(null, null); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - try (InputStream is = Files.newInputStream(Paths.get(caCertPath))) { - Certificate cert = cf.generateCertificate(is); - trustStore.setCertificateEntry("showcase-ca", cert); - } - - // Since Conscrypt was registered at position 1 in setUp(), - // trustCertificates will resolve SSLContext using Conscrypt, - // and NetHttpTransport will automatically wrap the socket factory to enforce PQC. - NetHttpTransport transport = - new NetHttpTransport.Builder().trustCertificates(trustStore).build(); + // Build NetHttpTransport with certificate validation disabled + NetHttpTransport transport = new NetHttpTransport.Builder().doNotValidateCertificate().build(); HttpJsonHeaderCapturingInterceptor interceptor = new HttpJsonHeaderCapturingInterceptor(); InstantiatingHttpJsonChannelProvider transportChannelProvider = EchoSettings.defaultHttpJsonTransportProviderBuilder() .setHttpTransport(transport) - .setEndpoint(httpjsonEndpoint) + .setEndpoint(HTTPJSON_ENDPOINT) .setInterceptorProvider(() -> Collections.singletonList(interceptor)) .build(); @@ -209,6 +166,52 @@ void testHttpJsonPqc() throws Exception { } } + @Test + void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception { + Provider explicitConscryptProvider = Conscrypt.newProvider(); + + // Build NetHttpTransport specifying the Conscrypt provider explicitly + NetHttpTransport transport = + new NetHttpTransport.Builder() + .setSecurityProvider(explicitConscryptProvider) + .doNotValidateCertificate() + .build(); + + HttpJsonHeaderCapturingInterceptor interceptor = new HttpJsonHeaderCapturingInterceptor(); + + InstantiatingHttpJsonChannelProvider transportChannelProvider = + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport(transport) + .setEndpoint(HTTPJSON_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.getCapturedHeaders(); + assertThat(capturedHeaders).isNotNull(); + + String negotiatedGroup = getSingleHeaderString(capturedHeaders, "x-showcase-tls-group"); + assertThat(negotiatedGroup).isEqualTo("X25519MLKEM768"); + + String tlsVersion = getSingleHeaderString(capturedHeaders, "x-showcase-tls-version"); + assertThat(tlsVersion).isEqualTo("TLS 1.3"); + + String tlsCipher = getSingleHeaderString(capturedHeaders, "x-showcase-tls-cipher"); + assertThat(tlsCipher).isEqualTo("TLS_AES_128_GCM_SHA256"); + } + } + private static class GrpcHeaderCapturingInterceptor implements ClientInterceptor { private Metadata capturedHeaders; From 164044a9507a294820f205e9010b11d7e6b29caf Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 30 Jun 2026 20:08:11 +0000 Subject: [PATCH 10/10] test(showcase): clean up ITPqc and build script, enforce CA cert presence, and reuse TestClientInitializer constants TAG=agy CONV=385b9ab5-874c-4c9a-b331-66dab51fef61 --- build-with-local-http-client.sh | 4 +- .../com/google/showcase/v1beta1/it/ITPqc.java | 312 +++++++++++------- .../java/com/google/cloud/pqc/BqPqcTest.java | 148 +++------ 3 files changed, 230 insertions(+), 234 deletions(-) diff --git a/build-with-local-http-client.sh b/build-with-local-http-client.sh index 48ac87ca12ad..f5293924273b 100755 --- a/build-with-local-http-client.sh +++ b/build-with-local-http-client.sh @@ -26,6 +26,8 @@ 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}" @@ -38,7 +40,7 @@ if [ ! -d "${HTTP_CLIENT_DIR}" ]; then 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/2.1.2-SNAPSHOT/google-http-client-2.1.2-SNAPSHOT.jar" +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}." 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 422669a45db6..ec79e288c2a5 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 @@ -17,17 +17,13 @@ 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.ApiMethodDescriptor; -import com.google.api.gax.httpjson.ForwardingHttpJsonClientCall; -import com.google.api.gax.httpjson.ForwardingHttpJsonClientCallListener; -import com.google.api.gax.httpjson.HttpJsonCallOptions; -import com.google.api.gax.httpjson.HttpJsonChannel; -import com.google.api.gax.httpjson.HttpJsonClientCall; -import com.google.api.gax.httpjson.HttpJsonClientInterceptor; import com.google.api.gax.httpjson.HttpJsonMetadata; import com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider; import com.google.api.gax.rpc.FixedTransportChannelProvider; @@ -36,87 +32,151 @@ 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.netty.shaded.io.grpc.netty.GrpcSslContexts; -import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext; -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; -import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +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 org.conscrypt.Conscrypt; +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: + * + *

    + *
  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 { - private static final String GRPC_ENDPOINT = "localhost:7470"; - private static final String HTTPJSON_ENDPOINT = "https://localhost:7470"; + // 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() { - // Force Conscrypt and OpenJDK to prefer X25519MLKEM768 for TLS 1.3 - System.setProperty("jdk.tls.namedGroups", "X25519MLKEM768,X25519,secp256r1"); + 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 { - // Build insecure Netty SslContext to bypass certificate validation for testing - SslContext sslContext = GrpcSslContexts.configure( - SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)).build(); - ManagedChannel channel = - NettyChannelBuilder.forTarget(GRPC_ENDPOINT).sslContext(sslContext).build(); - 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("x-showcase-tls-group", Metadata.ASCII_STRING_MARSHALLER); - Metadata.Key versionKey = - Metadata.Key.of("x-showcase-tls-version", Metadata.ASCII_STRING_MARSHALLER); - Metadata.Key cipherKey = - Metadata.Key.of("x-showcase-tls-cipher", Metadata.ASCII_STRING_MARSHALLER); - Metadata.Key supportedGroupsKey = - Metadata.Key.of( - "x-showcase-tls-client-supported-groups", Metadata.ASCII_STRING_MARSHALLER); - - assertThat(capturedHeaders.get(groupKey)).isEqualTo("X25519MLKEM768"); - assertThat(capturedHeaders.get(versionKey)).isEqualTo("TLS 1.3"); - assertThat(capturedHeaders.get(cipherKey)).isEqualTo("TLS_AES_128_GCM_SHA256"); - assertThat(capturedHeaders.get(supportedGroupsKey)).isNotNull(); + // 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); @@ -125,15 +185,17 @@ void testGrpcPqc() throws Exception { @Test void testHttpJsonPqc() throws Exception { - // Build NetHttpTransport with certificate validation disabled - NetHttpTransport transport = new NetHttpTransport.Builder().doNotValidateCertificate().build(); - HttpJsonHeaderCapturingInterceptor interceptor = new HttpJsonHeaderCapturingInterceptor(); + // 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(HTTPJSON_ENDPOINT) + .setEndpoint(DEFAULT_HTTPJSON_ENDPOINT.replace("http://", "https://")) .setInterceptorProvider(() -> Collections.singletonList(interceptor)) .build(); @@ -148,41 +210,47 @@ void testHttpJsonPqc() throws Exception { client.echo(EchoRequest.newBuilder().setContent("pqc-httpjson-test").build()); assertThat(response.getContent()).isEqualTo("pqc-httpjson-test"); - HttpJsonMetadata capturedHeaders = interceptor.getCapturedHeaders(); + HttpJsonMetadata capturedHeaders = interceptor.metadata; assertThat(capturedHeaders).isNotNull(); - String negotiatedGroup = getSingleHeaderString(capturedHeaders, "x-showcase-tls-group"); - assertThat(negotiatedGroup).isEqualTo("X25519MLKEM768"); + String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER); + assertThat(negotiatedGroup).isEqualTo(EXPECTED_TLS_GROUP); - String tlsVersion = getSingleHeaderString(capturedHeaders, "x-showcase-tls-version"); - assertThat(tlsVersion).isEqualTo("TLS 1.3"); + String tlsVersion = getSingleHeaderString(capturedHeaders, TLS_VERSION_HEADER); + assertThat(tlsVersion).isEqualTo(EXPECTED_TLS_VERSION); - String tlsCipher = getSingleHeaderString(capturedHeaders, "x-showcase-tls-cipher"); - assertThat(tlsCipher).isEqualTo("TLS_AES_128_GCM_SHA256"); + String tlsCipher = getSingleHeaderString(capturedHeaders, TLS_CIPHER_HEADER); + assertThat(tlsCipher).isEqualTo(EXPECTED_TLS_CIPHER); - String supportedGroups = - getSingleHeaderString(capturedHeaders, "x-showcase-tls-client-supported-groups"); + String supportedGroups = getSingleHeaderString(capturedHeaders, TLS_SUPPORTED_GROUPS_HEADER); assertThat(supportedGroups).isNotNull(); } } @Test void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception { - Provider explicitConscryptProvider = Conscrypt.newProvider(); - - // Build NetHttpTransport specifying the Conscrypt provider explicitly + // 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() - .setSecurityProvider(explicitConscryptProvider) - .doNotValidateCertificate() - .build(); + new NetHttpTransport.Builder().setSslSocketFactory(sslContext.getSocketFactory()).build(); - HttpJsonHeaderCapturingInterceptor interceptor = new HttpJsonHeaderCapturingInterceptor(); + HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor(); InstantiatingHttpJsonChannelProvider transportChannelProvider = EchoSettings.defaultHttpJsonTransportProviderBuilder() .setHttpTransport(transport) - .setEndpoint(HTTPJSON_ENDPOINT) + .setEndpoint(DEFAULT_HTTPJSON_ENDPOINT.replace("http://", "https://")) .setInterceptorProvider(() -> Collections.singletonList(interceptor)) .build(); @@ -198,20 +266,28 @@ void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception { EchoRequest.newBuilder().setContent("pqc-httpjson-explicit-provider-test").build()); assertThat(response.getContent()).isEqualTo("pqc-httpjson-explicit-provider-test"); - HttpJsonMetadata capturedHeaders = interceptor.getCapturedHeaders(); + HttpJsonMetadata capturedHeaders = interceptor.metadata; assertThat(capturedHeaders).isNotNull(); - String negotiatedGroup = getSingleHeaderString(capturedHeaders, "x-showcase-tls-group"); - assertThat(negotiatedGroup).isEqualTo("X25519MLKEM768"); + 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, "x-showcase-tls-version"); + String tlsVersion = getSingleHeaderString(capturedHeaders, TLS_VERSION_HEADER); assertThat(tlsVersion).isEqualTo("TLS 1.3"); - String tlsCipher = getSingleHeaderString(capturedHeaders, "x-showcase-tls-cipher"); + 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; @@ -241,38 +317,13 @@ public Metadata getCapturedHeaders() { } } - private static class HttpJsonHeaderCapturingInterceptor implements HttpJsonClientInterceptor { - private HttpJsonMetadata capturedHeaders; - - @Override - public HttpJsonClientCall interceptCall( - ApiMethodDescriptor method, - HttpJsonCallOptions callOptions, - HttpJsonChannel next) { - return new ForwardingHttpJsonClientCall.SimpleForwardingHttpJsonClientCall( - next.newCall(method, callOptions)) { - @Override - public void start( - HttpJsonClientCall.Listener responseListener, HttpJsonMetadata requestHeaders) { - super.start( - new ForwardingHttpJsonClientCallListener.SimpleForwardingHttpJsonClientCallListener< - RespT>(responseListener) { - @Override - public void onHeaders(HttpJsonMetadata responseHeaders) { - capturedHeaders = responseHeaders; - super.onHeaders(responseHeaders); - } - }, - requestHeaders); - } - }; - } - - public HttpJsonMetadata 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; @@ -323,8 +374,8 @@ public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedE private static String getSingleHeaderString(HttpJsonMetadata metadata, String name) { Object valueObj = metadata.getHeaders().get(name); - if (valueObj instanceof java.util.List) { - java.util.List list = (java.util.List) valueObj; + if (valueObj instanceof List) { + List list = (List) valueObj; if (!list.isEmpty()) { return String.valueOf(list.get(0)); } @@ -333,4 +384,15 @@ private static String getSingleHeaderString(HttpJsonMetadata metadata, String na } 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/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java b/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java index 48234a6c47f0..97fdee4cd102 100644 --- a/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java +++ b/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java @@ -28,36 +28,38 @@ import java.net.Socket; import java.net.UnknownHostException; import java.security.Security; -import org.conscrypt.Conscrypt; -import org.conscrypt.OpenSSLSocketImpl; 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. + * 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. + *

This code requires Conscrypt on the classpath to enable and detect PQC algorithms. */ public class BqPqcTest { public static void main(String[] args) throws Exception { - // 1. Try to register Conscrypt provider - registerConscrypt(); 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") + ")"); + 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 get Conscrypt version: " + t.getMessage()); + System.out.println("[DEBUG] Failed to register or get Conscrypt version: " + t.getMessage()); } - // Force Conscrypt and OpenJDK to prefer X25519MLKEM768 for TLS 1.3 - System.setProperty("jdk.tls.namedGroups", "X25519MLKEM768,X25519,secp256r1"); - - // 2. Build custom HttpTransportFactory with Tracing SSLSocketFactory + // 1. Build custom HttpTransportFactory with Tracing SSLSocketFactory HttpTransportFactory transportFactory = new TracingHttpTransportFactory(); HttpTransportOptions transportOptions = @@ -86,30 +88,23 @@ public static void main(String[] args) throws Exception { } } - private static void registerConscrypt() { - try { - java.security.Provider provider = Conscrypt.newProvider(); - if (Security.getProvider("Conscrypt") == null) { - Security.insertProviderAt(provider, 1); - System.out.println("Registered Conscrypt provider at position 1."); - } - } catch (Throwable t) { - System.err.println("Failed to register Conscrypt: " + t.getMessage()); - t.printStackTrace(); - } - } - private static void logHandshakeDetails( - String protocol, String cipherSuite, String curve, String methodUsed, String socketClassName) { + 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)")); + 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); @@ -121,15 +116,15 @@ private static class TracingHttpTransportFactory implements HttpTransportFactory @Override public HttpTransport create() { try { - // Strictly use Conscrypt provider for TLS context + // Build a standard Conscrypt-backed TLS context SSLContext sslContext = SSLContext.getInstance("TLS", "Conscrypt"); sslContext.init(null, null, null); + SSLSocketFactory conscryptFactory = sslContext.getSocketFactory(); - SSLSocketFactory baseFactory = sslContext.getSocketFactory(); - SSLSocketFactory pqcFactory = new PqcEnforcingSSLSocketFactory( - baseFactory, new String[] {"X25519MLKEM768", "X25519"}); - SSLSocketFactory tracingFactory = new TracingSSLSocketFactory(pqcFactory); + // 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); @@ -137,6 +132,11 @@ public HttpTransport create() { } } + /** + * 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; @@ -153,16 +153,17 @@ private Socket wrap(Socket socket) { 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()); + logHandshakeDetails( + protocol, cipherSuite, curve, methodUsed, rawSocket.getClass().getName()); } catch (Exception e) { System.err.println("Failed to log TLS handshake: " + e.getMessage()); } @@ -214,73 +215,4 @@ public Socket createSocket( return wrap(delegate.createSocket(address, port, localAddress, localPort)); } } - - private static void trySetNamedGroups(SSLSocket sslSocket, String[] groups) { - try { - Conscrypt.setNamedGroups(sslSocket, groups); - } catch (Exception e) { - System.err.println("[TLS TRACE] Failed to set named groups: " + e.getMessage()); - e.printStackTrace(); - } - } - - private static class PqcEnforcingSSLSocketFactory extends SSLSocketFactory { - private final SSLSocketFactory delegate; - private final String[] groups; - - PqcEnforcingSSLSocketFactory(SSLSocketFactory delegate, String[] groups) { - this.delegate = delegate; - this.groups = groups; - } - - private Socket configure(Socket socket) { - if (socket instanceof SSLSocket) { - trySetNamedGroups((SSLSocket) socket, groups); - } - 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 configure(delegate.createSocket(s, host, port, autoClose)); - } - - @Override - public Socket createSocket() throws IOException { - return configure(delegate.createSocket()); - } - - @Override - public Socket createSocket(String host, int port) throws IOException, UnknownHostException { - return configure(delegate.createSocket(host, port)); - } - - @Override - public Socket createSocket(String host, int port, InetAddress localHost, int localPort) - throws IOException, UnknownHostException { - return configure(delegate.createSocket(host, port, localHost, localPort)); - } - - @Override - public Socket createSocket(InetAddress host, int port) throws IOException { - return configure(delegate.createSocket(host, port)); - } - - @Override - public Socket createSocket( - InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { - return configure(delegate.createSocket(address, port, localAddress, localPort)); - } - } }