diff --git a/.github/workflows/pqc-tests.yml b/.github/workflows/pqc-tests.yml
new file mode 100644
index 000000000000..364340a3d2c7
--- /dev/null
+++ b/.github/workflows/pqc-tests.yml
@@ -0,0 +1,65 @@
+name: PQC Connectivity Integration Tests
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ pqc-tests:
+ runs-on: ubuntu-latest
+
+ steps:
+ # 1. Checkout sibling HTTP Client repository
+ - name: Checkout google-http-java-client
+ uses: actions/checkout@v4
+ with:
+ repository: googleapis/google-http-java-client
+ ref: chore/pqc-poc-2
+ path: google-http-java-client
+
+ # 2. Checkout this monorepo
+ - name: Checkout google-cloud-java-pqc
+ uses: actions/checkout@v4
+ with:
+ path: google-cloud-java-pqc
+
+ # 3. Set up JDK 17
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: 'maven'
+ cache-dependency-path: 'google-cloud-java-pqc/pom.xml'
+
+ # 4. Build and install modified google-http-client SNAPSHOT locally
+ - name: Build and Install google-http-java-client
+ run: |
+ cd google-http-java-client
+ mvn clean install -DskipTests=true -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip
+
+ # 5. Build and Install sdk-platform-java core libraries first (using monorepo root reactor to resolve siblings)
+ - name: Build and Install sdk-platform-java Core
+ run: |
+ cd google-cloud-java-pqc
+ mvn clean install -pl :gax,:gax-grpc,:gax-httpjson,:google-cloud-shared-dependencies,:first-party-dependencies,:third-party-dependencies -am -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -DskipTests=true
+
+ # 6. Build and Install snapshot bigquery, java-translate, and pqc-test targets (specifying actual sub-modules)
+ - name: Build and Install Client Snapshot Libraries and Test Modules
+ run: |
+ cd google-cloud-java-pqc
+ mvn clean install -pl java-bigquery/google-cloud-bigquery,java-translate/google-cloud-translate,sdk-platform-java/pqc-test/pqc-test-snapshot,sdk-platform-java/pqc-test/pqc-test-release -am -T 1.5C -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -DskipTests=true
+
+ # 7. Run Snapshot PQC Tests (EXPECT PASS)
+ - name: Run Snapshot PQC Connectivity Tests (Expect PASS)
+ run: |
+ cd google-cloud-java-pqc
+ mvn install -pl sdk-platform-java/pqc-test/pqc-test-snapshot -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -Dtest=RunPqcTest
+
+ # 8. Run Release PQC Tests (Expect PASS because tests assert negative behavior and pass)
+ - name: Run Release PQC Connectivity Tests
+ run: |
+ cd google-cloud-java-pqc
+ mvn install -pl sdk-platform-java/pqc-test/pqc-test-release -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -Dtest=RunPqcTest
diff --git a/sdk-platform-java/gax-java/gax-grpc/pom.xml b/sdk-platform-java/gax-java/gax-grpc/pom.xml
index 927518b32cf7..da457fdb5762 100644
--- a/sdk-platform-java/gax-java/gax-grpc/pom.xml
+++ b/sdk-platform-java/gax-java/gax-grpc/pom.xml
@@ -70,7 +70,6 @@
io.grpc
grpc-netty-shaded
- runtime
io.grpc
@@ -131,6 +130,16 @@
com.google.protobuf
protobuf-java
+
+ org.bouncycastle
+ bcprov-jdk18on
+ 1.84
+
+
+ org.bouncycastle
+ bctls-jdk18on
+ 1.84
+
diff --git a/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java b/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java
index c4543d986741..c359cdddfe8b 100644
--- a/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java
+++ b/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java
@@ -82,6 +82,11 @@
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
+import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
+import io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig;
+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.SslProvider;
import javax.annotation.Nullable;
import javax.net.ssl.KeyManagerFactory;
@@ -812,6 +817,8 @@ public ManagedChannelBuilder> createDecoratedChannelBuilder() throws IOExcepti
if (interceptorProvider != null) {
builder.intercept(interceptorProvider.getInterceptors());
}
+ configurePqc(builder);
+
if (channelConfigurator != null) {
builder = channelConfigurator.apply(builder);
}
@@ -829,6 +836,34 @@ private ManagedChannel createSingleChannel() throws IOException {
return managedChannel;
}
+ private void configurePqc(ManagedChannelBuilder> builder) {
+ NettyChannelBuilder nettyBuilder = (NettyChannelBuilder) builder;
+ try {
+ ApplicationProtocolConfig apn =
+ new ApplicationProtocolConfig(
+ ApplicationProtocolConfig.Protocol.ALPN,
+ ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
+ ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
+ "h2");
+
+ java.security.Provider bcProvider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
+ java.security.Provider bcJsseProvider =
+ new org.bouncycastle.jsse.provider.BouncyCastleJsseProvider(bcProvider);
+
+ SslContext shadedSslContext =
+ SslContextBuilder.forClient()
+ .sslProvider(SslProvider.JDK)
+ .sslContextProvider(bcJsseProvider)
+ .protocols("TLSv1.3")
+ .applicationProtocolConfig(apn)
+ .build();
+
+ nettyBuilder.sslContext(shadedSslContext);
+ } catch (Exception e) {
+ LOG.log(Level.WARNING, "Failed to configure shaded gRPC Netty channel for PQC", e);
+ }
+ }
+
/* Remove provided headers that will also get set by {@link com.google.auth.ApiKeyCredentials}. They will be added as part of the grpc call when performing auth
* {@link io.grpc.auth.GoogleAuthLibraryCallCredentials#applyRequestMetadata}. GRPC does not dedup headers {@link https://github.com/grpc/grpc-java/blob/a140e1bb0cfa662bcdb7823d73320eb8d49046f1/api/src/main/java/io/grpc/Metadata.java#L504} so we must before initiating the call.
*
diff --git a/sdk-platform-java/pom.xml b/sdk-platform-java/pom.xml
index b14a458db938..26a6aa31a4be 100644
--- a/sdk-platform-java/pom.xml
+++ b/sdk-platform-java/pom.xml
@@ -23,6 +23,7 @@
gapic-generator-java-bom
java-shared-dependencies
sdk-platform-java-config
+ pqc-test
diff --git a/sdk-platform-java/pqc-test/pom.xml b/sdk-platform-java/pqc-test/pom.xml
new file mode 100644
index 000000000000..7f0b833adcd4
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pom.xml
@@ -0,0 +1,39 @@
+
+
+ 4.0.0
+
+
+ com.google.cloud
+ google-cloud-shared-config
+ 1.17.0
+
+
+ com.google.api
+ pqc-test-parent
+ pom
+ 2.81.0-SNAPSHOT
+
+
+ pqc-test-common
+ pqc-test-snapshot
+ pqc-test-release
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ -Djavax.net.ssl.trustStore=${project.basedir}/../pqc-test-common/target/classes/pqctest.p12
+ -Djavax.net.ssl.trustStorePassword=password
+ -Djavax.net.ssl.trustStoreType=PKCS12
+
+
+
+
+
+
diff --git a/sdk-platform-java/pqc-test/pqc-test-common/pom.xml b/sdk-platform-java/pqc-test/pqc-test-common/pom.xml
new file mode 100644
index 000000000000..927bf757003e
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-common/pom.xml
@@ -0,0 +1,73 @@
+
+
+ 4.0.0
+
+
+ com.google.api
+ pqc-test-parent
+ 2.81.0-SNAPSHOT
+ ../pom.xml
+
+
+ pqc-test-common
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.10.2
+
+
+ io.grpc
+ grpc-netty
+ 1.81.0
+
+
+ io.grpc
+ grpc-stub
+ 1.81.0
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+ 1.84
+
+
+ org.bouncycastle
+ bctls-jdk18on
+ 1.84
+
+
+ com.google.api
+ gax
+ 2.81.0-SNAPSHOT
+
+
+ com.google.api
+ gax-httpjson
+ 2.81.0-SNAPSHOT
+
+
+ com.google.api
+ gax-grpc
+ 2.81.0-SNAPSHOT
+
+
+ com.google.http-client
+ google-http-client
+ 2.1.1-SNAPSHOT
+
+
+ com.google.cloud
+ google-cloud-bigquery
+ 2.67.0-SNAPSHOT
+
+
+ com.google.cloud
+ google-cloud-translate
+ 2.93.0-SNAPSHOT
+
+
+
diff --git a/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/httpjson/PqcConnectivityTest.java b/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/httpjson/PqcConnectivityTest.java
new file mode 100644
index 000000000000..16baefb249c8
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/httpjson/PqcConnectivityTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.httpjson;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.InputStream;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public abstract class PqcConnectivityTest {
+
+ private static Process serverProcess;
+ protected static int grpcPqcPort;
+ protected static int grpcClassicalPort;
+
+ protected boolean clientSupportsPqc() {
+ return true;
+ }
+
+ protected abstract boolean grpcTestShouldSucceed();
+
+ @BeforeAll
+ public static void setup() throws Exception {
+
+ // 6. Spawn PqcTestServer in a separate background process to ensure physical
+ // JVM runtime isolation!
+ ProcessBuilder pb =
+ new ProcessBuilder(
+ "java",
+ "-cp",
+ System.getProperty("java.class.path"),
+ "com.google.api.gax.pqc.PqcTestServer");
+
+ // Force merging of error stream to ease debugging in test output
+ pb.redirectErrorStream(true);
+ serverProcess = pb.start();
+
+ // Read server's stdout to dynamically capture the allocated ephemeral ports
+ java.io.BufferedReader reader =
+ new java.io.BufferedReader(
+ new java.io.InputStreamReader(
+ serverProcess.getInputStream(), java.nio.charset.StandardCharsets.UTF_8));
+
+ String line;
+ boolean grpcPqcFound = false;
+ boolean grpcClassicalFound = false;
+
+ // Wait for the server process to output its HTTP and gRPC ports
+ long startTime = System.currentTimeMillis();
+ while ((line = reader.readLine()) != null) {
+ System.out.println("[SERVER-OUT] " + line);
+ if (line.startsWith("GRPC_PQC_PORT: ")) {
+ grpcPqcPort = Integer.parseInt(line.substring(15).trim());
+ grpcPqcFound = true;
+ } else if (line.startsWith("GRPC_CLASSICAL_PORT: ")) {
+ grpcClassicalPort = Integer.parseInt(line.substring(21).trim());
+ grpcClassicalFound = true;
+ }
+
+ if (grpcPqcFound && grpcClassicalFound) {
+ break;
+ }
+
+ // Ephemeral port detection timeout (10 seconds) to fail-fast on server startup
+ // errors
+ if (System.currentTimeMillis() - startTime > 10000) {
+ throw new RuntimeException(
+ "Timeout waiting for PqcTestServer ephemeral ports to be printed!");
+ }
+ }
+
+ if (!grpcPqcFound || !grpcClassicalFound) {
+ throw new RuntimeException("PqcTestServer failed to initialize ephemeral ports!");
+ }
+
+ // Start a background thread to continuously drain the server's stdout
+ Thread drainThread =
+ new Thread(
+ () -> {
+ try {
+ String l;
+ while ((l = reader.readLine()) != null) {
+ System.out.println("[SERVER-OUT] " + l);
+ }
+ } catch (java.io.IOException e) {
+ // Ignore stream closed
+ }
+ });
+ drainThread.setDaemon(true);
+ drainThread.start();
+ }
+
+ @AfterAll
+ public static void teardown() {
+ if (serverProcess != null) {
+ // Forcibly destroy the background process and close standard streams to allow
+ // clean exit
+ serverProcess.destroyForcibly();
+ }
+ }
+
+ private void runGrpcTest(int port, boolean shouldSucceed) throws Exception {
+ com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.Builder channelProviderBuilder =
+ com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.newBuilder()
+ .setEndpoint("localhost:" + port)
+ .setHeaderProvider(() -> java.util.Collections.emptyMap());
+
+
+ com.google.api.gax.grpc.InstantiatingGrpcChannelProvider channelProvider = channelProviderBuilder.build();
+ com.google.api.gax.rpc.TransportChannel transportChannel = channelProvider.getTransportChannel();
+ io.grpc.Channel channel = ((com.google.api.gax.grpc.GrpcTransportChannel) transportChannel).getChannel();
+
+ try {
+ io.grpc.MethodDescriptor method =
+ io.grpc.MethodDescriptor.newBuilder()
+ .setType(io.grpc.MethodDescriptor.MethodType.UNARY)
+ .setFullMethodName("Greeter/SayHello")
+ .setRequestMarshaller(new ByteMarshaller())
+ .setResponseMarshaller(new ByteMarshaller())
+ .build();
+
+ byte[] response = io.grpc.stub.ClientCalls.blockingUnaryCall(channel, method, io.grpc.CallOptions.DEFAULT, "request".getBytes());
+ if (!shouldSucceed) {
+ fail("Expected gRPC call to fail!");
+ }
+ assertNotNull(response);
+ assertEquals("PQC gRPC OK", new String(response));
+ } catch (Exception e) {
+ if (shouldSucceed) {
+ fail("Expected gRPC call to succeed, but failed: " + e.getMessage(), e);
+ }
+ } finally {
+ if (channel instanceof io.grpc.ManagedChannel) {
+ ((io.grpc.ManagedChannel) channel).shutdownNow();
+ }
+ }
+ }
+
+
+
+ @Test
+ public void testGrpcPqcServerEnforced() throws Exception {
+ runGrpcTest(grpcPqcPort, grpcTestShouldSucceed());
+ }
+
+ @Test
+ public void testGrpcClassicalServer() throws Exception {
+ runGrpcTest(grpcClassicalPort, true);
+ }
+
+ private static class ByteMarshaller implements io.grpc.MethodDescriptor.Marshaller {
+ @Override
+ public InputStream stream(byte[] value) {
+ return new java.io.ByteArrayInputStream(value);
+ }
+
+ @Override
+ public byte[] parse(InputStream stream) {
+ try {
+ return com.google.common.io.ByteStreams.toByteArray(stream);
+ } catch (java.io.IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
diff --git a/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/pqc/PqcTestServer.java b/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/pqc/PqcTestServer.java
new file mode 100644
index 000000000000..5008213bcc3c
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/pqc/PqcTestServer.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.google.api.gax.pqc;
+
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsParameters;
+import com.sun.net.httpserver.HttpsServer;
+import io.grpc.Server;
+import io.grpc.netty.NettyServerBuilder;
+import io.netty.handler.ssl.ApplicationProtocolConfig;
+import io.netty.handler.ssl.ClientAuth;
+import io.netty.handler.ssl.IdentityCipherSuiteFilter;
+import io.netty.handler.ssl.JdkSslContext;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.function.BiFunction;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
+
+/**
+ * PqcTestServer is a specialized test harness designed to validate Post-Quantum Cryptography (PQC)
+ * transport enforcement in the Google Cloud Java SDK.
+ */
+public class PqcTestServer {
+
+ private HttpsServer httpServerPqc;
+ private HttpsServer httpServerClassical;
+ private Server grpcServerPqc;
+ private Server grpcServerClassical;
+ private int httpPqcPort;
+ private int httpClassicalPort;
+ private int grpcPqcPort;
+ private int grpcClassicalPort;
+
+ public void start() throws Exception {
+
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ try (InputStream is = getClass().getResourceAsStream("/pqctest.p12")) {
+ if (is == null) {
+ throw new RuntimeException("pqctest.p12 not found in classpath");
+ }
+ ks.load(is, "password".toCharArray());
+ }
+
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(ks, "password".toCharArray());
+
+ javax.net.ssl.TrustManagerFactory tmf =
+ javax.net.ssl.TrustManagerFactory.getInstance(
+ javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(ks);
+
+ BouncyCastleProvider bcProvider = new BouncyCastleProvider();
+ BouncyCastleJsseProvider bcJsseProvider = new BouncyCastleJsseProvider(bcProvider);
+ SSLContext bcContext = SSLContext.getInstance("TLSv1.3", bcJsseProvider);
+ bcContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
+
+ SSLContext pqcEnforcingSslContext =
+ new SSLContext(
+ new PqcEnforcingSSLContextSpi(bcContext),
+ bcContext.getProvider(),
+ bcContext.getProtocol()) {};
+
+ SSLContext classicalSslContext = bcContext;
+
+ httpServerPqc = HttpsServer.create(new InetSocketAddress(0), 0);
+ configureHttpServer(httpServerPqc, pqcEnforcingSslContext);
+ httpServerPqc.start();
+ httpPqcPort = httpServerPqc.getAddress().getPort();
+
+ httpServerClassical = HttpsServer.create(new InetSocketAddress(0), 0);
+ configureHttpServer(httpServerClassical, classicalSslContext);
+ httpServerClassical.start();
+ httpClassicalPort = httpServerClassical.getAddress().getPort();
+
+ ApplicationProtocolConfig apn =
+ new ApplicationProtocolConfig(
+ ApplicationProtocolConfig.Protocol.ALPN,
+ ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
+ ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
+ "h2");
+
+ io.netty.handler.ssl.SslContext nettySslContextPqc =
+ new JdkSslContext(
+ pqcEnforcingSslContext, false, null, IdentityCipherSuiteFilter.INSTANCE, apn, ClientAuth.NONE);
+
+ io.netty.handler.ssl.SslContext nettySslContextClassical =
+ new JdkSslContext(
+ classicalSslContext, false, null, IdentityCipherSuiteFilter.INSTANCE, apn, ClientAuth.NONE);
+
+ grpcServerPqc = createGrpcServer(nettySslContextPqc);
+ grpcServerPqc.start();
+ grpcPqcPort = grpcServerPqc.getPort();
+
+ grpcServerClassical = createGrpcServer(nettySslContextClassical);
+ grpcServerClassical.start();
+ grpcClassicalPort = grpcServerClassical.getPort();
+ }
+
+ private void configureHttpServer(HttpsServer server, SSLContext sslContext) {
+ server.setHttpsConfigurator(
+ new HttpsConfigurator(sslContext) {
+ @Override
+ public void configure(HttpsParameters params) {
+ SSLParameters sslparams = getSSLContext().getDefaultSSLParameters();
+ sslparams.setProtocols(new String[] {"TLSv1.3"});
+ params.setSSLParameters(sslparams);
+ }
+ });
+
+ server.createContext(
+ "/test",
+ exchange -> {
+ String response = "PQC HTTP OK";
+ exchange.sendResponseHeaders(200, response.length());
+ exchange.getResponseBody().write(response.getBytes());
+ exchange.getResponseBody().close();
+ });
+
+ server.createContext(
+ "/bigquery/v2/projects/test-project/datasets",
+ exchange -> {
+ String response = "{\"kind\": \"bigquery#datasetList\"}";
+ exchange.getResponseHeaders().set("Content-Type", "application/json");
+ exchange.sendResponseHeaders(200, response.length());
+ exchange.getResponseBody().write(response.getBytes());
+ exchange.getResponseBody().close();
+ });
+
+ server.createContext(
+ "/v3/",
+ exchange -> {
+ if ("POST".equalsIgnoreCase(exchange.getRequestMethod())) {
+ String response =
+ "{\"translations\": [{\"translatedText\": \"mocked translated text\"}]}";
+ exchange.getResponseHeaders().set("Content-Type", "application/json");
+ exchange.sendResponseHeaders(200, response.length());
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(response.getBytes());
+ }
+ }
+ });
+ }
+
+ private Server createGrpcServer(io.netty.handler.ssl.SslContext nettySslContext) {
+ io.grpc.MethodDescriptor method =
+ io.grpc.MethodDescriptor.newBuilder()
+ .setType(io.grpc.MethodDescriptor.MethodType.UNARY)
+ .setFullMethodName("Greeter/SayHello")
+ .setRequestMarshaller(new ByteMarshaller())
+ .setResponseMarshaller(new ByteMarshaller())
+ .build();
+
+ io.grpc.ServerServiceDefinition serviceDef =
+ io.grpc.ServerServiceDefinition.builder("Greeter")
+ .addMethod(
+ method,
+ io.grpc.stub.ServerCalls.asyncUnaryCall(
+ (request, responseObserver) -> {
+ responseObserver.onNext("PQC gRPC OK".getBytes());
+ responseObserver.onCompleted();
+ }))
+ .build();
+
+ io.grpc.MethodDescriptor translateMethod =
+ io.grpc.MethodDescriptor.newBuilder()
+ .setType(io.grpc.MethodDescriptor.MethodType.UNARY)
+ .setFullMethodName("google.cloud.translation.v3.TranslationService/TranslateText")
+ .setRequestMarshaller(new ByteMarshaller())
+ .setResponseMarshaller(new ByteMarshaller())
+ .build();
+
+ io.grpc.ServerServiceDefinition translationServiceDef =
+ io.grpc.ServerServiceDefinition.builder("google.cloud.translation.v3.TranslationService")
+ .addMethod(
+ translateMethod,
+ io.grpc.stub.ServerCalls.asyncUnaryCall(
+ (request, responseObserver) -> {
+ responseObserver.onNext(new byte[0]); // Empty proto response
+ responseObserver.onCompleted();
+ }))
+ .build();
+
+ return NettyServerBuilder.forPort(0)
+ .sslContext(nettySslContext)
+ .addService(serviceDef)
+ .addService(translationServiceDef)
+ .build();
+ }
+
+ public void stop() {
+ if (httpServerPqc != null) httpServerPqc.stop(0);
+ if (httpServerClassical != null) httpServerClassical.stop(0);
+ if (grpcServerPqc != null) grpcServerPqc.shutdown();
+ if (grpcServerClassical != null) grpcServerClassical.shutdown();
+ }
+
+ public int getHttpPqcPort() {
+ return httpPqcPort;
+ }
+
+ public int getHttpClassicalPort() {
+ return httpClassicalPort;
+ }
+
+ public int getGrpcPqcPort() {
+ return grpcPqcPort;
+ }
+
+ public int getGrpcClassicalPort() {
+ return grpcClassicalPort;
+ }
+
+ private static class ByteMarshaller implements io.grpc.MethodDescriptor.Marshaller {
+ @Override
+ public InputStream stream(byte[] value) {
+ return new java.io.ByteArrayInputStream(value);
+ }
+
+ @Override
+ public byte[] parse(InputStream stream) {
+ try {
+ return com.google.common.io.ByteStreams.toByteArray(stream);
+ } catch (java.io.IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ PqcTestServer server = new PqcTestServer();
+ server.start();
+
+ System.out.println("HTTP_PQC_PORT: " + server.getHttpPqcPort());
+ System.out.println("HTTP_CLASSICAL_PORT: " + server.getHttpClassicalPort());
+ System.out.println("GRPC_PQC_PORT: " + server.getGrpcPqcPort());
+ System.out.println("GRPC_CLASSICAL_PORT: " + server.getGrpcClassicalPort());
+ System.out.flush();
+
+ try {
+ while (System.in.read() != -1) {
+ Thread.sleep(1000);
+ }
+ } catch (Exception e) {
+ } finally {
+ server.stop();
+ }
+ }
+
+ @org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
+ private static class PqcEnforcingSSLEngine extends javax.net.ssl.SSLEngine {
+ private final javax.net.ssl.SSLEngine delegate;
+
+ PqcEnforcingSSLEngine(javax.net.ssl.SSLEngine delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void setSSLParameters(javax.net.ssl.SSLParameters params) {
+ delegate.setSSLParameters(params);
+ Object objEngine = delegate;
+ if (objEngine instanceof org.bouncycastle.jsse.BCSSLEngine) {
+ org.bouncycastle.jsse.BCSSLEngine bcEngine = (org.bouncycastle.jsse.BCSSLEngine) objEngine;
+ org.bouncycastle.jsse.BCSSLParameters bcParams = bcEngine.getParameters();
+ bcParams.setNamedGroups(new String[] {"X25519MLKEM768"});
+ bcEngine.setParameters(bcParams);
+ }
+ }
+
+ @Override
+ public void setHandshakeApplicationProtocolSelector(
+ BiFunction, String> selector) {
+ delegate.setHandshakeApplicationProtocolSelector(
+ (engine, protocols) -> selector.apply(this, protocols));
+ }
+
+ @Override
+ public BiFunction, String> getHandshakeApplicationProtocolSelector() {
+ return delegate.getHandshakeApplicationProtocolSelector();
+ }
+
+ @Override
+ public String getApplicationProtocol() {
+ return delegate.getApplicationProtocol();
+ }
+
+ @Override
+ public String getHandshakeApplicationProtocol() {
+ return delegate.getHandshakeApplicationProtocol();
+ }
+
+ @Override
+ public javax.net.ssl.SSLParameters getSSLParameters() {
+ return delegate.getSSLParameters();
+ }
+
+ @Override
+ public void beginHandshake() throws javax.net.ssl.SSLException {
+ delegate.beginHandshake();
+ }
+
+ @Override
+ public void closeInbound() throws javax.net.ssl.SSLException {
+ delegate.closeInbound();
+ }
+
+ @Override
+ public void closeOutbound() {
+ delegate.closeOutbound();
+ }
+
+ @Override
+ public java.lang.Runnable getDelegatedTask() {
+ return delegate.getDelegatedTask();
+ }
+
+ @Override
+ public java.lang.String[] getEnabledCipherSuites() {
+ return delegate.getEnabledCipherSuites();
+ }
+
+ @Override
+ public java.lang.String[] getEnabledProtocols() {
+ return delegate.getEnabledProtocols();
+ }
+
+ @Override
+ public javax.net.ssl.SSLEngineResult.HandshakeStatus getHandshakeStatus() {
+ return delegate.getHandshakeStatus();
+ }
+
+ @Override
+ public boolean getNeedClientAuth() {
+ return delegate.getNeedClientAuth();
+ }
+
+ @Override
+ public javax.net.ssl.SSLSession getSession() {
+ return delegate.getSession();
+ }
+
+ @Override
+ public java.lang.String[] getSupportedCipherSuites() {
+ return delegate.getSupportedCipherSuites();
+ }
+
+ @Override
+ public java.lang.String[] getSupportedProtocols() {
+ return delegate.getSupportedProtocols();
+ }
+
+ @Override
+ public boolean getUseClientMode() {
+ return delegate.getUseClientMode();
+ }
+
+ @Override
+ public boolean getWantClientAuth() {
+ return delegate.getWantClientAuth();
+ }
+
+ @Override
+ public boolean isInboundDone() {
+ return delegate.isInboundDone();
+ }
+
+ @Override
+ public boolean isOutboundDone() {
+ return delegate.isOutboundDone();
+ }
+
+ @Override
+ public void setEnabledCipherSuites(java.lang.String[] suites) {
+ delegate.setEnabledCipherSuites(suites);
+ }
+
+ @Override
+ public void setEnabledProtocols(java.lang.String[] protocols) {
+ delegate.setEnabledProtocols(protocols);
+ }
+
+ @Override
+ public void setNeedClientAuth(boolean need) {
+ delegate.setNeedClientAuth(need);
+ }
+
+ @Override
+ public void setUseClientMode(boolean mode) {
+ delegate.setUseClientMode(mode);
+ }
+
+ @Override
+ public void setWantClientAuth(boolean want) {
+ delegate.setWantClientAuth(want);
+ }
+
+ @Override
+ public javax.net.ssl.SSLEngineResult unwrap(
+ java.nio.ByteBuffer src, java.nio.ByteBuffer[] dsts, int offset, int length)
+ throws javax.net.ssl.SSLException {
+ return delegate.unwrap(src, dsts, offset, length);
+ }
+
+ @Override
+ public javax.net.ssl.SSLEngineResult wrap(
+ java.nio.ByteBuffer[] srcs, int offset, int length, java.nio.ByteBuffer dst)
+ throws javax.net.ssl.SSLException {
+ return delegate.wrap(srcs, offset, length, dst);
+ }
+
+ @Override
+ public boolean getEnableSessionCreation() {
+ return delegate.getEnableSessionCreation();
+ }
+
+ @Override
+ public void setEnableSessionCreation(boolean flag) {
+ delegate.setEnableSessionCreation(flag);
+ }
+
+ @Override
+ public javax.net.ssl.SSLSession getHandshakeSession() {
+ return delegate.getHandshakeSession();
+ }
+ }
+
+ private static class PqcEnforcingSSLContextSpi extends javax.net.ssl.SSLContextSpi {
+ private final javax.net.ssl.SSLContext delegate;
+
+ PqcEnforcingSSLContextSpi(javax.net.ssl.SSLContext delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ protected javax.net.ssl.SSLEngine engineCreateSSLEngine() {
+ return new PqcEnforcingSSLEngine(delegate.createSSLEngine());
+ }
+
+ @Override
+ protected javax.net.ssl.SSLEngine engineCreateSSLEngine(java.lang.String host, int port) {
+ return new PqcEnforcingSSLEngine(delegate.createSSLEngine(host, port));
+ }
+
+ @Override
+ protected javax.net.ssl.SSLSessionContext engineGetClientSessionContext() {
+ return delegate.getClientSessionContext();
+ }
+
+ @Override
+ protected javax.net.ssl.SSLSessionContext engineGetServerSessionContext() {
+ return delegate.getServerSessionContext();
+ }
+
+ @Override
+ protected javax.net.ssl.SSLServerSocketFactory engineGetServerSocketFactory() {
+ return delegate.getServerSocketFactory();
+ }
+
+ @Override
+ protected javax.net.ssl.SSLSocketFactory engineGetSocketFactory() {
+ return delegate.getSocketFactory();
+ }
+
+ @Override
+ protected void engineInit(
+ javax.net.ssl.KeyManager[] km,
+ javax.net.ssl.TrustManager[] tm,
+ java.security.SecureRandom sr)
+ throws java.security.KeyManagementException {}
+ }
+}
diff --git a/sdk-platform-java/pqc-test/pqc-test-common/src/main/resources/pqctest.p12 b/sdk-platform-java/pqc-test/pqc-test-common/src/main/resources/pqctest.p12
new file mode 100644
index 000000000000..92c74c66d3f0
Binary files /dev/null and b/sdk-platform-java/pqc-test/pqc-test-common/src/main/resources/pqctest.p12 differ
diff --git a/sdk-platform-java/pqc-test/pqc-test-release/pom.xml b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml
new file mode 100644
index 000000000000..a4fa2e7cc352
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml
@@ -0,0 +1,44 @@
+
+
+ 4.0.0
+
+
+ com.google.api
+ pqc-test-parent
+ 2.81.0-SNAPSHOT
+ ../pom.xml
+
+
+ pqc-test-release
+
+
+
+ com.google.http-client
+ google-http-client
+ 2.1.1-SNAPSHOT
+
+
+ com.google.api
+ pqc-test-common
+ 2.81.0-SNAPSHOT
+
+
+ com.google.cloud
+ google-cloud-bigquery
+ 2.66.0
+
+
+ com.google.cloud
+ google-cloud-translate
+ 2.92.0
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.10.2
+ test
+
+
+
diff --git a/sdk-platform-java/pqc-test/pqc-test-release/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java b/sdk-platform-java/pqc-test/pqc-test-release/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java
new file mode 100644
index 000000000000..2ed7e408754c
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-release/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.google.api.gax.httpjson;
+
+public class RunPqcTest extends PqcConnectivityTest {
+
+ @Override
+ protected boolean clientSupportsPqc() {
+ return false;
+ }
+
+ @Override
+ protected boolean grpcTestShouldSucceed() {
+ return false;
+ }
+}
diff --git a/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml b/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml
new file mode 100644
index 000000000000..3b5271924c6c
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+
+ com.google.api
+ pqc-test-parent
+ 2.81.0-SNAPSHOT
+ ../pom.xml
+
+
+ pqc-test-snapshot
+
+
+
+ com.google.api
+ pqc-test-common
+ 2.81.0-SNAPSHOT
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.10.2
+ test
+
+
+ com.google.cloud
+ google-cloud-bigquery
+ 2.67.0-SNAPSHOT
+ test
+
+
+ com.google.cloud
+ google-cloud-translate
+ 2.93.0-SNAPSHOT
+
+
+
+
diff --git a/sdk-platform-java/pqc-test/pqc-test-snapshot/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java b/sdk-platform-java/pqc-test/pqc-test-snapshot/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java
new file mode 100644
index 000000000000..5454cd4df934
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-snapshot/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.httpjson;
+
+public class RunPqcTest extends PqcConnectivityTest {
+
+ @Override
+ protected boolean clientSupportsPqc() {
+ return true;
+ }
+
+ @Override
+ protected boolean grpcTestShouldSucceed() {
+ return true;
+ }
+}