diff --git a/.github/workflows/pqc-tests.yml b/.github/workflows/pqc-tests.yml
new file mode 100644
index 000000000000..6fdcac705332
--- /dev/null
+++ b/.github/workflows/pqc-tests.yml
@@ -0,0 +1,67 @@
+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 the entire monorepo core components required by the tests
+ - name: Build and Install Core Dependency Reactor
+ run: |
+ cd google-cloud-java-pqc
+ mvn clean install -pl 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
+
+ # 6. Run Snapshot PQC Tests (EXPECT PASS)
+ - name: Run Snapshot PQC Connectivity Tests (Expect PASS)
+ run: |
+ cd google-cloud-java-pqc/sdk-platform-java/pqc-test/pqc-test-snapshot
+ mvn install -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -Dtest=RunPqcTest
+
+ # 7. Run Release PQC Tests (EXPECT FAIL)
+ - name: Run Release PQC Connectivity Tests (Expect FAIL)
+ # We expect this step to fail. If it passes, it means release libraries are negotiating PQC (which is incorrect).
+ # Thus we run it and assert that the maven command fails (exit code != 0).
+ run: |
+ cd google-cloud-java-pqc/sdk-platform-java/pqc-test/pqc-test-release
+ if mvn install -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -Dtest=RunPqcTest; then
+ echo "Error: Release tests passed but they were expected to fail!"
+ exit 1
+ else
+ echo "Success: Release tests failed-fast as expected."
+ exit 0
+ fi
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java
index 643c3dc7dc65..944987e0b3a6 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java
@@ -31,6 +31,10 @@
package com.google.auth.oauth2;
+import com.google.api.client.util.SslUtils;
+import java.security.GeneralSecurityException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
@@ -104,7 +108,21 @@ enum Pkcs8Algorithm {
public static final String CLOUD_PLATFORM_SCOPE =
"https://www.googleapis.com/auth/cloud-platform";
- static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
+ private static final Logger logger = Logger.getLogger(OAuth2Utils.class.getName());
+
+ static final HttpTransport HTTP_TRANSPORT;
+ static {
+ HttpTransport transport;
+ try {
+ transport = new NetHttpTransport.Builder()
+ .setSslSocketFactory(SslUtils.getTlsSslContext().getSocketFactory())
+ .build();
+ } catch (GeneralSecurityException e) {
+ logger.log(Level.WARNING, "Failed to initialize PQC-hardened HTTP transport, falling back to default", e);
+ transport = new NetHttpTransport();
+ }
+ HTTP_TRANSPORT = transport;
+ }
public static final HttpTransportFactory HTTP_TRANSPORT_FACTORY =
new DefaultHttpTransportFactory();
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 26ad2cd570f1..1daa8c36b883 100644
--- a/sdk-platform-java/gapic-generator-java-pom-parent/pom.xml
+++ b/sdk-platform-java/gapic-generator-java-pom-parent/pom.xml
@@ -19,6 +19,7 @@
+ 1.80
false
java.header
8
@@ -27,7 +28,7 @@
consistent across modules in this repository -->
1.3.2
1.81.0
- 2.1.0
+ 2.1.1-SNAPSHOT
2.13.2
33.5.0-jre
4.33.2
diff --git a/sdk-platform-java/gax-java/gax-grpc/pom.xml b/sdk-platform-java/gax-java/gax-grpc/pom.xml
index 927518b32cf7..1299568b7016 100644
--- a/sdk-platform-java/gax-java/gax-grpc/pom.xml
+++ b/sdk-platform-java/gax-java/gax-grpc/pom.xml
@@ -99,6 +99,17 @@
true
+
+ org.bouncycastle
+ bcprov-jdk18on
+ ${bouncycastle.version}
+
+
+ org.bouncycastle
+ bctls-jdk18on
+ ${bouncycastle.version}
+
+
io.grpc
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..ad26f50b83d4 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
@@ -812,6 +812,9 @@ public ManagedChannelBuilder> createDecoratedChannelBuilder() throws IOExcepti
if (interceptorProvider != null) {
builder.intercept(interceptorProvider.getInterceptors());
}
+ // Apply PQC configuration by default as a standard feature of GAX.
+ builder = applyPqcConfiguration(builder);
+
if (channelConfigurator != null) {
builder = channelConfigurator.apply(builder);
}
@@ -819,6 +822,148 @@ public ManagedChannelBuilder> createDecoratedChannelBuilder() throws IOExcepti
return builder;
}
+ private static final class OpenSslReflectionHolder {
+ private static final Class> SHADED_GRPC_SSL_CONTEXTS;
+ private static final Class> SHADED_SSL_CONTEXT_BUILDER;
+ private static final java.lang.reflect.Method SHADED_FOR_CLIENT;
+ private static final Object SHADED_GROUPS_OPTION;
+ private static final java.lang.reflect.Method SHADED_OPTION_METHOD;
+ private static final java.lang.reflect.Method SHADED_BUILD_METHOD;
+ private static final java.lang.reflect.Method SHADED_SSL_CONTEXT_METHOD;
+ private static final Class> SHADED_SSL_CONTEXT;
+ private static final boolean SHADED_AVAILABLE;
+
+ private static final Class> UNSHADED_GRPC_SSL_CONTEXTS;
+ private static final Class> UNSHADED_SSL_CONTEXT_BUILDER;
+ private static final java.lang.reflect.Method UNSHADED_FOR_CLIENT;
+ private static final Object UNSHADED_GROUPS_OPTION;
+ private static final java.lang.reflect.Method UNSHADED_OPTION_METHOD;
+ private static final java.lang.reflect.Method UNSHADED_BUILD_METHOD;
+ private static final java.lang.reflect.Method UNSHADED_SSL_CONTEXT_METHOD;
+ private static final Class> UNSHADED_SSL_CONTEXT;
+ private static final boolean UNSHADED_AVAILABLE;
+
+ static {
+ // 1. Shaded Netty Lookups
+ Class> shadedGrpcSslCtx = null;
+ Class> shadedSslCtxBuilder = null;
+ java.lang.reflect.Method shadedForClient = null;
+ Object shadedGroupsOpt = null;
+ java.lang.reflect.Method shadedOption = null;
+ java.lang.reflect.Method shadedBuild = null;
+ java.lang.reflect.Method shadedSslCtxMethod = null;
+ Class> shadedSslCtx = null;
+ boolean shadedAvailable = false;
+ try {
+ String p = "io.grpc.netty.shaded.";
+ shadedGrpcSslCtx = Class.forName(p + "io.grpc.netty.GrpcSslContexts");
+ shadedSslCtxBuilder = Class.forName(p + "io.netty.handler.ssl.SslContextBuilder");
+ Class> openSslCtxOpt = Class.forName(p + "io.netty.handler.ssl.OpenSslContextOption");
+ Class> sslCtxOpt = Class.forName(p + "io.netty.handler.ssl.SslContextOption");
+ shadedSslCtx = Class.forName(p + "io.netty.handler.ssl.SslContext");
+
+ shadedForClient = shadedGrpcSslCtx.getMethod("forClient");
+ java.lang.reflect.Field groupsField = openSslCtxOpt.getDeclaredField("GROUPS");
+ shadedGroupsOpt = groupsField.get(null);
+ shadedOption = shadedSslCtxBuilder.getMethod("option", sslCtxOpt, Object.class);
+ shadedBuild = shadedSslCtxBuilder.getMethod("build");
+
+ Class> nettyBuilderClass = Class.forName(p + "io.grpc.netty.NettyChannelBuilder");
+ shadedSslCtxMethod = nettyBuilderClass.getMethod("sslContext", shadedSslCtx);
+
+ shadedAvailable = true;
+ } catch (Throwable t) {
+ // Ignore: Shaded Netty is not available
+ }
+ SHADED_GRPC_SSL_CONTEXTS = shadedGrpcSslCtx;
+ SHADED_SSL_CONTEXT_BUILDER = shadedSslCtxBuilder;
+ SHADED_FOR_CLIENT = shadedForClient;
+ SHADED_GROUPS_OPTION = shadedGroupsOpt;
+ SHADED_OPTION_METHOD = shadedOption;
+ SHADED_BUILD_METHOD = shadedBuild;
+ SHADED_SSL_CONTEXT_METHOD = shadedSslCtxMethod;
+ SHADED_SSL_CONTEXT = shadedSslCtx;
+ SHADED_AVAILABLE = shadedAvailable;
+
+ // 2. Unshaded Netty Lookups
+ Class> unshadedGrpcSslCtx = null;
+ Class> unshadedSslCtxBuilder = null;
+ java.lang.reflect.Method unshadedForClient = null;
+ Object unshadedGroupsOpt = null;
+ java.lang.reflect.Method unshadedOption = null;
+ java.lang.reflect.Method unshadedBuild = null;
+ java.lang.reflect.Method unshadedSslCtxMethod = null;
+ Class> unshadedSslCtx = null;
+ boolean unshadedAvailable = false;
+ try {
+ unshadedGrpcSslCtx = Class.forName("io.grpc.netty.GrpcSslContexts");
+ unshadedSslCtxBuilder = Class.forName("io.netty.handler.ssl.SslContextBuilder");
+ Class> openSslCtxOpt = Class.forName("io.netty.handler.ssl.OpenSslContextOption");
+ Class> sslCtxOpt = Class.forName("io.netty.handler.ssl.SslContextOption");
+ unshadedSslCtx = Class.forName("io.netty.handler.ssl.SslContext");
+
+ unshadedForClient = unshadedGrpcSslCtx.getMethod("forClient");
+ java.lang.reflect.Field groupsField = openSslCtxOpt.getDeclaredField("GROUPS");
+ unshadedGroupsOpt = groupsField.get(null);
+ unshadedOption = unshadedSslCtxBuilder.getMethod("option", sslCtxOpt, Object.class);
+ unshadedBuild = unshadedSslCtxBuilder.getMethod("build");
+
+ Class> nettyBuilderClass = Class.forName("io.grpc.netty.NettyChannelBuilder");
+ unshadedSslCtxMethod = nettyBuilderClass.getMethod("sslContext", unshadedSslCtx);
+
+ unshadedAvailable = true;
+ } catch (Throwable t) {
+ // Ignore: Unshaded Netty is not available
+ }
+ UNSHADED_GRPC_SSL_CONTEXTS = unshadedGrpcSslCtx;
+ UNSHADED_SSL_CONTEXT_BUILDER = unshadedSslCtxBuilder;
+ UNSHADED_FOR_CLIENT = unshadedForClient;
+ UNSHADED_GROUPS_OPTION = unshadedGroupsOpt;
+ UNSHADED_OPTION_METHOD = unshadedOption;
+ UNSHADED_BUILD_METHOD = unshadedBuild;
+ UNSHADED_SSL_CONTEXT_METHOD = unshadedSslCtxMethod;
+ UNSHADED_SSL_CONTEXT = unshadedSslCtx;
+ UNSHADED_AVAILABLE = unshadedAvailable;
+ }
+ }
+
+ private ManagedChannelBuilder> applyPqcConfiguration(ManagedChannelBuilder> builder) {
+ // Configure the PQ and classical hybrid named groups:
+ // 1. X25519MLKEM768 (codepoint 4588): Hybrid classical (X25519) + post-quantum (ML-KEM-768) key exchange.
+ // Provides defense-in-depth: if ML-KEM is compromised, security reverts to classical strength of X25519.
+ // 2. MLKEM768 (codepoint 1896): Pure post-quantum key exchange using ML-KEM-768.
+ // 3. X25519 (codepoint 29): Classical elliptic curve Diffie-Hellman key exchange, used as a fallback.
+ String[] hybridGroups = new String[] {"X25519MLKEM768", "MLKEM768", "X25519"};
+ String builderClassName = builder.getClass().getName();
+ boolean isShaded = "io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder".equals(builderClassName);
+ boolean isUnshaded = "io.grpc.netty.NettyChannelBuilder".equals(builderClassName);
+
+ if (isShaded && OpenSslReflectionHolder.SHADED_AVAILABLE) {
+ try {
+ Object sslContextBuilder = OpenSslReflectionHolder.SHADED_FOR_CLIENT.invoke(null);
+ OpenSslReflectionHolder.SHADED_OPTION_METHOD.invoke(
+ sslContextBuilder, OpenSslReflectionHolder.SHADED_GROUPS_OPTION, (Object) hybridGroups);
+ Object sslContext = OpenSslReflectionHolder.SHADED_BUILD_METHOD.invoke(sslContextBuilder);
+ OpenSslReflectionHolder.SHADED_SSL_CONTEXT_METHOD.invoke(builder, sslContext);
+ return builder;
+ } catch (java.lang.reflect.InvocationTargetException | IllegalAccessException | RuntimeException e) {
+ LOG.log(Level.WARNING, "Failed to configure shaded PQC transport fallback", e);
+ }
+ } else if (isUnshaded && OpenSslReflectionHolder.UNSHADED_AVAILABLE) {
+ try {
+ Object sslContextBuilder = OpenSslReflectionHolder.UNSHADED_FOR_CLIENT.invoke(null);
+ OpenSslReflectionHolder.UNSHADED_OPTION_METHOD.invoke(
+ sslContextBuilder, OpenSslReflectionHolder.UNSHADED_GROUPS_OPTION, (Object) hybridGroups);
+ Object sslContext = OpenSslReflectionHolder.UNSHADED_BUILD_METHOD.invoke(sslContextBuilder);
+ OpenSslReflectionHolder.UNSHADED_SSL_CONTEXT_METHOD.invoke(builder, sslContext);
+ return builder;
+ } catch (java.lang.reflect.InvocationTargetException | IllegalAccessException | RuntimeException e) {
+ LOG.log(Level.WARNING, "Failed to configure unshaded PQC transport fallback", e);
+ }
+ }
+ return builder;
+ }
+
private ManagedChannel createSingleChannel() throws IOException {
ManagedChannelBuilder> builder = createDecoratedChannelBuilder();
diff --git a/sdk-platform-java/gax-java/gax-httpjson/pom.xml b/sdk-platform-java/gax-java/gax-httpjson/pom.xml
index a7d38f523cc4..09b1539617c0 100644
--- a/sdk-platform-java/gax-java/gax-httpjson/pom.xml
+++ b/sdk-platform-java/gax-java/gax-httpjson/pom.xml
@@ -20,6 +20,17 @@
+
+ org.bouncycastle
+ bcprov-jdk18on
+ ${bouncycastle.version}
+
+
+ org.bouncycastle
+ bctls-jdk18on
+ ${bouncycastle.version}
+
+
com.google.api
gax
diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java
index daf94a498cc4..1afc1f20f2e4 100644
--- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java
+++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java
@@ -42,6 +42,8 @@
import com.google.auth.mtls.DefaultMtlsProviderFactory;
import com.google.auth.mtls.MtlsProvider;
import com.google.common.annotations.VisibleForTesting;
+import javax.net.ssl.SSLContext;
+import java.security.NoSuchAlgorithmException;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
@@ -186,6 +188,8 @@ public TransportChannelProvider withCredentials(Credentials credentials) {
HttpTransport createHttpTransport() throws IOException, GeneralSecurityException {
if (mtlsProvider == null) {
+ // Returning null allows ManagedHttpJsonChannel to instantiate a default NetHttpTransport,
+ // which is automatically PQC-hardened if Bouncy Castle JSSE is available on the classpath.
return null;
}
if (certificateBasedAccess.useMtlsClientCertificate()) {
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..7363433014d8
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pom.xml
@@ -0,0 +1,24 @@
+
+
+ 4.0.0
+
+
+ com.google.api
+ gapic-generator-java-pom-parent
+ 2.73.0-SNAPSHOT
+ ../gapic-generator-java-pom-parent
+
+
+ com.google.api
+ pqc-test-parent
+ pom
+ 2.81.0-SNAPSHOT
+
+
+ pqc-test-common
+ pqc-test-snapshot
+ pqc-test-release
+
+
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..f0956897e630
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-common/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+
+ com.google.api
+ pqc-test-parent
+ 2.81.0-SNAPSHOT
+ ../pom.xml
+
+
+ pqc-test-common
+
+
+
+ com.google.api
+ gax-httpjson
+ 2.81.0-SNAPSHOT
+
+
+ com.google.api
+ gax-grpc
+ 2.81.0-SNAPSHOT
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+ ${bouncycastle.version}
+
+
+ org.bouncycastle
+ bctls-jdk18on
+ ${bouncycastle.version}
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.10.2
+
+
+ io.grpc
+ grpc-netty
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-stub
+ ${grpc.version}
+
+
+ com.google.cloud
+ google-cloud-bigquery
+ 2.67.0-SNAPSHOT
+ provided
+
+
+
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..30534091a559
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/httpjson/PqcConnectivityTest.java
@@ -0,0 +1,238 @@
+package com.google.api.gax.httpjson;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.gax.pqc.PqcTestServer;
+import io.grpc.ManagedChannel;
+import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
+import java.io.InputStream;
+import java.net.URL;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import java.security.Security;
+
+import com.google.cloud.bigquery.BigQuery;
+import com.google.cloud.bigquery.BigQueryOptions;
+import com.google.cloud.NoCredentials;
+import com.google.cloud.TransportOptions;
+import com.google.cloud.http.HttpTransportOptions;
+import com.google.auth.http.HttpTransportFactory;
+
+/**
+ * PqcConnectivityTest serves as the base class for validating Post-Quantum Cryptography (PQC)
+ * connectivity in the Google Cloud Java SDK.
+ */
+public class PqcConnectivityTest {
+
+ private static PqcTestServer server;
+
+ @BeforeAll
+ public static void setup() throws Exception {
+ System.setProperty("javax.net.debug", "all");
+
+ // NOTE: Enforcing MLKEM768 globally via system property is strictly isolated to this test JVM execution.
+ // This ensures that the SunJSSE engine (used by old released libraries when pqc.enable is false)
+ // attempts to negotiate MLKEM768. Since SunJSSE does not implement MLKEM768, it immediately
+ // aborts the handshake with a handshake_failure, allowing us to confirm that older client libraries
+ // cleanly fail-fast as expected, validating the integration test negative assertions.
+ System.setProperty("jdk.tls.namedGroups", "MLKEM768");
+
+ Security.addProvider(new BouncyCastleProvider());
+ if (Boolean.getBoolean("pqc.enable")) {
+ Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
+ } else {
+ Security.addProvider(new BouncyCastleJsseProvider());
+ }
+
+ server = new PqcTestServer();
+ server.start();
+ }
+
+ @AfterAll
+ public static void teardown() {
+ if (server != null) {
+ server.stop();
+ }
+ Security.removeProvider("BCJSSE");
+ Security.removeProvider("BC");
+ }
+
+ public void runTests() throws Exception {
+ testHttpPqc();
+ testGrpcPqc();
+ testBigQueryPqc();
+ }
+
+ @Test
+ public void testHttpPqc() throws Exception {
+ java.security.KeyStore ks = java.security.KeyStore.getInstance("PKCS12");
+ ks.load(PqcTestServer.class.getResourceAsStream("/pqctest.p12"), "password".toCharArray());
+
+ javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance(javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(ks);
+
+ // Build a custom HttpTransport explicitly trusting the self-signed certificate keystore.
+ com.google.api.client.http.HttpTransport httpTransport = new com.google.api.client.http.javanet.NetHttpTransport.Builder()
+ .trustCertificates(ks)
+ .build();
+
+ // Pass the pre-configured httpTransport to the InstantiatingHttpJsonChannelProvider.
+ InstantiatingHttpJsonChannelProvider provider = InstantiatingHttpJsonChannelProvider.newBuilder()
+ .setEndpoint("localhost:" + server.getHttpPort())
+ .setHeaderProvider(() -> java.util.Collections.emptyMap())
+ .setHttpTransport(httpTransport)
+ .build();
+
+ HttpJsonTransportChannel transportChannel = provider.getTransportChannel();
+ ManagedHttpJsonChannel managedChannel = transportChannel.getManagedChannel();
+
+ while (managedChannel instanceof ManagedHttpJsonInterceptorChannel) {
+ managedChannel = ((ManagedHttpJsonInterceptorChannel) managedChannel).getChannel();
+ }
+
+ java.lang.reflect.Field field = ManagedHttpJsonChannel.class.getDeclaredField("httpTransport");
+ field.setAccessible(true);
+ com.google.api.client.http.HttpTransport transportFromChannel = (com.google.api.client.http.HttpTransport) field.get(managedChannel);
+ com.google.api.client.http.HttpRequest request = transportFromChannel.createRequestFactory().buildGetRequest(
+ new com.google.api.client.http.GenericUrl("https://localhost:" + server.getHttpPort() + "/test"));
+
+ HttpResponse response = request.execute();
+ assertEquals(200, response.getStatusCode());
+ String content = response.parseAsString();
+ assertEquals("PQC HTTP OK", content.trim());
+ }
+
+ @Test
+ public void testGrpcPqc() throws Exception {
+ io.grpc.MethodDescriptor method = io.grpc.MethodDescriptor.newBuilder()
+ .setType(io.grpc.MethodDescriptor.MethodType.UNARY)
+ .setFullMethodName("Greeter/SayHello")
+ .setRequestMarshaller(new ByteMarshaller())
+ .setResponseMarshaller(new ByteMarshaller())
+ .build();
+
+ InstantiatingGrpcChannelProvider.Builder providerBuilder = InstantiatingGrpcChannelProvider.newBuilder()
+ .setEndpoint("localhost:" + server.getGrpcPort())
+ .setHeaderProvider(() -> java.util.Collections.emptyMap());
+
+ if (Boolean.getBoolean("pqc.enable")) {
+ providerBuilder.setChannelConfigurator(new com.google.api.core.ApiFunction() {
+ @Override
+ public io.grpc.ManagedChannelBuilder apply(io.grpc.ManagedChannelBuilder builder) {
+ builder.overrideAuthority("localhost");
+
+ // Using reflection for the test since grpc-netty-shaded is runtime in gax-grpc compilation context,
+ // but we can configure it dynamically using SslContextBuilder's sslContextProvider.
+ String builderClassName = builder.getClass().getName();
+ if ("io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder".equals(builderClassName)) {
+ try {
+ // Reflectively configure shaded Netty using Bouncy Castle JJSSE
+ Class> sslContextBuilderClass = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder");
+ Class> sslProviderEnum = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.SslProvider");
+ Object sslProviderJdk = Enum.valueOf((Class) sslProviderEnum, "JDK");
+
+ Class> apnClass = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig");
+ Class> protocolEnum = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig$Protocol");
+ Object alpnProtocol = Enum.valueOf((Class) protocolEnum, "ALPN");
+ Class> selectorBehaviorEnum = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig$SelectorFailureBehavior");
+ Object noAdvertiseBehavior = Enum.valueOf((Class) selectorBehaviorEnum, "NO_ADVERTISE");
+ Class> listenerBehaviorEnum = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig$SelectedListenerFailureBehavior");
+ Object acceptBehavior = Enum.valueOf((Class) listenerBehaviorEnum, "ACCEPT");
+
+ java.lang.reflect.Constructor> apnConstructor = apnClass.getConstructor(
+ protocolEnum, selectorBehaviorEnum, listenerBehaviorEnum, String[].class
+ );
+ Object apn = apnConstructor.newInstance(alpnProtocol, noAdvertiseBehavior, acceptBehavior, new String[]{"h2"});
+
+ Class> tmFactoryClass = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory");
+ Object tmFactoryInstance = tmFactoryClass.getField("INSTANCE").get(null);
+
+ java.lang.reflect.Method forClientMethod = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder").getMethod("forClient");
+ Object scBuilder = forClientMethod.invoke(null);
+
+ // Configure SslContextBuilder
+ scBuilder.getClass().getMethod("sslProvider", sslProviderEnum).invoke(scBuilder, sslProviderJdk);
+ scBuilder.getClass().getMethod("sslContextProvider", java.security.Provider.class).invoke(scBuilder, new BouncyCastleJsseProvider());
+ scBuilder.getClass().getMethod("protocols", String[].class).invoke(scBuilder, (Object) new String[]{"TLSv1.3"});
+ scBuilder.getClass().getMethod("applicationProtocolConfig", apnClass).invoke(scBuilder, apn);
+ scBuilder.getClass().getMethod("trustManager", javax.net.ssl.TrustManagerFactory.class).invoke(scBuilder, tmFactoryInstance);
+
+ Object shadedSslContext = scBuilder.getClass().getMethod("build").invoke(scBuilder);
+
+ Class> sslContextClass = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.SslContext");
+ builder.getClass().getMethod("sslContext", sslContextClass).invoke(builder, shadedSslContext);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return builder;
+ }
+ });
+ }
+
+ InstantiatingGrpcChannelProvider provider = providerBuilder.build();
+
+ io.grpc.Channel channel = ((com.google.api.gax.grpc.GrpcTransportChannel) provider.getTransportChannel()).getChannel();
+
+ byte[] response = io.grpc.stub.ClientCalls.blockingUnaryCall(
+ channel, method, io.grpc.CallOptions.DEFAULT, "Hello".getBytes());
+
+ assertEquals("PQC gRPC OK", new String(response).trim());
+ ((io.grpc.ManagedChannel) channel).shutdown();
+ }
+
+ @Test
+ public void testBigQueryPqc() throws Exception {
+ java.security.KeyStore ks = java.security.KeyStore.getInstance("PKCS12");
+ ks.load(PqcTestServer.class.getResourceAsStream("/pqctest.p12"), "password".toCharArray());
+
+ // Build a custom HttpTransport explicitly trusting the self-signed certificate keystore.
+ final com.google.api.client.http.HttpTransport httpTransport = new com.google.api.client.http.javanet.NetHttpTransport.Builder()
+ .trustCertificates(ks)
+ .build();
+
+ TransportOptions transportOptions = HttpTransportOptions.newBuilder()
+ .setHttpTransportFactory(new HttpTransportFactory() {
+ @Override
+ public com.google.api.client.http.HttpTransport create() {
+ return httpTransport;
+ }
+ })
+ .build();
+
+ BigQueryOptions bigqueryOptions = BigQueryOptions.newBuilder()
+ .setProjectId("test-project")
+ .setHost("https://localhost:" + server.getHttpPort())
+ .setCredentials(NoCredentials.getInstance())
+ .setTransportOptions(transportOptions)
+ .build();
+
+ BigQuery bigquery = bigqueryOptions.getService();
+
+ // This will trigger a request to https://localhost:httpPort/bigquery/v2/projects/test-project/datasets
+ // Handshake must succeed. If it fails, it throws SSLHandshakeException.
+ bigquery.listDatasets();
+ }
+
+ 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..9cd7a14521cb
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/pqc/PqcTestServer.java
@@ -0,0 +1,150 @@
+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 java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.security.KeyStore;
+import java.security.Security;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+/**
+ * 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 httpServer;
+ private Server grpcServer;
+ private int httpPort;
+ private int grpcPort;
+
+ public void start() throws Exception {
+ // 1. BouncyCastleProvider (JCA provider, name "BC"): Implements low-level cryptographic algorithms
+ // like signature generation, hashing, key agreement, and ML-KEM key representations.
+ Security.addProvider(new BouncyCastleProvider());
+
+ // 2. BouncyCastleJsseProvider (JSSE provider, name "BCJSSE"): Implements high-level TLS protocol support
+ // (TLSv1.3 engines, cipher suites, extensions, and socket factories). It depends on the JCA provider.
+ Security.addProvider(new BouncyCastleJsseProvider());
+
+ // Set system property to strictly enforce ML-KEM hybrid named group on the server.
+ // NOTE: This system property is set strictly inside test harness setup.
+ // Since this server class is only compiled and executed inside integration test contexts,
+ // it has zero impact on production runtimes (which never load or execute this class).
+ System.setProperty("jdk.tls.namedGroups", "MLKEM768");
+
+ // PKCS12 is the key store format to bundle the private key + the certificate.
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ try (InputStream is = getClass().getResourceAsStream("/pqctest.p12")) {
+ if (is == null) {
+ throw new RuntimeException("pqctest.p12 not found in classpath");
+ }
+ // Load the key with a dummy password
+ ks.load(is, "password".toCharArray());
+ }
+
+ // Key manager factory used to choose credentials for the TLS handshake.
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(ks, "password".toCharArray());
+
+ // Trust manager factory used to decide whether a client should be trusted.
+ javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance(javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(ks);
+
+ // 1. Start HTTP Server utilizing Bouncy Castle JJSSE
+ BouncyCastleJsseProvider bcProvider = new BouncyCastleJsseProvider();
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.3", bcProvider);
+ sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
+
+ httpServer = HttpsServer.create(new InetSocketAddress(0), 0);
+ httpServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
+ @Override
+ public void configure(HttpsParameters params) {
+ SSLParameters sslparams = getSSLContext().getDefaultSSLParameters();
+ // Enforce TLSv1.3 protocol
+ sslparams.setProtocols(new String[]{"TLSv1.3"});
+ params.setSSLParameters(sslparams);
+ }
+ });
+ httpServer.createContext("/test", exchange -> {
+ String response = "PQC HTTP OK";
+ exchange.sendResponseHeaders(200, response.length());
+ exchange.getResponseBody().write(response.getBytes());
+ exchange.getResponseBody().close();
+ });
+ httpServer.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();
+ });
+ httpServer.start();
+ httpPort = httpServer.getAddress().getPort();
+
+ // 2. Start gRPC Server using JDK SSL Provider bound specifically to Bouncy Castle JJSSE
+ io.netty.handler.ssl.SslContext nettySslContext = io.grpc.netty.GrpcSslContexts.configure(
+ io.netty.handler.ssl.SslContextBuilder.forServer(kmf)
+ .sslContextProvider(bcProvider), // Bind Netty statically to BC JJSSE!
+ io.netty.handler.ssl.SslProvider.JDK
+ )
+ .protocols("TLSv1.3") // Enforce TLSv1.3
+ .build();
+
+ 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();
+
+ grpcServer = NettyServerBuilder.forPort(0)
+ .sslContext(nettySslContext)
+ .addService(serviceDef)
+ .build()
+ .start();
+ grpcPort = grpcServer.getPort();
+ }
+
+ public void stop() {
+ if (httpServer != null) httpServer.stop(0);
+ if (grpcServer != null) grpcServer.shutdown();
+ // Remove BC JCA and JSSE providers on stop
+ Security.removeProvider("BCJSSE");
+ Security.removeProvider("BC");
+ }
+
+ public int getHttpPort() { return httpPort; }
+ public int getGrpcPort() { return grpcPort; }
+
+ 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/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..e9629cdd7e25
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml
@@ -0,0 +1,84 @@
+
+
+ 4.0.0
+
+
+ com.google.api
+ pqc-test-parent
+ 2.81.0-SNAPSHOT
+ ../pom.xml
+
+
+ pqc-test-release
+
+
+
+ com.google.api
+ pqc-test-common
+ 2.81.0-SNAPSHOT
+
+
+ com.google.api
+ gax-httpjson
+
+
+ com.google.api
+ gax-grpc
+
+
+ com.google.cloud
+ google-cloud-bigquery
+
+
+
+
+ com.google.cloud
+ google-cloud-bigquery
+ 2.66.0
+
+
+ com.google.api
+ gax-httpjson
+ 2.80.0
+
+
+ com.google.api
+ gax-grpc
+ 2.80.0
+
+
+ com.google.auth
+ google-auth-library-oauth2-http
+ 1.47.0
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.10.2
+ test
+
+
+ io.grpc
+ grpc-netty-shaded
+ ${grpc.version}
+ runtime
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ false
+
+
+
+
+
+
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..ecceab971251
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-release/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java
@@ -0,0 +1,5 @@
+package com.google.api.gax.httpjson;
+
+public class RunPqcTest extends PqcConnectivityTest {
+ // Inherits all @Test methods from PqcConnectivityTest to run in this module classpath context.
+}
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..22770277caa2
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 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
+
+
+ io.grpc
+ grpc-netty-shaded
+ ${grpc.version}
+ runtime
+
+
+ com.google.cloud
+ google-cloud-bigquery
+ 2.67.0-SNAPSHOT
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ true
+
+
+
+
+
+
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..ecceab971251
--- /dev/null
+++ b/sdk-platform-java/pqc-test/pqc-test-snapshot/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java
@@ -0,0 +1,5 @@
+package com.google.api.gax.httpjson;
+
+public class RunPqcTest extends PqcConnectivityTest {
+ // Inherits all @Test methods from PqcConnectivityTest to run in this module classpath context.
+}