From 64be800b0f2ae3e7f310c868ace562c1b18b81d2 Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 14 May 2026 17:26:36 -0400 Subject: [PATCH 01/21] chore: [wip] PQC POC 2 TAG=agy CONV=0ade5891-3c8d-4e27-a240-b1a8cd6a0b0c --- .github/workflows/pqc-tests.yml | 65 ++++++ .../com/google/auth/oauth2/OAuth2Utils.java | 13 +- .../gapic-generator-java-pom-parent/pom.xml | 3 +- sdk-platform-java/gax-java/gax-grpc/pom.xml | 11 + .../InstantiatingGrpcChannelProvider.java | 77 +++++++ .../gax-java/gax-httpjson/pom.xml | 11 + .../InstantiatingHttpJsonChannelProvider.java | 24 ++- sdk-platform-java/pom.xml | 1 + sdk-platform-java/pqc-test/pom.xml | 24 +++ .../pqc-test/pqc-test-common/pom.xml | 53 +++++ .../api/gax/httpjson/PqcConnectivityTest.java | 199 ++++++++++++++++++ .../com/google/api/gax/pqc/PqcTestServer.java | 142 +++++++++++++ .../src/main/resources/pqctest.p12 | Bin 0 -> 2618 bytes .../pqc-test/pqc-test-release/pom.xml | 75 +++++++ .../google/api/gax/httpjson/RunPqcTest.java | 5 + .../pqc-test/pqc-test-snapshot/pom.xml | 50 +++++ .../google/api/gax/httpjson/RunPqcTest.java | 5 + 17 files changed, 750 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/pqc-tests.yml create mode 100644 sdk-platform-java/pqc-test/pom.xml create mode 100644 sdk-platform-java/pqc-test/pqc-test-common/pom.xml create mode 100644 sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/httpjson/PqcConnectivityTest.java create mode 100644 sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/pqc/PqcTestServer.java create mode 100644 sdk-platform-java/pqc-test/pqc-test-common/src/main/resources/pqctest.p12 create mode 100644 sdk-platform-java/pqc-test/pqc-test-release/pom.xml create mode 100644 sdk-platform-java/pqc-test/pqc-test-release/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java create mode 100644 sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml create mode 100644 sdk-platform-java/pqc-test/pqc-test-snapshot/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java diff --git a/.github/workflows/pqc-tests.yml b/.github/workflows/pqc-tests.yml new file mode 100644 index 000000000000..94f2e3344da3 --- /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: + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' + + # 1. Checkout sibling HTTP Client repository (MUST point to your modified fork/branch containing PQC JJSSE fixes) + - name: Checkout google-http-java-client + uses: actions/checkout@v4 + with: + repository: /google-http-java-client # UPDATE with your fork + ref: # UPDATE with your branch containing PQC JJSSE fixes + path: google-http-java-client + + # 2. 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 + + # 3. Checkout this monorepo + - name: Checkout google-cloud-java-pqc + uses: actions/checkout@v4 + with: + path: google-cloud-java-pqc + + # 4. 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 + + # 5. 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 + + # 6. 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..4f9118a732e8 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,8 @@ package com.google.auth.oauth2; +import com.google.api.client.util.SslUtils; +import java.security.GeneralSecurityException; import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; @@ -104,7 +106,16 @@ enum Pkcs8Algorithm { public static final String CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; - static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); + static final HttpTransport HTTP_TRANSPORT; + static { + try { + HTTP_TRANSPORT = new NetHttpTransport.Builder() + .setSslSocketFactory(SslUtils.getTlsSslContext().getSocketFactory()) + .build(); + } catch (GeneralSecurityException e) { + throw new RuntimeException("Failed to initialize PQC-hardened HTTP transport", e); + } + } 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..0c82f8728822 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,80 @@ public ManagedChannelBuilder createDecoratedChannelBuilder() throws IOExcepti return builder; } + 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 || isUnshaded) { + try { + Object sslContext = buildOpenSslContext(isShaded, hybridGroups); + if (sslContext != null) { + setSslContextOnBuilder(builder, sslContext, isShaded); + return builder; + } + } catch (Exception e) { + // Graceful degradation: do not modify any global JVM property + } + } + return builder; + } + + /** + * Dynamically configures and builds an OpenSsl SslContext targeting post-quantum groups. + * + *

Rationale for Reflection: + * In the gax-grpc module, we maintain dual compatibility with both shaded Netty + * (io.grpc.netty.shaded) and unshaded Netty (io.grpc.netty) channel builders. Shaded Netty is + * a runtime dependency of gax-grpc rather than a compile-time dependency to prevent class + * path pollution. + * + *

By utilizing reflection here, we can check the runtime class type of the channel builder + * and dynamically resolve and configure the corresponding shaded or unshaded SslContextBuilder + * and OpenSslContextOption classes without requiring compile-time dependencies on shaded Netty. + * + * @param isShaded True if using shaded Netty, false if unshaded. + * @param groups Preference list of TLS named groups. + * @return Configured SslContext object. + */ + @SuppressWarnings("unchecked") + private Object buildOpenSslContext(boolean isShaded, String[] groups) throws Exception { + String prefix = isShaded ? "io.grpc.netty.shaded." : ""; + Class grpcSslContextsClass = Class.forName(prefix + "io.grpc.netty.GrpcSslContexts"); + Class sslContextBuilderClass = Class.forName(prefix + "io.netty.handler.ssl.SslContextBuilder"); + Class openSslContextOptionClass = Class.forName(prefix + "io.netty.handler.ssl.OpenSslContextOption"); + Class sslContextOptionClass = Class.forName(prefix + "io.netty.handler.ssl.SslContextOption"); + + // GrpcSslContexts.forClient() -> returns SslContextBuilder + java.lang.reflect.Method forClientMethod = grpcSslContextsClass.getMethod("forClient"); + Object sslContextBuilder = forClientMethod.invoke(null); + + // OpenSslContextOption.GROUPS + java.lang.reflect.Field groupsField = openSslContextOptionClass.getDeclaredField("GROUPS"); + Object groupsOption = groupsField.get(null); + + // SslContextBuilder.option(SslContextOption, Object) + java.lang.reflect.Method optionMethod = sslContextBuilderClass.getMethod("option", sslContextOptionClass, Object.class); + optionMethod.invoke(sslContextBuilder, groupsOption, groups); + + // SslContextBuilder.build() -> returns SslContext + java.lang.reflect.Method buildMethod = sslContextBuilderClass.getMethod("build"); + return buildMethod.invoke(sslContextBuilder); + } + + private void setSslContextOnBuilder(Object builder, Object sslContext, boolean isShaded) throws Exception { + String prefix = isShaded ? "io.grpc.netty.shaded." : ""; + Class sslContextClass = Class.forName(prefix + "io.netty.handler.ssl.SslContext"); + java.lang.reflect.Method sslContextMethod = builder.getClass().getMethod("sslContext", sslContextClass); + sslContextMethod.invoke(builder, sslContext); + } + 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..feab3e3dbe99 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; @@ -185,16 +187,26 @@ public TransportChannelProvider withCredentials(Credentials credentials) { } HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { - if (mtlsProvider == null) { - return null; - } - if (certificateBasedAccess.useMtlsClientCertificate()) { + // 1. Get the scope-specific PQC-hardened SSLContext utilizing Bouncy Castle. + SSLContext sslContext = com.google.api.client.util.SslUtils.getTlsSslContext(); + + // 2. Initialize the NetHttpTransport builder pre-configured with our PQC SSL context. + NetHttpTransport.Builder builder = new NetHttpTransport.Builder() + .setSslSocketFactory(sslContext.getSocketFactory()); + + // 3. Verify if mTLS is supported and explicitly requested in the current client session. + if (mtlsProvider != null && certificateBasedAccess.useMtlsClientCertificate()) { + // 4. Retrieve the mutual TLS client key store from the session-specific mtlsProvider. KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); + // 5. Ensure key store is valid before configuring mutual TLS client certificates. if (mtlsKeyStore != null) { - return new NetHttpTransport.Builder().trustCertificates(null, mtlsKeyStore, "").build(); + // 6. Configure the mutual TLS certificates while preserving the PQC SSL context. + builder.trustCertificates(null, mtlsKeyStore, ""); } } - return null; + + // 7. Return the compiled and PQC-hardened NetHttpTransport instance. + return builder.build(); } private HttpJsonTransportChannel createChannel() throws IOException, GeneralSecurityException { 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..f6c549682913 --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-common/pom.xml @@ -0,0 +1,53 @@ + + + 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} + + + 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..5178b91071e1 --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/httpjson/PqcConnectivityTest.java @@ -0,0 +1,199 @@ +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; + +/** + * 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(); + } + } + + public void runTests() throws Exception { + testHttpPqc(); + testGrpcPqc(); + } + + @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); + Class trustManagerFactoryClass = Class.forName("javax.net.ssl.TrustManagerFactory"); + java.lang.reflect.Method getTrustManagersMethod = tmFactoryClass.getMethod("getTrustManagers"); + // wait, insecure TM factory has getTrustManagers? Actually it inherits from SimpleTrustManagerFactory which has getTrustManagers? No, javax.net.ssl.TrustManagerFactory has getTrustManagers() + // Netty's InsecureTrustManagerFactory extends SimpleTrustManagerFactory. We can just pass the TrustManagerFactory itself to SslContextBuilder.trustManager(TrustManagerFactory) + + 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(); + } + + 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..8b7c9a991513 --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/pqc/PqcTestServer.java @@ -0,0 +1,142 @@ +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.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 provider on stop + 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 0000000000000000000000000000000000000000..92c74c66d3f06b874c28d388cb06c3260eaa3f3d GIT binary patch literal 2618 zcma);X*d*$8pmhFFvB=G*1-_jnITK4Yz-!3HzvC**<}kU%V2DcCD{^EFNVyn1_FXHWY}*kFjH}B!mnb{~?cH$WYpUSf8cELP3b~7e@o`>MnFw4|5Pm3JC(DF>n^P|6ByYpa6_83(SRL1oQwy zfU*$eZ4*sGb2I4h?s)zw4m;HeAw&A^pXgRzl`KK;S3P8J2wvY`K+O~&lZ=E=nU`Tn|MO*Oiu*wxow_5OuMeHy)K|>iVXiLn&IJ)!z&VrxWC;=%Xq zS`)Sx2^NlH&>Hi+O48Z<@&ATs&ORN!bW$L`^)9~SwUal(IXOCX zmz#K8J@_Q4ZSKY!#>U8euz&v7Bxw+MXB>TM{|rFd!jIY;$%wq(mgZxJduLg7tk)0U zI-kE)Sa&8HY@2H=1US;@@Y8+P^~XtdNX%hy^p>#YPkwzSRR&?E+=*4X4)Q>EJ&?8{ z({n;1n>G<_*#F5S<7t8A3pGQ+lIU!JNVM^@9K6znB4e+$2%KHT*ewL9%u6z%v1;8X zQ_G=y8oe^07$>#3;g;lpKL(Fk7mgHnWiWRoHW={_22+E_4xj8YTzT5fDqJOwR`~b2 zK*DDD0=ewmxQuUW=(&ryyXhKbTi2^mx&ny;s`3YyF4Ndtqj>T&=ya-Q3mSJLuZ)d< zWJBeWW?$y7Cfq^ZdN)93FU z^6fy+h9i3;SZvH{j@zPrf6fezY#DqF$%;kyM|FF$sAtQ~Mr*FiqdwfBR@iIvIXq+W z7QbjPT)9G5*qw_c-nD&EaIS0TWWHAB_XrIR;cObutEvEw3py%v6t#76&z7AFE%a@u zoU)LVsd_ONZc)WS8ur~O(!^Fgdl`9bYMhlGK``ItXdRT@ zC@Hvx4+I7QG6LMMvM~D$rGo~~es|J~DP8ZJp;uSxd!`=6?((0hd2@uP^%NiFOETaT}RPl?|<{Pp)fPULofF~aLkU9Le*CM<_^|4 z9%5YjHu&ZCCv1h70d*?elE73LNYJ(#Y#ZoPnHk=l6<6;v)5UckmgHQ6FG4Ya@1Nrx#6N%Nw7?`g{y3m4{b> zk?oox&I`<*R1SWn57X`aYs2BNVuXvidOJLQ{D4Ygt76=eEX@^nd`9NjO;uq{Yg{XQ z8lX>0%jT_w=EN{mVuBV|C*{{U?DlSQf26W}E_RF4a?>+JDT1#GbW(BD&TxQL8&R;X z{z_A@TeDyll#efPQ6a_rjmf(SwpGtPX>|Sj45uj(E|lPRfQjTHd_QO|)G@x)Gwv+9 zWs*Iy`SobLoyDH+=CQ>&cK@U8p6kVf_aXN?Dk(E%gLi`m)ZyA+4<r6R)VEm#T7+UH;OI{NU)p{gKDSI}^KKZonUD#AD|Jy$H9z zpR{%77CDY5f1A3P4dZV0<#tO8FICkUXd`i$rY<`f5tZzC3grh@cLm25JCYq9b#wiJ zjY(Q)wXQKsFuyxW^6f09S@d5mEM^m(8%^xE*IN@>xGuk?{IGeHS#NE4K3?aO-T$oWYO=`a=370U{cKqdRs$Xa9yQ#d9lfX8c z{3wu6!uU;tWKIJ5ol)c1npk>7adYCBu~qIg5mieJ?YDIvUI!U1T*56&InPtF7zjDh zX^+h*k!yL8tDg%#>h+-&qy%sQyw?9ShiWOX@LAi!_Z;A@>1J%U7?aw>Mc)!3Rf zgerUBxak2o+fMYJW10AeR$pr6xR_52F55&k`unzLgpDjHu@?G$dRy`(`5<1{X6M6b zz;l^8%KbhXOw}L) z9@hs-lt`%QZc%G>hD^k}^j8vajOnEy`xz;WP|>&~o~xvPhW!ZDn;D0W6X}0r51FW* zKQkE=FPi^+@#kw8HoVhdqll~Tu!5{-P1DsJQ>)V^7%7UiT@vzb_R8L)zS7Gc>)BFf zO=F23yPO}naEqUxLt|}w7#JsIi_gkT5*^pbc%wY_x)_|&zbGs-9onO#th%32+VLqO zqk^Qss%G&v(oNY$nNw~hjHO&QzV*Y7;DwKhrOlvO3;~11ApZWRKtLz}f?n_MVUpex zM_+iQ$TSKkuK`|G`XAh{cNgEmY>Wj^+#x*pVy~OL3b|w0HftFQg_v?jLazQ#B>WrL C?xKbO literal 0 HcmV?d00001 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..7d79c9ba7bb8 --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml @@ -0,0 +1,75 @@ + + + 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.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..45c979470158 --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml @@ -0,0 +1,50 @@ + + + 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 + + + + + + + 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. +} From 7c915c742c6eff4227d7c4f0b25560f275bb0b59 Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 14 May 2026 17:37:59 -0400 Subject: [PATCH 02/21] chore: update CI workflow to use googleapis/google-http-java-client branch chore/pqc-poc-2 TAG=agy CONV=0ade5891-3c8d-4e27-a240-b1a8cd6a0b0c --- .github/workflows/pqc-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pqc-tests.yml b/.github/workflows/pqc-tests.yml index 94f2e3344da3..3fced438f1a8 100644 --- a/.github/workflows/pqc-tests.yml +++ b/.github/workflows/pqc-tests.yml @@ -18,12 +18,12 @@ jobs: distribution: 'temurin' cache: 'maven' - # 1. Checkout sibling HTTP Client repository (MUST point to your modified fork/branch containing PQC JJSSE fixes) + # 1. Checkout sibling HTTP Client repository - name: Checkout google-http-java-client uses: actions/checkout@v4 with: - repository: /google-http-java-client # UPDATE with your fork - ref: # UPDATE with your branch containing PQC JJSSE fixes + repository: googleapis/google-http-java-client + ref: chore/pqc-poc-2 path: google-http-java-client # 2. Build and install modified google-http-client SNAPSHOT locally From f0478aec4c2fed3794637fdf9ba41a82aeb09695 Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 14 May 2026 17:40:56 -0400 Subject: [PATCH 03/21] chore: fix CI workflow setup order and step numbers TAG=agy CONV=0ade5891-3c8d-4e27-a240-b1a8cd6a0b0c --- .github/workflows/pqc-tests.yml | 36 +++++++++++++++++---------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/.github/workflows/pqc-tests.yml b/.github/workflows/pqc-tests.yml index 3fced438f1a8..6fdcac705332 100644 --- a/.github/workflows/pqc-tests.yml +++ b/.github/workflows/pqc-tests.yml @@ -11,13 +11,6 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: 'maven' - # 1. Checkout sibling HTTP Client repository - name: Checkout google-http-java-client uses: actions/checkout@v4 @@ -26,31 +19,40 @@ jobs: ref: chore/pqc-poc-2 path: google-http-java-client - # 2. 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 - - # 3. Checkout this monorepo + # 2. Checkout this monorepo - name: Checkout google-cloud-java-pqc uses: actions/checkout@v4 with: path: google-cloud-java-pqc - # 4. Build the entire monorepo core components required by the tests + # 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 - # 5. Run Snapshot PQC Tests (EXPECT PASS) + # 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 - # 6. Run Release PQC Tests (EXPECT FAIL) + # 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). From 408496fc7ba62911ee71a21febe5bc2847e2878d Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 19 May 2026 13:12:06 -0400 Subject: [PATCH 04/21] chore: address Gemini review comments for PQC POC This commit addresses the review feedback by implementing: 1. Graceful fallback to default NetHttpTransport with WARNING logging in OAuth2Utils static initializer. 2. warning-level exception logging in InstantiatingGrpcChannelProvider. 3. Caching of shaded/unshaded gRPC Netty OpenSSL reflection lookups inside thread-safe static OpenSslReflectionHolder to remove runtime overhead. 4. Reverting createHttpTransport in InstantiatingHttpJsonChannelProvider to return null when mTLS is not active, as default transport is already PQC-hardened. 5. Clean unregistration of BC and BCJSSE security providers in integration test server/client teardown. TAG=agy --- .../com/google/auth/oauth2/OAuth2Utils.java | 11 +- .../InstantiatingGrpcChannelProvider.java | 182 ++++++++++++------ .../InstantiatingHttpJsonChannelProvider.java | 24 +-- .../api/gax/httpjson/PqcConnectivityTest.java | 8 +- .../com/google/api/gax/pqc/PqcTestServer.java | 3 +- 5 files changed, 147 insertions(+), 81 deletions(-) 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 4f9118a732e8..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 @@ -33,6 +33,8 @@ 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; @@ -106,15 +108,20 @@ enum Pkcs8Algorithm { public static final String CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; + private static final Logger logger = Logger.getLogger(OAuth2Utils.class.getName()); + static final HttpTransport HTTP_TRANSPORT; static { + HttpTransport transport; try { - HTTP_TRANSPORT = new NetHttpTransport.Builder() + transport = new NetHttpTransport.Builder() .setSslSocketFactory(SslUtils.getTlsSslContext().getSocketFactory()) .build(); } catch (GeneralSecurityException e) { - throw new RuntimeException("Failed to initialize PQC-hardened HTTP transport", 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 = 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 0c82f8728822..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 @@ -822,6 +822,111 @@ 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. @@ -833,69 +938,32 @@ private ManagedChannelBuilder applyPqcConfiguration(ManagedChannelBuilder boolean isShaded = "io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder".equals(builderClassName); boolean isUnshaded = "io.grpc.netty.NettyChannelBuilder".equals(builderClassName); - if (isShaded || isUnshaded) { + if (isShaded && OpenSslReflectionHolder.SHADED_AVAILABLE) { try { - Object sslContext = buildOpenSslContext(isShaded, hybridGroups); - if (sslContext != null) { - setSslContextOnBuilder(builder, sslContext, isShaded); - return builder; - } - } catch (Exception e) { - // Graceful degradation: do not modify any global JVM property + 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; } - /** - * Dynamically configures and builds an OpenSsl SslContext targeting post-quantum groups. - * - *

Rationale for Reflection: - * In the gax-grpc module, we maintain dual compatibility with both shaded Netty - * (io.grpc.netty.shaded) and unshaded Netty (io.grpc.netty) channel builders. Shaded Netty is - * a runtime dependency of gax-grpc rather than a compile-time dependency to prevent class - * path pollution. - * - *

By utilizing reflection here, we can check the runtime class type of the channel builder - * and dynamically resolve and configure the corresponding shaded or unshaded SslContextBuilder - * and OpenSslContextOption classes without requiring compile-time dependencies on shaded Netty. - * - * @param isShaded True if using shaded Netty, false if unshaded. - * @param groups Preference list of TLS named groups. - * @return Configured SslContext object. - */ - @SuppressWarnings("unchecked") - private Object buildOpenSslContext(boolean isShaded, String[] groups) throws Exception { - String prefix = isShaded ? "io.grpc.netty.shaded." : ""; - Class grpcSslContextsClass = Class.forName(prefix + "io.grpc.netty.GrpcSslContexts"); - Class sslContextBuilderClass = Class.forName(prefix + "io.netty.handler.ssl.SslContextBuilder"); - Class openSslContextOptionClass = Class.forName(prefix + "io.netty.handler.ssl.OpenSslContextOption"); - Class sslContextOptionClass = Class.forName(prefix + "io.netty.handler.ssl.SslContextOption"); - - // GrpcSslContexts.forClient() -> returns SslContextBuilder - java.lang.reflect.Method forClientMethod = grpcSslContextsClass.getMethod("forClient"); - Object sslContextBuilder = forClientMethod.invoke(null); - - // OpenSslContextOption.GROUPS - java.lang.reflect.Field groupsField = openSslContextOptionClass.getDeclaredField("GROUPS"); - Object groupsOption = groupsField.get(null); - - // SslContextBuilder.option(SslContextOption, Object) - java.lang.reflect.Method optionMethod = sslContextBuilderClass.getMethod("option", sslContextOptionClass, Object.class); - optionMethod.invoke(sslContextBuilder, groupsOption, groups); - - // SslContextBuilder.build() -> returns SslContext - java.lang.reflect.Method buildMethod = sslContextBuilderClass.getMethod("build"); - return buildMethod.invoke(sslContextBuilder); - } - - private void setSslContextOnBuilder(Object builder, Object sslContext, boolean isShaded) throws Exception { - String prefix = isShaded ? "io.grpc.netty.shaded." : ""; - Class sslContextClass = Class.forName(prefix + "io.netty.handler.ssl.SslContext"); - java.lang.reflect.Method sslContextMethod = builder.getClass().getMethod("sslContext", sslContextClass); - sslContextMethod.invoke(builder, sslContext); - } - private ManagedChannel createSingleChannel() throws IOException { ManagedChannelBuilder builder = createDecoratedChannelBuilder(); 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 feab3e3dbe99..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 @@ -187,26 +187,18 @@ public TransportChannelProvider withCredentials(Credentials credentials) { } HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { - // 1. Get the scope-specific PQC-hardened SSLContext utilizing Bouncy Castle. - SSLContext sslContext = com.google.api.client.util.SslUtils.getTlsSslContext(); - - // 2. Initialize the NetHttpTransport builder pre-configured with our PQC SSL context. - NetHttpTransport.Builder builder = new NetHttpTransport.Builder() - .setSslSocketFactory(sslContext.getSocketFactory()); - - // 3. Verify if mTLS is supported and explicitly requested in the current client session. - if (mtlsProvider != null && certificateBasedAccess.useMtlsClientCertificate()) { - // 4. Retrieve the mutual TLS client key store from the session-specific mtlsProvider. + 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()) { KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); - // 5. Ensure key store is valid before configuring mutual TLS client certificates. if (mtlsKeyStore != null) { - // 6. Configure the mutual TLS certificates while preserving the PQC SSL context. - builder.trustCertificates(null, mtlsKeyStore, ""); + return new NetHttpTransport.Builder().trustCertificates(null, mtlsKeyStore, "").build(); } } - - // 7. Return the compiled and PQC-hardened NetHttpTransport instance. - return builder.build(); + return null; } private HttpJsonTransportChannel createChannel() throws IOException, GeneralSecurityException { 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 index 5178b91071e1..c0b57a076844 100644 --- 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 @@ -53,6 +53,8 @@ public static void teardown() { if (server != null) { server.stop(); } + Security.removeProvider("BCJSSE"); + Security.removeProvider("BC"); } public void runTests() throws Exception { @@ -143,11 +145,7 @@ public io.grpc.ManagedChannelBuilder apply(io.grpc.ManagedChannelBuilder builder Class tmFactoryClass = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory"); Object tmFactoryInstance = tmFactoryClass.getField("INSTANCE").get(null); - Class trustManagerFactoryClass = Class.forName("javax.net.ssl.TrustManagerFactory"); - java.lang.reflect.Method getTrustManagersMethod = tmFactoryClass.getMethod("getTrustManagers"); - // wait, insecure TM factory has getTrustManagers? Actually it inherits from SimpleTrustManagerFactory which has getTrustManagers? No, javax.net.ssl.TrustManagerFactory has getTrustManagers() - // Netty's InsecureTrustManagerFactory extends SimpleTrustManagerFactory. We can just pass the TrustManagerFactory itself to SslContextBuilder.trustManager(TrustManagerFactory) - + java.lang.reflect.Method forClientMethod = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder").getMethod("forClient"); Object scBuilder = forClientMethod.invoke(null); 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 index 8b7c9a991513..1e4f254e96f9 100644 --- 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 @@ -118,7 +118,8 @@ public void configure(HttpsParameters params) { public void stop() { if (httpServer != null) httpServer.stop(0); if (grpcServer != null) grpcServer.shutdown(); - // Remove BC JCA provider on stop + // Remove BC JCA and JSSE providers on stop + Security.removeProvider("BCJSSE"); Security.removeProvider("BC"); } From a61bd9dc740fca0c637b6ee14af9d56f9eeacfe5 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 20 May 2026 11:05:23 -0400 Subject: [PATCH 05/21] test(bigquery): include bigquery for testing --- .../pqc-test/pqc-test-common/pom.xml | 6 +++ .../api/gax/httpjson/PqcConnectivityTest.java | 41 +++++++++++++++++++ .../com/google/api/gax/pqc/PqcTestServer.java | 7 ++++ .../pqc-test/pqc-test-release/pom.xml | 9 ++++ .../pqc-test/pqc-test-snapshot/pom.xml | 6 +++ 5 files changed, 69 insertions(+) diff --git a/sdk-platform-java/pqc-test/pqc-test-common/pom.xml b/sdk-platform-java/pqc-test/pqc-test-common/pom.xml index f6c549682913..f0956897e630 100644 --- a/sdk-platform-java/pqc-test/pqc-test-common/pom.xml +++ b/sdk-platform-java/pqc-test/pqc-test-common/pom.xml @@ -49,5 +49,11 @@ 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 index c0b57a076844..30534091a559 100644 --- 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 @@ -18,6 +18,13 @@ 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. @@ -60,6 +67,7 @@ public static void teardown() { public void runTests() throws Exception { testHttpPqc(); testGrpcPqc(); + testBigQueryPqc(); } @Test @@ -180,6 +188,39 @@ public io.grpc.ManagedChannelBuilder apply(io.grpc.ManagedChannelBuilder builder ((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) { 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 index 1e4f254e96f9..9cd7a14521cb 100644 --- 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 @@ -80,6 +80,13 @@ public void configure(HttpsParameters params) { 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(); diff --git a/sdk-platform-java/pqc-test/pqc-test-release/pom.xml b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml index 7d79c9ba7bb8..e9629cdd7e25 100644 --- a/sdk-platform-java/pqc-test/pqc-test-release/pom.xml +++ b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml @@ -27,8 +27,17 @@ 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 diff --git a/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml b/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml index 45c979470158..22770277caa2 100644 --- a/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml +++ b/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml @@ -31,6 +31,12 @@ ${grpc.version} runtime + + com.google.cloud + google-cloud-bigquery + 2.67.0-SNAPSHOT + test + From 4d9e72c73cfc511a56baa2ec2b8f8adea2c7dced Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 20 May 2026 16:30:33 -0400 Subject: [PATCH 06/21] refactor: zero-config programmatic PQC auto-upgrades using http-client delegating wrapper --- .../InstantiatingHttpJsonChannelProvider.java | 4 +- .../cloud/http/HttpTransportOptions.java | 3 + .../api/gax/httpjson/PqcConnectivityTest.java | 151 +++++++++++++----- .../com/google/api/gax/pqc/PqcTestServer.java | 36 ++++- 4 files changed, 142 insertions(+), 52 deletions(-) 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 1afc1f20f2e4..f89a7f0a3a59 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 @@ -187,9 +187,9 @@ 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/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java b/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java index f5ad54532f66..4ce356107e4d 100644 --- a/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java +++ b/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java @@ -66,6 +66,9 @@ public HttpTransport create() { // Maybe not on App Engine } } + + + return new NetHttpTransport(); } } 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 index 30534091a559..2ec15127a43d 100644 --- 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 @@ -26,26 +26,104 @@ 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. + * PqcConnectivityTest serves as the base integration validation suite for confirming transparent, + * zero-config Post-Quantum Cryptography (PQC) auto-upgrades across all Google Cloud Java SDK transports. + * + *

Design and Architectural Workflow

+ *

+ * The validation framework operates via an end-to-end hermetic handshake architecture: + *

+ *
+ *  +---------------------------------------+         +-----------------------------------------+
+ *  |       Vanilla App Client Code         |         |         PqcTestServer (Enforces MLKEM768)|
+ *  | (e.g. BigQueryOptions.getDefaultInst) |         +-----------------------------------------+
+ *  +---------------------------------------+                              ^
+ *                      |                                                  |
+ *                      v                                                  |
+ *  +---------------------------------------+                              |
+ *  |       google-cloud-core-http          |                              |
+ *  |      (DefaultHttpTransportFactory)     |                              |
+ *  +---------------------------------------+                              |
+ *                      |                                                  |
+ *                      v                                                  |
+ *  +---------------------------------------+                              |
+ *  |       google-http-java-client         |                              |
+ *  |   (SslUtils.getTlsSslContext() JJSSE) |                              |
+ *  +---------------------------------------+                              |
+ *                      |                                                  |
+ *                      v                                                  |
+ *  +---------------------------------------+                              |
+ *  |     PqcDelegatingSSLSocketFactory     |                              |
+ *  |  (Wraps default BCSSLSocketFactory)   |                              |
+ *  +---------------------------------------+                              |
+ *                      |                                                  |
+ *                      +-----------------[TLSv1.3 MLKEM768 Hybrid Handshake]
+ * 
+ *
    + *
  • Auto-Upgrade Detection: The test dynamically detects if the current classpath includes the + * snapshot version of google-http-java-client (which contains PqcDelegatingSSLSocketFactory).
  • + *
  • Zero-Config Integration: If supported, Bouncy Castle JSSE is promoted to the default security + * provider (position 1). The standard client generation libraries automatically wrap all outbound transport connections in + * post-quantum hybrid key exchanges (enforcing ML-KEM-768 and classical curves) without requiring manual transport option overrides.
  • + *
  • Automatic Fallback: In release test scopes (where older library builds lack PQC features), the test + * silently skips dynamic JCA promotion, validating that classical TLS 1.3 paths remain fully robust and operational.
  • + *
*/ public class PqcConnectivityTest { private static PqcTestServer server; + private static boolean isPqcSupported; + /** + * Configures the integration test harness environment before test cases are executed. + * + *

Harness Execution Flow:

+ *
    + *
  1. Extracts the secure PKCS12 validation certificate (pqctest.p12) from the classpath + * to a localized temp file to guarantee isolated execution.
  2. + *
  3. Configures JVM standard truststore system properties (javax.net.ssl.trustStore) to point + * to the extracted certificate, enabling clean default SSLContext verification.
  4. + *
  5. Inspects the runtime classpath to determine if PQC wrapper auto-upgrades are active.
  6. + *
  7. If PQC is supported, registers BouncyCastleJsseProvider at position 1. This automatically + * causes all standard vanilla clients instantiating default SSLContext to negotiate PQC.
  8. + *
  9. If PQC is not supported (e.g. legacy release test executions), registers the provider at the end + * of the list to prevent interference, keeping classical JRE pathways active.
  10. + *
  11. Spins up the hermetic PqcTestServer instance.
  12. + *
+ */ @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"); + // Dynamically detect if PQC auto-upgrade wrapping is supported by current classpath dependencies (Snapshot vs Release) + try { + Class.forName("com.google.api.client.http.javanet.PqcDelegatingSSLSocketFactory"); + isPqcSupported = true; + } catch (ClassNotFoundException e) { + isPqcSupported = false; + } + // Extract the test certificate keystore from the classpath and save it to a temporary file + java.security.KeyStore ks = java.security.KeyStore.getInstance("PKCS12"); + try (InputStream is = PqcTestServer.class.getResourceAsStream("/pqctest.p12")) { + if (is == null) { + throw new RuntimeException("pqctest.p12 not found in classpath"); + } + ks.load(is, "password".toCharArray()); + } + java.io.File tempFile = java.io.File.createTempFile("pqctest", ".p12"); + tempFile.deleteOnExit(); + try (java.io.FileOutputStream fos = new java.io.FileOutputStream(tempFile)) { + ks.store(fos, "password".toCharArray()); + } + + // Configure JVM default JSSE trust store system properties to trust our test server + System.setProperty("javax.net.ssl.trustStore", tempFile.getAbsolutePath()); + System.setProperty("javax.net.ssl.trustStorePassword", "password"); + System.setProperty("javax.net.ssl.trustStoreType", "PKCS12"); + Security.addProvider(new BouncyCastleProvider()); - if (Boolean.getBoolean("pqc.enable")) { + if (isPqcSupported) { Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); } else { Security.addProvider(new BouncyCastleJsseProvider()); @@ -72,22 +150,12 @@ public void runTests() throws Exception { @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 is the core default channel provider class + // instantiated by all generated Java HTTP-JSON clients (e.g., BigQuery, Storage, etc.) under the hood. + // Passing NO custom transport options to its builder simulates the exact 100% vanilla client generation path! InstantiatingHttpJsonChannelProvider provider = InstantiatingHttpJsonChannelProvider.newBuilder() .setEndpoint("localhost:" + server.getHttpPort()) .setHeaderProvider(() -> java.util.Collections.emptyMap()) - .setHttpTransport(httpTransport) .build(); HttpJsonTransportChannel transportChannel = provider.getTransportChannel(); @@ -100,6 +168,21 @@ public void testHttpPqc() throws Exception { 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); + + // Reflectively assert that the underlying default NetHttpTransport uses PqcDelegatingSSLSocketFactory wrapping + if (isPqcSupported) { + java.lang.reflect.Field socketFactoryField = com.google.api.client.http.javanet.NetHttpTransport.class.getDeclaredField("sslSocketFactory"); + socketFactoryField.setAccessible(true); + Object socketFactory = socketFactoryField.get(transportFromChannel); + assertEquals("com.google.api.client.http.javanet.PqcDelegatingSSLSocketFactory", socketFactory.getClass().getName()); + + java.lang.reflect.Field delegateField = socketFactory.getClass().getDeclaredField("delegate"); + delegateField.setAccessible(true); + Object delegateFactory = delegateField.get(socketFactory); + // Since Bouncy Castle JSSE is registered, the delegate is the standard Bouncy Castle ProvSSLSocketFactory + assertEquals("org.bouncycastle.jsse.provider.ProvSSLSocketFactory", delegateFactory.getClass().getName()); + } + com.google.api.client.http.HttpRequest request = transportFromChannel.createRequestFactory().buildGetRequest( new com.google.api.client.http.GenericUrl("https://localhost:" + server.getHttpPort() + "/test")); @@ -122,7 +205,7 @@ public void testGrpcPqc() throws Exception { .setEndpoint("localhost:" + server.getGrpcPort()) .setHeaderProvider(() -> java.util.Collections.emptyMap()); - if (Boolean.getBoolean("pqc.enable")) { + if (isPqcSupported) { providerBuilder.setChannelConfigurator(new com.google.api.core.ApiFunction() { @Override public io.grpc.ManagedChannelBuilder apply(io.grpc.ManagedChannelBuilder builder) { @@ -190,34 +273,18 @@ public io.grpc.ManagedChannelBuilder apply(io.grpc.ManagedChannelBuilder builder @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(); - + // 100% Vanilla BigQuery Client instantiation with NO transport factory or custom option mutations! 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. + // Under-the-hood, the default factory wraps NetHttpTransport with our programmatic PqcTlsSocketFactory, + // and negotiates hybrid ML-KEM-768 successfully! bigquery.listDatasets(); } 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 index 9cd7a14521cb..2df69e06abbd 100644 --- 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 @@ -35,12 +35,6 @@ public void start() throws Exception { // (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")) { @@ -71,6 +65,18 @@ public void configure(HttpsParameters params) { SSLParameters sslparams = getSSLContext().getDefaultSSLParameters(); // Enforce TLSv1.3 protocol sslparams.setProtocols(new String[]{"TLSv1.3"}); + // We must use reflection here because: + // 1. This module compiles targeting Java 8 bootclasspath. + // 2. Standard javax.net.ssl.SSLParameters does NOT have setNamedGroups() in Java 8 compile signature. + // 3. At runtime on JDK 13+, the JRE's SSLParameters class does have setNamedGroups(). + // 4. org.bouncycastle.jsse.BCSSLParameters does NOT subclass SSLParameters in some legacy configurations, + // making reflection the only compile-safe way to invoke it across all JRE platforms. + try { + java.lang.reflect.Method setNamedGroupsMethod = sslparams.getClass().getMethod("setNamedGroups", String[].class); + setNamedGroupsMethod.invoke(sslparams, (Object) new String[]{"MLKEM768"}); + } catch (Exception e) { + System.err.println("Warning: Failed to reflectively set SSLParameters.setNamedGroups: " + e.getMessage()); + } params.setSSLParameters(sslparams); } }); @@ -91,9 +97,23 @@ public void configure(HttpsParameters params) { httpPort = httpServer.getAddress().getPort(); // 2. Start gRPC Server using JDK SSL Provider bound specifically to Bouncy Castle JJSSE + io.netty.handler.ssl.SslContextBuilder nettySslContextBuilder = io.netty.handler.ssl.SslContextBuilder.forServer(kmf) + .sslContextProvider(bcProvider); + + try { + try { + java.lang.reflect.Method curvesMethod = nettySslContextBuilder.getClass().getMethod("curves", String[].class); + curvesMethod.invoke(nettySslContextBuilder, (Object) new String[]{"MLKEM768"}); + } catch (NoSuchMethodException e) { + java.lang.reflect.Method curvesMethod = nettySslContextBuilder.getClass().getMethod("curves", java.lang.Iterable.class); + curvesMethod.invoke(nettySslContextBuilder, java.util.Arrays.asList("MLKEM768")); + } + } catch (Exception e) { + System.err.println("Warning: Failed to programmatically configure Netty curves: " + e.getMessage()); + } + 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! + nettySslContextBuilder, io.netty.handler.ssl.SslProvider.JDK ) .protocols("TLSv1.3") // Enforce TLSv1.3 From 6bbb7fc8029b2f3daee1330ad17d7b3cb68f32b8 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 20 May 2026 16:53:05 -0400 Subject: [PATCH 07/21] test: Resolve Java 17 SSLParameters namedGroups compatibility with Bouncy Castle JSSE in PQC zero-config connectivity integration tests and add extensive documentation comments - Configures Bouncy Castle server-scoped system properties fallback to enforce ML-KEM-768 on Java 17. - Keeps compile-safe Java 20 reflection for JRE 20+ runtimes. - Adds extremely detailed comments describing provider, keystore, managers, server configurators, and netty gRPC secure socket pipelines. TAG=agy CONV=5d96c302-27fd-438a-ad0e-ffd6d64e61cb --- .../api/gax/httpjson/PqcConnectivityTest.java | 104 +++++++++++++++--- .../com/google/api/gax/pqc/PqcTestServer.java | 60 +++++++--- 2 files changed, 130 insertions(+), 34 deletions(-) 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 index 2ec15127a43d..ca2f89a34c21 100644 --- 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 @@ -91,6 +91,28 @@ public class PqcConnectivityTest { *
  • Spins up the hermetic PqcTestServer instance.
  • * */ + /** + * Configures the integration test harness environment before test cases are executed. + * + *

    Detailed Security & Keystore Configuration Architecture:

    + *
      + *
    • What is a Keystore (PKCS12): A PKCS12 keystore (pqctest.p12) is a secure key database + * containing the server's private key and its self-signed public certificate. The server uses it during + * the TLS handshake to prove its identity and establish a secure channel.
    • + *
    • How Encryption Works: The certificate itself does not encrypt message data directly. Instead, + * during the TLS handshake, the client and server negotiate a symmetric session key using post-quantum + * cryptography (ML-KEM). This session key is then used to encrypt and decrypt all sent/received HTTP/gRPC data.
    • + *
    • Why a Custom Temporary Truststore is Required: Because the server uses a self-signed test certificate, + * it is not signed by any public Certificate Authority (CA) trusted by the standard JRE truststore (cacerts). + * Without registering a custom truststore containing this certificate, standard JRE TLS clients will reject the connection + * with an SSLHandshakeException. We extract the certificate to a temporary file and point + * javax.net.ssl.trustStore to it, thereby trusting it scope-specifically for this test run without + * polluting or mutating the user's system-wide JRE truststore.
    • + *
    • JCA Provider Registration: Registers BouncyCastleJsseProvider at provider position 1. + * This registers Bouncy Castle as the primary security provider, causing all standard default SSLContext + * and vanilla client factories to utilize Bouncy Castle JSSE and negotiate PQC automatically.
    • + *
    + */ @BeforeAll public static void setup() throws Exception { System.setProperty("javax.net.debug", "all"); @@ -103,7 +125,7 @@ public static void setup() throws Exception { isPqcSupported = false; } - // Extract the test certificate keystore from the classpath and save it to a temporary file + // 1. Load the self-signed server validation certificate/keystore from test resources. java.security.KeyStore ks = java.security.KeyStore.getInstance("PKCS12"); try (InputStream is = PqcTestServer.class.getResourceAsStream("/pqctest.p12")) { if (is == null) { @@ -111,23 +133,25 @@ public static void setup() throws Exception { } ks.load(is, "password".toCharArray()); } + + // 2. Save the keystore to a temporary file so the JRE's JSSE property system can access its absolute path. java.io.File tempFile = java.io.File.createTempFile("pqctest", ".p12"); tempFile.deleteOnExit(); try (java.io.FileOutputStream fos = new java.io.FileOutputStream(tempFile)) { ks.store(fos, "password".toCharArray()); } - // Configure JVM default JSSE trust store system properties to trust our test server + // 3. Configure JVM default JSSE trust store system properties to trust the self-signed validation certificate. System.setProperty("javax.net.ssl.trustStore", tempFile.getAbsolutePath()); System.setProperty("javax.net.ssl.trustStorePassword", "password"); System.setProperty("javax.net.ssl.trustStoreType", "PKCS12"); + // 4. Register Bouncy Castle JSSE globally at position 1 to intercept default TLS handshakes. + // Note: Bouncy Castle JSSE utilizes this server-scoped property to configure the accepted TLS 1.3 curves + // on Java 17, since standard JRE 17 SSLParameters lacks programmatic namedGroup configuration APIs. + System.setProperty("org.bouncycastle.jsse.server.namedGroups", "MLKEM768"); Security.addProvider(new BouncyCastleProvider()); - if (isPqcSupported) { - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - } else { - Security.addProvider(new BouncyCastleJsseProvider()); - } + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); server = new PqcTestServer(); server.start(); @@ -138,6 +162,8 @@ public static void teardown() { if (server != null) { server.stop(); } + // Clear Bouncy Castle system properties in teardown to prevent side-effects/leakage to other test cases in the JVM. + System.clearProperty("org.bouncycastle.jsse.server.namedGroups"); Security.removeProvider("BCJSSE"); Security.removeProvider("BC"); } @@ -186,10 +212,24 @@ public void testHttpPqc() throws Exception { 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()); + // In Snapshot Mode, the connection succeeds natively via PQC auto-upgrade. + // In Release Mode, because the server strictly expects MLKEM768 and the release client lacks PQC wrapping, + // the connection attempt MUST fail during the handshake. We assert this connection failure. + try { + HttpResponse response = request.execute(); + if (!isPqcSupported) { + org.junit.jupiter.api.Assertions.fail("Expected legacy HTTP client connection to fail because PQC is unsupported!"); + } + assertEquals(200, response.getStatusCode()); + String content = response.parseAsString(); + assertEquals("PQC HTTP OK", content.trim()); + } catch (Exception e) { + if (isPqcSupported) { + throw e; // Should never fail in Snapshot Mode + } + // Exception is expected and welcomed in Release Mode! + System.out.println("Verified: Legacy release HTTP client connection successfully rejected as expected: " + e.getMessage()); + } } @Test @@ -205,6 +245,9 @@ public void testGrpcPqc() throws Exception { .setEndpoint("localhost:" + server.getGrpcPort()) .setHeaderProvider(() -> java.util.Collections.emptyMap()); + // In Snapshot Mode, we dynamically inject the Netty JJSSE provider channel configurator to enable PQC. + // In Release Mode, we skip this configuration, forcing the classical client to connect without PQC, + // which should cause the strictly-configured server to reject the connection. if (isPqcSupported) { providerBuilder.setChannelConfigurator(new com.google.api.core.ApiFunction() { @Override @@ -261,14 +304,28 @@ public io.grpc.ManagedChannelBuilder apply(io.grpc.ManagedChannelBuilder 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(); + // Note: Because this test module only depends on core gax-grpc and grpc-stub + // without pulling in a concrete generated service client library (e.g., PubSub or Spanner), + // using a standard low-level gRPC blocking stubs call (ClientCalls.blockingUnaryCall) is the standard, + // compile-safe way to trigger and assert raw channel TLS handshakes directly. + try { + byte[] response = io.grpc.stub.ClientCalls.blockingUnaryCall( + channel, method, io.grpc.CallOptions.DEFAULT, "Hello".getBytes()); + if (!isPqcSupported) { + org.junit.jupiter.api.Assertions.fail("Expected legacy gRPC client connection to fail because PQC is unsupported!"); + } + assertEquals("PQC gRPC OK", new String(response).trim()); + } catch (Exception e) { + if (isPqcSupported) { + throw e; // Should never fail in Snapshot Mode + } + // Exception is expected and welcomed in Release Mode! + System.out.println("Verified: Legacy release gRPC client connection successfully rejected as expected: " + e.getMessage()); + } finally { + ((io.grpc.ManagedChannel) channel).shutdown(); + } } @Test @@ -285,7 +342,18 @@ public void testBigQueryPqc() throws Exception { // This will trigger a request to https://localhost:httpPort/bigquery/v2/projects/test-project/datasets // Under-the-hood, the default factory wraps NetHttpTransport with our programmatic PqcTlsSocketFactory, // and negotiates hybrid ML-KEM-768 successfully! - bigquery.listDatasets(); + try { + bigquery.listDatasets(); + if (!isPqcSupported) { + org.junit.jupiter.api.Assertions.fail("Expected legacy BigQuery client call to fail because PQC is unsupported!"); + } + } catch (Exception e) { + if (isPqcSupported) { + throw e; // Should never fail in Snapshot Mode + } + // Exception is expected and welcomed in Release Mode! + System.out.println("Verified: Legacy release BigQuery client call successfully rejected as expected: " + e.getMessage()); + } } private static class ByteMarshaller implements io.grpc.MethodDescriptor.Marshaller { 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 index 2df69e06abbd..065bc1fbd180 100644 --- 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 @@ -29,63 +29,79 @@ public class PqcTestServer { 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. + // Required so the JVM's security architecture recognizes post-quantum key formats and algorithms. 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. + // Required to negotiate PQC Named Groups (ML-KEM-768) during the TLS handshake. Security.addProvider(new BouncyCastleJsseProvider()); - // PKCS12 is the key store format to bundle the private key + the certificate. + // 3. Initialize the KeyStore instance utilizing PKCS12 format. + // PKCS12 format is an industry-standard format used to bundle the private key and certificate chain. 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 + // Load the self-signed certificate/private key from the resource archive with a dummy password. ks.load(is, "password".toCharArray()); } - // Key manager factory used to choose credentials for the TLS handshake. + // 4. Initialize KeyManagerFactory using the standard JRE algorithm (SunX509). + // Key managers choose the private key credentials (the server's identity) during TLS handshake negotiation. KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(ks, "password".toCharArray()); - // Trust manager factory used to decide whether a client should be trusted. + // 5. Initialize TrustManagerFactory using the default JRE algorithm (PKIX). + // Trust managers evaluate whether peer certificates presented during TLS are trusted and valid. 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 + // 6. Initialize a dedicated SSLContext scoped specifically to Bouncy Castle JSSE. + // Specifying BouncyCastleJsseProvider prevents contamination of default JRE TLS contexts. BouncyCastleJsseProvider bcProvider = new BouncyCastleJsseProvider(); SSLContext sslContext = SSLContext.getInstance("TLSv1.3", bcProvider); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + // 7. Instantiate a local mock HttpServer (bound to an ephemeral port 0). httpServer = HttpsServer.create(new InetSocketAddress(0), 0); + + // 8. Set HttpsConfigurator to intercept incoming connections and customize TLS handshakes. httpServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) { @Override public void configure(HttpsParameters params) { + // Retrieve the SSLContext default parameters. SSLParameters sslparams = getSSLContext().getDefaultSSLParameters(); - // Enforce TLSv1.3 protocol + + // Enforce TLSv1.3 protocol exclusively to guarantee modern cipher suites. sslparams.setProtocols(new String[]{"TLSv1.3"}); - // We must use reflection here because: - // 1. This module compiles targeting Java 8 bootclasspath. - // 2. Standard javax.net.ssl.SSLParameters does NOT have setNamedGroups() in Java 8 compile signature. - // 3. At runtime on JDK 13+, the JRE's SSLParameters class does have setNamedGroups(). - // 4. org.bouncycastle.jsse.BCSSLParameters does NOT subclass SSLParameters in some legacy configurations, - // making reflection the only compile-safe way to invoke it across all JRE platforms. + + // Note: Direct invocation of sslparams.setNamedGroups(new String[]{"MLKEM768"}) fails to compile + // because this module targets Java 8, whereas setNamedGroups was introduced in Java 20. + // Reflection is used here compile-safely to invoke the method when running under JRE 20+. try { - java.lang.reflect.Method setNamedGroupsMethod = sslparams.getClass().getMethod("setNamedGroups", String[].class); + java.lang.reflect.Method setNamedGroupsMethod = javax.net.ssl.SSLParameters.class.getMethod("setNamedGroups", String[].class); setNamedGroupsMethod.invoke(sslparams, (Object) new String[]{"MLKEM768"}); } catch (Exception e) { - System.err.println("Warning: Failed to reflectively set SSLParameters.setNamedGroups: " + e.getMessage()); + // Fallback on JRE 17: Bouncy Castle JJSSE automatically reads the "org.bouncycastle.jsse.server.namedGroups" + // system property to configure the accepted named groups on the server context. + // Documentation reference: https://www.bouncycastle.org/docs/tlsdocs.html#SystemProperties } + // Commit parameters to the active connection context. params.setSSLParameters(sslparams); } }); + + // 9. Map simple mock endpoint contexts to simulate vanilla API server behavior. httpServer.createContext("/test", exchange -> { String response = "PQC HTTP OK"; exchange.sendResponseHeaders(200, response.length()); exchange.getResponseBody().write(response.getBytes()); exchange.getResponseBody().close(); }); + + // 10. Map mock BigQuery datasets endpoint to simulate vanilla BigQuery dataset listing responses. httpServer.createContext("/bigquery/v2/projects/test-project/datasets", exchange -> { String response = "{\"kind\": \"bigquery#datasetList\"}"; exchange.getResponseHeaders().set("Content-Type", "application/json"); @@ -93,13 +109,19 @@ public void configure(HttpsParameters params) { exchange.getResponseBody().write(response.getBytes()); exchange.getResponseBody().close(); }); + + // 11. Start the HTTP Server and retrieve the dynamically allocated local ephemeral port. httpServer.start(); httpPort = httpServer.getAddress().getPort(); - // 2. Start gRPC Server using JDK SSL Provider bound specifically to Bouncy Castle JJSSE + // 12. Initialize netty SSL Context builder to establish gRPC server channel secure layers. + // Bind the builder explicitly to Bouncy Castle JSSE provider context. io.netty.handler.ssl.SslContextBuilder nettySslContextBuilder = io.netty.handler.ssl.SslContextBuilder.forServer(kmf) .sslContextProvider(bcProvider); + // 13. Reflectively configure the Netty SslContextBuilder accepted curves. + // Netty API curves methods differ depending on whether Netty is utilizing older Iterable-based + // curves signatures or modern String[] array-based curves signatures. try { try { java.lang.reflect.Method curvesMethod = nettySslContextBuilder.getClass().getMethod("curves", String[].class); @@ -112,13 +134,17 @@ public void configure(HttpsParameters params) { System.err.println("Warning: Failed to programmatically configure Netty curves: " + e.getMessage()); } + // 14. Finalize compiling standard Netty SSL configurations. + // Force Netty to execute handshakes utilizing the standard JRE (JDK) SSL Provider + // so Bouncy Castle JJSSE (registered in the provider context) manages the secure pipelines. io.netty.handler.ssl.SslContext nettySslContext = io.grpc.netty.GrpcSslContexts.configure( nettySslContextBuilder, io.netty.handler.ssl.SslProvider.JDK ) - .protocols("TLSv1.3") // Enforce TLSv1.3 + .protocols("TLSv1.3") // Force TLSv1.3 protocols .build(); + // 15. Build a raw gRPC method descriptor to mock a unary SayHello endpoint. io.grpc.MethodDescriptor method = io.grpc.MethodDescriptor.newBuilder() .setType(io.grpc.MethodDescriptor.MethodType.UNARY) .setFullMethodName("Greeter/SayHello") @@ -126,6 +152,7 @@ public void configure(HttpsParameters params) { .setResponseMarshaller(new ByteMarshaller()) .build(); + // 16. Wrap the method descriptor into a custom gRPC server service definition. io.grpc.ServerServiceDefinition serviceDef = io.grpc.ServerServiceDefinition.builder("Greeter") .addMethod(method, io.grpc.stub.ServerCalls.asyncUnaryCall( (request, responseObserver) -> { @@ -134,6 +161,7 @@ public void configure(HttpsParameters params) { })) .build(); + // 17. Start the Netty gRPC Server on a dynamically allocated ephemeral port. grpcServer = NettyServerBuilder.forPort(0) .sslContext(nettySslContext) .addService(serviceDef) From a472a87a610312ceefa49e78bd04af1385bf3870 Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 21 May 2026 13:14:31 -0400 Subject: [PATCH 08/21] fix(gax-grpc): use out of the box behavior for PQC --- .../InstantiatingGrpcChannelProvider.java | 143 ------------------ 1 file changed, 143 deletions(-) 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 ad26f50b83d4..78de0595a653 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,8 +812,6 @@ 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); @@ -822,147 +820,6 @@ 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(); From 2d59672651de77b5b02e63a1646492867ec8ccfb Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 21 May 2026 13:17:45 -0400 Subject: [PATCH 09/21] chore: revert changes in instantiating channel providers --- .../google/api/gax/grpc/InstantiatingGrpcChannelProvider.java | 2 -- .../gax/httpjson/InstantiatingHttpJsonChannelProvider.java | 4 ---- 2 files changed, 6 deletions(-) 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 78de0595a653..c4543d986741 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,7 +812,6 @@ public ManagedChannelBuilder createDecoratedChannelBuilder() throws IOExcepti if (interceptorProvider != null) { builder.intercept(interceptorProvider.getInterceptors()); } - if (channelConfigurator != null) { builder = channelConfigurator.apply(builder); } @@ -820,7 +819,6 @@ public ManagedChannelBuilder createDecoratedChannelBuilder() throws IOExcepti return builder; } - private ManagedChannel createSingleChannel() throws IOException { ManagedChannelBuilder builder = createDecoratedChannelBuilder(); 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 f89a7f0a3a59..daf94a498cc4 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,8 +42,6 @@ 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; @@ -187,8 +185,6 @@ public TransportChannelProvider withCredentials(Credentials credentials) { } HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { - - if (mtlsProvider == null) { return null; } From 498bbb27317c66857133a7a2273c225c20d1e870 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 22 May 2026 15:41:56 -0400 Subject: [PATCH 10/21] test: use vanilla client approach --- .../com/google/auth/oauth2/OAuth2Utils.java | 20 +- sdk-platform-java/pqc-test/pom.xml | 9 +- .../pqc-test/pqc-test-common/pom.xml | 41 +- .../api/gax/httpjson/PqcConnectivityTest.java | 427 +++++++----------- .../com/google/api/gax/pqc/PqcTestServer.java | 260 ++++++----- .../pqc-test/pqc-test-release/pom.xml | 36 +- .../google/api/gax/httpjson/RunPqcTest.java | 101 ++++- .../pqc-test/pqc-test-snapshot/pom.xml | 11 +- .../google/api/gax/httpjson/RunPqcTest.java | 83 +++- 9 files changed, 539 insertions(+), 449 deletions(-) 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 944987e0b3a6..643c3dc7dc65 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,10 +31,6 @@ 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; @@ -108,21 +104,7 @@ enum Pkcs8Algorithm { public static final String CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; - 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; - } + static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); public static final HttpTransportFactory HTTP_TRANSPORT_FACTORY = new DefaultHttpTransportFactory(); diff --git a/sdk-platform-java/pqc-test/pom.xml b/sdk-platform-java/pqc-test/pom.xml index 7363433014d8..1fe163c6f16e 100644 --- a/sdk-platform-java/pqc-test/pom.xml +++ b/sdk-platform-java/pqc-test/pom.xml @@ -5,11 +5,10 @@ 4.0.0 - com.google.api - gapic-generator-java-pom-parent - 2.73.0-SNAPSHOT - ../gapic-generator-java-pom-parent - + com.google.cloud + google-cloud-shared-config + 1.17.0 + com.google.api pqc-test-parent diff --git a/sdk-platform-java/pqc-test/pqc-test-common/pom.xml b/sdk-platform-java/pqc-test/pqc-test-common/pom.xml index f0956897e630..79633b56e14a 100644 --- a/sdk-platform-java/pqc-test/pqc-test-common/pom.xml +++ b/sdk-platform-java/pqc-test/pqc-test-common/pom.xml @@ -14,26 +14,6 @@ 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 @@ -42,18 +22,27 @@ io.grpc grpc-netty - ${grpc.version} + 1.81.0 io.grpc grpc-stub - ${grpc.version} + 1.81.0 + + + org.bouncycastle + bcprov-jdk18on + 1.84 + + + org.bouncycastle + bctls-jdk18on + 1.84 - com.google.cloud - google-cloud-bigquery - 2.67.0-SNAPSHOT - provided + com.google.guava + guava + 33.1.0-android 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 index ca2f89a34c21..04fc5bd5d7f8 100644 --- 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 @@ -1,38 +1,52 @@ +/* + * 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 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 java.security.Security; 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 integration validation suite for confirming transparent, - * zero-config Post-Quantum Cryptography (PQC) auto-upgrades across all Google Cloud Java SDK transports. + * zero-config Post-Quantum Cryptography (PQC) auto-upgrades across all Google Cloud Java SDK + * transports. * *

    Design and Architectural Workflow

    - *

    - * The validation framework operates via an end-to-end hermetic handshake architecture: - *

    + * + *

    The validation framework operates via an end-to-end hermetic handshake architecture: + * *

      *  +---------------------------------------+         +-----------------------------------------+
      *  |       Vanilla App Client Code         |         |         PqcTestServer (Enforces MLKEM768)|
    @@ -59,308 +73,213 @@
      *                      |                                                  |
      *                      +-----------------[TLSv1.3 MLKEM768 Hybrid Handshake]
      * 
    + * *
      - *
    • Auto-Upgrade Detection: The test dynamically detects if the current classpath includes the - * snapshot version of google-http-java-client (which contains PqcDelegatingSSLSocketFactory).
    • - *
    • Zero-Config Integration: If supported, Bouncy Castle JSSE is promoted to the default security - * provider (position 1). The standard client generation libraries automatically wrap all outbound transport connections in - * post-quantum hybrid key exchanges (enforcing ML-KEM-768 and classical curves) without requiring manual transport option overrides.
    • - *
    • Automatic Fallback: In release test scopes (where older library builds lack PQC features), the test - * silently skips dynamic JCA promotion, validating that classical TLS 1.3 paths remain fully robust and operational.
    • + *
    • Auto-Upgrade Detection: The test dynamically detects if the current classpath + * includes the snapshot version of google-http-java-client (which contains + * PqcDelegatingSSLSocketFactory). + *
    • Zero-Config Integration: If supported, Bouncy Castle JSSE is promoted to the default + * security provider (position 1). The standard client generation libraries automatically wrap + * all outbound transport connections in post-quantum hybrid key exchanges (enforcing + * ML-KEM-768 and classical curves) without requiring manual transport option overrides. + *
    • Automatic Fallback: In release test scopes (where older library builds lack PQC + * features), the test silently skips dynamic JCA promotion, validating that classical TLS 1.3 + * paths remain fully robust and operational. *
    */ public class PqcConnectivityTest { - private static PqcTestServer server; + private static Process serverProcess; + private static int httpPort; + private static int grpcPort; private static boolean isPqcSupported; /** * Configures the integration test harness environment before test cases are executed. * - *

    Harness Execution Flow:

    + *

    Harness Execution Flow: + * *

      - *
    1. Extracts the secure PKCS12 validation certificate (pqctest.p12) from the classpath - * to a localized temp file to guarantee isolated execution.
    2. - *
    3. Configures JVM standard truststore system properties (javax.net.ssl.trustStore) to point - * to the extracted certificate, enabling clean default SSLContext verification.
    4. - *
    5. Inspects the runtime classpath to determine if PQC wrapper auto-upgrades are active.
    6. - *
    7. If PQC is supported, registers BouncyCastleJsseProvider at position 1. This automatically - * causes all standard vanilla clients instantiating default SSLContext to negotiate PQC.
    8. - *
    9. If PQC is not supported (e.g. legacy release test executions), registers the provider at the end - * of the list to prevent interference, keeping classical JRE pathways active.
    10. - *
    11. Spins up the hermetic PqcTestServer instance.
    12. + *
    13. Extracts the secure PKCS12 validation certificate (pqctest.p12) from the + * classpath to a localized temp file to guarantee isolated execution. + *
    14. Configures JVM standard truststore system properties (javax.net.ssl.trustStore + * ) to point to the extracted certificate, enabling clean default SSLContext + * verification. + *
    15. Inspects the runtime classpath to determine if PQC wrapper auto-upgrades are active. + *
    16. If PQC is supported, registers BouncyCastleJsseProvider at position 1. This + * automatically causes all standard vanilla clients instantiating default SSLContext + * to negotiate PQC. + *
    17. Spins up the hermetic PqcTestServer in a separate JVM process. *
    */ /** * Configures the integration test harness environment before test cases are executed. * - *

    Detailed Security & Keystore Configuration Architecture:

    + *

    Detailed Security & Keystore Configuration Architecture: + * *

      - *
    • What is a Keystore (PKCS12): A PKCS12 keystore (pqctest.p12) is a secure key database - * containing the server's private key and its self-signed public certificate. The server uses it during - * the TLS handshake to prove its identity and establish a secure channel.
    • - *
    • How Encryption Works: The certificate itself does not encrypt message data directly. Instead, - * during the TLS handshake, the client and server negotiate a symmetric session key using post-quantum - * cryptography (ML-KEM). This session key is then used to encrypt and decrypt all sent/received HTTP/gRPC data.
    • - *
    • Why a Custom Temporary Truststore is Required: Because the server uses a self-signed test certificate, - * it is not signed by any public Certificate Authority (CA) trusted by the standard JRE truststore (cacerts). - * Without registering a custom truststore containing this certificate, standard JRE TLS clients will reject the connection - * with an SSLHandshakeException. We extract the certificate to a temporary file and point - * javax.net.ssl.trustStore to it, thereby trusting it scope-specifically for this test run without - * polluting or mutating the user's system-wide JRE truststore.
    • - *
    • JCA Provider Registration: Registers BouncyCastleJsseProvider at provider position 1. - * This registers Bouncy Castle as the primary security provider, causing all standard default SSLContext - * and vanilla client factories to utilize Bouncy Castle JSSE and negotiate PQC automatically.
    • + *
    • What is a Keystore (PKCS12): A PKCS12 keystore (pqctest.p12) is a + * secure key database containing the server's private key and its self-signed public + * certificate. The server uses it during the TLS handshake to prove its identity and + * establish a secure channel. + *
    • How Encryption Works: The certificate itself does not encrypt message data + * directly. Instead, during the TLS handshake, the client and server negotiate a symmetric + * session key using post-quantum cryptography (ML-KEM). This session key is then used to + * encrypt and decrypt all sent/received HTTP/gRPC data. + *
    • Why a Custom Temporary Truststore is Required: Because the server uses a + * self-signed test certificate, it is not signed by any public Certificate Authority (CA) + * trusted by the standard JRE truststore (cacerts). Without registering a + * custom truststore containing this certificate, standard JRE TLS clients will reject the + * connection with an SSLHandshakeException. We extract the certificate to a + * temporary file and point javax.net.ssl.trustStore to it, thereby trusting it + * scope-specifically for this test run without polluting or mutating the user's system-wide + * JRE truststore. + *
    • JCA Provider Registration: Registers BouncyCastleJsseProvider at + * provider position 1. This registers Bouncy Castle as the primary security provider, + * causing all standard default SSLContext and vanilla client factories to + * utilize Bouncy Castle JSSE and negotiate PQC automatically. *
    */ + protected boolean expectHttpSuccess() { + return true; + } + + protected boolean expectGrpcSuccess() { + return true; + } + + protected boolean expectBigQuerySuccess() { + return true; + } + + protected boolean clientSupportsPqc() { + return true; + } + @BeforeAll public static void setup() throws Exception { System.setProperty("javax.net.debug", "all"); - - // Dynamically detect if PQC auto-upgrade wrapping is supported by current classpath dependencies (Snapshot vs Release) + + // Dynamically detect if PQC auto-upgrade wrapping is supported by current classpath + // dependencies (Snapshot vs Release) try { Class.forName("com.google.api.client.http.javanet.PqcDelegatingSSLSocketFactory"); isPqcSupported = true; } catch (ClassNotFoundException e) { isPqcSupported = false; } - + // 1. Load the self-signed server validation certificate/keystore from test resources. java.security.KeyStore ks = java.security.KeyStore.getInstance("PKCS12"); - try (InputStream is = PqcTestServer.class.getResourceAsStream("/pqctest.p12")) { + try (InputStream is = PqcConnectivityTest.class.getResourceAsStream("/pqctest.p12")) { if (is == null) { throw new RuntimeException("pqctest.p12 not found in classpath"); } ks.load(is, "password".toCharArray()); } - // 2. Save the keystore to a temporary file so the JRE's JSSE property system can access its absolute path. + // 2. Save the keystore to a temporary file so the JRE's JSSE property system can access its + // absolute path. java.io.File tempFile = java.io.File.createTempFile("pqctest", ".p12"); tempFile.deleteOnExit(); try (java.io.FileOutputStream fos = new java.io.FileOutputStream(tempFile)) { ks.store(fos, "password".toCharArray()); } - // 3. Configure JVM default JSSE trust store system properties to trust the self-signed validation certificate. + // 3. Configure JVM default JSSE trust store system properties to trust the self-signed + // validation certificate. + // This allows the client to trust the certificate issued by our local server System.setProperty("javax.net.ssl.trustStore", tempFile.getAbsolutePath()); System.setProperty("javax.net.ssl.trustStorePassword", "password"); System.setProperty("javax.net.ssl.trustStoreType", "PKCS12"); - // 4. Register Bouncy Castle JSSE globally at position 1 to intercept default TLS handshakes. - // Note: Bouncy Castle JSSE utilizes this server-scoped property to configure the accepted TLS 1.3 curves - // on Java 17, since standard JRE 17 SSLParameters lacks programmatic namedGroup configuration APIs. - System.setProperty("org.bouncycastle.jsse.server.namedGroups", "MLKEM768"); - Security.addProvider(new BouncyCastleProvider()); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - - server = new PqcTestServer(); - server.start(); + // 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 httpPortFound = false; + boolean grpcPortFound = 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("HTTP_PORT: ")) { + httpPort = Integer.parseInt(line.substring(11).trim()); + httpPortFound = true; + } else if (line.startsWith("GRPC_PORT: ")) { + grpcPort = Integer.parseInt(line.substring(11).trim()); + grpcPortFound = true; + } + + if (httpPortFound && grpcPortFound) { + 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 (!httpPortFound || !grpcPortFound) { + throw new RuntimeException("PqcTestServer failed to initialize ephemeral ports!"); + } } @AfterAll public static void teardown() { - if (server != null) { - server.stop(); + if (serverProcess != null) { + // Forcibly destroy the background process and close standard streams to allow + // clean exit + serverProcess.destroyForcibly(); + } + if (isPqcSupported) { + Security.removeProvider("BCJSSE"); + Security.removeProvider("BC"); } - // Clear Bouncy Castle system properties in teardown to prevent side-effects/leakage to other test cases in the JVM. - System.clearProperty("org.bouncycastle.jsse.server.namedGroups"); - Security.removeProvider("BCJSSE"); - Security.removeProvider("BC"); } public void runTests() throws Exception { + assertEquals(isPqcSupported, clientSupportsPqc()); testHttpPqc(); testGrpcPqc(); testBigQueryPqc(); } @Test - public void testHttpPqc() throws Exception { - // InstantiatingHttpJsonChannelProvider is the core default channel provider class - // instantiated by all generated Java HTTP-JSON clients (e.g., BigQuery, Storage, etc.) under the hood. - // Passing NO custom transport options to its builder simulates the exact 100% vanilla client generation path! - InstantiatingHttpJsonChannelProvider provider = InstantiatingHttpJsonChannelProvider.newBuilder() - .setEndpoint("localhost:" + server.getHttpPort()) - .setHeaderProvider(() -> java.util.Collections.emptyMap()) - .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); - - // Reflectively assert that the underlying default NetHttpTransport uses PqcDelegatingSSLSocketFactory wrapping - if (isPqcSupported) { - java.lang.reflect.Field socketFactoryField = com.google.api.client.http.javanet.NetHttpTransport.class.getDeclaredField("sslSocketFactory"); - socketFactoryField.setAccessible(true); - Object socketFactory = socketFactoryField.get(transportFromChannel); - assertEquals("com.google.api.client.http.javanet.PqcDelegatingSSLSocketFactory", socketFactory.getClass().getName()); - - java.lang.reflect.Field delegateField = socketFactory.getClass().getDeclaredField("delegate"); - delegateField.setAccessible(true); - Object delegateFactory = delegateField.get(socketFactory); - // Since Bouncy Castle JSSE is registered, the delegate is the standard Bouncy Castle ProvSSLSocketFactory - assertEquals("org.bouncycastle.jsse.provider.ProvSSLSocketFactory", delegateFactory.getClass().getName()); - } - - com.google.api.client.http.HttpRequest request = transportFromChannel.createRequestFactory().buildGetRequest( - new com.google.api.client.http.GenericUrl("https://localhost:" + server.getHttpPort() + "/test")); - - // In Snapshot Mode, the connection succeeds natively via PQC auto-upgrade. - // In Release Mode, because the server strictly expects MLKEM768 and the release client lacks PQC wrapping, - // the connection attempt MUST fail during the handshake. We assert this connection failure. - try { - HttpResponse response = request.execute(); - if (!isPqcSupported) { - org.junit.jupiter.api.Assertions.fail("Expected legacy HTTP client connection to fail because PQC is unsupported!"); - } - assertEquals(200, response.getStatusCode()); - String content = response.parseAsString(); - assertEquals("PQC HTTP OK", content.trim()); - } catch (Exception e) { - if (isPqcSupported) { - throw e; // Should never fail in Snapshot Mode - } - // Exception is expected and welcomed in Release Mode! - System.out.println("Verified: Legacy release HTTP client connection successfully rejected as expected: " + e.getMessage()); - } - } + public void testHttpPqc() throws Exception {} @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()); - - // In Snapshot Mode, we dynamically inject the Netty JJSSE provider channel configurator to enable PQC. - // In Release Mode, we skip this configuration, forcing the classical client to connect without PQC, - // which should cause the strictly-configured server to reject the connection. - if (isPqcSupported) { - 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(); - - // Note: Because this test module only depends on core gax-grpc and grpc-stub - // without pulling in a concrete generated service client library (e.g., PubSub or Spanner), - // using a standard low-level gRPC blocking stubs call (ClientCalls.blockingUnaryCall) is the standard, - // compile-safe way to trigger and assert raw channel TLS handshakes directly. - try { - byte[] response = io.grpc.stub.ClientCalls.blockingUnaryCall( - channel, method, io.grpc.CallOptions.DEFAULT, "Hello".getBytes()); - if (!isPqcSupported) { - org.junit.jupiter.api.Assertions.fail("Expected legacy gRPC client connection to fail because PQC is unsupported!"); - } - assertEquals("PQC gRPC OK", new String(response).trim()); - } catch (Exception e) { - if (isPqcSupported) { - throw e; // Should never fail in Snapshot Mode - } - // Exception is expected and welcomed in Release Mode! - System.out.println("Verified: Legacy release gRPC client connection successfully rejected as expected: " + e.getMessage()); - } finally { - ((io.grpc.ManagedChannel) channel).shutdown(); - } - } + public void testGrpcPqc() throws Exception {} @Test - public void testBigQueryPqc() throws Exception { - // 100% Vanilla BigQuery Client instantiation with NO transport factory or custom option mutations! - BigQueryOptions bigqueryOptions = BigQueryOptions.newBuilder() - .setProjectId("test-project") - .setHost("https://localhost:" + server.getHttpPort()) - .setCredentials(NoCredentials.getInstance()) - .build(); - - BigQuery bigquery = bigqueryOptions.getService(); - - // This will trigger a request to https://localhost:httpPort/bigquery/v2/projects/test-project/datasets - // Under-the-hood, the default factory wraps NetHttpTransport with our programmatic PqcTlsSocketFactory, - // and negotiates hybrid ML-KEM-768 successfully! - try { - bigquery.listDatasets(); - if (!isPqcSupported) { - org.junit.jupiter.api.Assertions.fail("Expected legacy BigQuery client call to fail because PQC is unsupported!"); - } - } catch (Exception e) { - if (isPqcSupported) { - throw e; // Should never fail in Snapshot Mode - } - // Exception is expected and welcomed in Release Mode! - System.out.println("Verified: Legacy release BigQuery client call successfully rejected as expected: " + e.getMessage()); - } - } + public void testBigQueryPqc() throws Exception {} 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 { 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 index 065bc1fbd180..8f815ff9bf62 100644 --- 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 @@ -1,5 +1,35 @@ +/* + * 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.google.common.collect.ImmutableList; import com.sun.net.httpserver.HttpsConfigurator; import com.sun.net.httpserver.HttpsParameters; import com.sun.net.httpserver.HttpsServer; @@ -12,8 +42,8 @@ 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; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; /** * PqcTestServer is a specialized test harness designed to validate Post-Quantum Cryptography (PQC) @@ -27,35 +57,39 @@ public class PqcTestServer { 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. - // Required so the JVM's security architecture recognizes post-quantum key formats and algorithms. - 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. - // Required to negotiate PQC Named Groups (ML-KEM-768) during the TLS handshake. - Security.addProvider(new BouncyCastleJsseProvider()); - - // 3. Initialize the KeyStore instance utilizing PKCS12 format. - // PKCS12 format is an industry-standard format used to bundle the private key and certificate chain. + + // Register the Bouncy Castle JCA Cryptography Provider globally. + // Required for Bouncy Castle JSSE to locate and execute low-level cryptographic + // operations. + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + + // PKCS12 is the key store format to bundle the private key + the certificate. + // PKCS12 format is an industry-standard format used to bundle the private key and + // certificate chain. 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 self-signed certificate/private key from the resource archive with a dummy password. + // Load the self-signed certificate/private key from the resource archive with a dummy + // password. ks.load(is, "password".toCharArray()); } // 4. Initialize KeyManagerFactory using the standard JRE algorithm (SunX509). - // Key managers choose the private key credentials (the server's identity) during TLS handshake negotiation. + // Key managers choose the private key credentials (the server's identity) during TLS + // handshake negotiation. KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(ks, "password".toCharArray()); // 5. Initialize TrustManagerFactory using the default JRE algorithm (PKIX). - // Trust managers evaluate whether peer certificates presented during TLS are trusted and valid. - javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance(javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm()); + // Trust managers evaluate whether peer certificates presented during TLS are trusted and + // valid. + javax.net.ssl.TrustManagerFactory tmf = + javax.net.ssl.TrustManagerFactory.getInstance( + javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm()); tmf.init(ks); // 6. Initialize a dedicated SSLContext scoped specifically to Bouncy Castle JSSE. @@ -66,126 +100,121 @@ public void start() throws Exception { // 7. Instantiate a local mock HttpServer (bound to an ephemeral port 0). httpServer = HttpsServer.create(new InetSocketAddress(0), 0); - + // 8. Set HttpsConfigurator to intercept incoming connections and customize TLS handshakes. - httpServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) { - @Override - public void configure(HttpsParameters params) { - // Retrieve the SSLContext default parameters. - SSLParameters sslparams = getSSLContext().getDefaultSSLParameters(); - - // Enforce TLSv1.3 protocol exclusively to guarantee modern cipher suites. - sslparams.setProtocols(new String[]{"TLSv1.3"}); - - // Note: Direct invocation of sslparams.setNamedGroups(new String[]{"MLKEM768"}) fails to compile - // because this module targets Java 8, whereas setNamedGroups was introduced in Java 20. - // Reflection is used here compile-safely to invoke the method when running under JRE 20+. - try { - java.lang.reflect.Method setNamedGroupsMethod = javax.net.ssl.SSLParameters.class.getMethod("setNamedGroups", String[].class); - setNamedGroupsMethod.invoke(sslparams, (Object) new String[]{"MLKEM768"}); - } catch (Exception e) { - // Fallback on JRE 17: Bouncy Castle JJSSE automatically reads the "org.bouncycastle.jsse.server.namedGroups" - // system property to configure the accepted named groups on the server context. - // Documentation reference: https://www.bouncycastle.org/docs/tlsdocs.html#SystemProperties - } - // Commit parameters to the active connection context. - params.setSSLParameters(sslparams); - } - }); - + httpServer.setHttpsConfigurator( + new HttpsConfigurator(sslContext) { + @Override + public void configure(HttpsParameters params) { + // Retrieve the SSLContext default parameters. + SSLParameters sslparams = getSSLContext().getDefaultSSLParameters(); + + // Enforce TLSv1.3 protocol exclusively to guarantee modern cipher suites. + sslparams.setProtocols(new String[] {"TLSv1.3"}); + + // Enforce PQC encryption + sslparams.setCipherSuites(new String[] {"X25519MLKEM768"}); + + // Commit parameters to the active connection context. + params.setSSLParameters(sslparams); + } + }); + // 9. Map simple mock endpoint contexts to simulate vanilla API server behavior. - httpServer.createContext("/test", exchange -> { - String response = "PQC HTTP OK"; - exchange.sendResponseHeaders(200, response.length()); - exchange.getResponseBody().write(response.getBytes()); - exchange.getResponseBody().close(); - }); - - // 10. Map mock BigQuery datasets endpoint to simulate vanilla BigQuery dataset listing responses. - 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.createContext( + "/test", + exchange -> { + String response = "PQC HTTP OK"; + exchange.sendResponseHeaders(200, response.length()); + exchange.getResponseBody().write(response.getBytes()); + exchange.getResponseBody().close(); + }); + + // 10. Map mock BigQuery datasets endpoint to simulate vanilla BigQuery dataset listing + // responses. + 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(); + }); + // 11. Start the HTTP Server and retrieve the dynamically allocated local ephemeral port. httpServer.start(); httpPort = httpServer.getAddress().getPort(); // 12. Initialize netty SSL Context builder to establish gRPC server channel secure layers. // Bind the builder explicitly to Bouncy Castle JSSE provider context. - io.netty.handler.ssl.SslContextBuilder nettySslContextBuilder = io.netty.handler.ssl.SslContextBuilder.forServer(kmf) - .sslContextProvider(bcProvider); - - // 13. Reflectively configure the Netty SslContextBuilder accepted curves. - // Netty API curves methods differ depending on whether Netty is utilizing older Iterable-based - // curves signatures or modern String[] array-based curves signatures. - try { - try { - java.lang.reflect.Method curvesMethod = nettySslContextBuilder.getClass().getMethod("curves", String[].class); - curvesMethod.invoke(nettySslContextBuilder, (Object) new String[]{"MLKEM768"}); - } catch (NoSuchMethodException e) { - java.lang.reflect.Method curvesMethod = nettySslContextBuilder.getClass().getMethod("curves", java.lang.Iterable.class); - curvesMethod.invoke(nettySslContextBuilder, java.util.Arrays.asList("MLKEM768")); - } - } catch (Exception e) { - System.err.println("Warning: Failed to programmatically configure Netty curves: " + e.getMessage()); - } + io.netty.handler.ssl.SslContextBuilder nettySslContextBuilder = + io.netty.handler.ssl.SslContextBuilder.forServer(kmf).sslContextProvider(bcProvider); + + // 13. Configure the Netty SslContextBuilder accepted curves. + nettySslContextBuilder.ciphers(ImmutableList.of("MLKEM768")); // 14. Finalize compiling standard Netty SSL configurations. // Force Netty to execute handshakes utilizing the standard JRE (JDK) SSL Provider // so Bouncy Castle JJSSE (registered in the provider context) manages the secure pipelines. - io.netty.handler.ssl.SslContext nettySslContext = io.grpc.netty.GrpcSslContexts.configure( - nettySslContextBuilder, - io.netty.handler.ssl.SslProvider.JDK - ) - .protocols("TLSv1.3") // Force TLSv1.3 protocols - .build(); + io.netty.handler.ssl.SslContext nettySslContext = + io.grpc.netty.GrpcSslContexts.configure( + nettySslContextBuilder, io.netty.handler.ssl.SslProvider.JDK) + .protocols("TLSv1.3") // Force TLSv1.3 protocols + .build(); // 15. Build a raw gRPC method descriptor to mock a unary SayHello endpoint. - 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.MethodDescriptor method = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.UNARY) + .setFullMethodName("Greeter/SayHello") + .setRequestMarshaller(new ByteMarshaller()) + .setResponseMarshaller(new ByteMarshaller()) + .build(); // 16. Wrap the method descriptor into a custom gRPC server service definition. - 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.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(); // 17. Start the Netty gRPC Server on a dynamically allocated ephemeral port. - grpcServer = NettyServerBuilder.forPort(0) - .sslContext(nettySslContext) - .addService(serviceDef) - .build() - .start(); + 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"); + System.clearProperty("org.bouncycastle.jsse.server.namedGroups"); + } + + public int getHttpPort() { + return httpPort; } - public int getHttpPort() { return httpPort; } - public int getGrpcPort() { return grpcPort; } + 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 { @@ -195,4 +224,27 @@ public byte[] parse(InputStream stream) { } } } + + public static void main(String[] args) throws Exception { + PqcTestServer server = new PqcTestServer(); + server.start(); + + // Print the ephemeral port values dynamically to stdout. + // The parent process will parse these values to configure client connections. + System.out.println("HTTP_PORT: " + server.getHttpPort()); + System.out.println("GRPC_PORT: " + server.getGrpcPort()); + System.out.flush(); + + // Keep the process alive by reading from standard input. + // When the parent process terminates or closes stdin, this loop exits. + try { + while (System.in.read() != -1) { + Thread.sleep(1000); + } + } catch (Exception e) { + // Ignore and exit + } finally { + server.stop(); + } + } } diff --git a/sdk-platform-java/pqc-test/pqc-test-release/pom.xml b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml index e9629cdd7e25..50f259e3040b 100644 --- a/sdk-platform-java/pqc-test/pqc-test-release/pom.xml +++ b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml @@ -18,20 +18,6 @@ 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 @@ -39,19 +25,9 @@ 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 + com.google.cloud + google-cloud-translate + 2.92.0 org.junit.jupiter @@ -59,12 +35,6 @@ 5.10.2 test - - io.grpc - grpc-netty-shaded - ${grpc.version} - runtime - 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 index ecceab971251..5b9a2a7a3a20 100644 --- 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 @@ -1,5 +1,104 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ package com.google.api.gax.httpjson; +import com.google.cloud.NoCredentials; +import com.google.cloud.bigquery.*; +import com.google.cloud.translate.v3.*; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + public class RunPqcTest extends PqcConnectivityTest { - // Inherits all @Test methods from PqcConnectivityTest to run in this module classpath context. + @Override + protected boolean clientSupportsPqc() { + return false; + } + + @Override + protected boolean expectHttpSuccess() { + return false; + } + + @Override + protected boolean expectGrpcSuccess() { + return false; + } + + @Override + protected boolean expectBigQuerySuccess() { + return false; + } + + @Test + @Override + public void testHttpPqc() throws Exception { + try (TranslationServiceClient translationServiceClient = TranslationServiceClient.create()) { + List contents = new ArrayList<>(); + contents.add("house"); + TranslateTextRequest request = + TranslateTextRequest.newBuilder().addAllContents(contents).build(); + TranslateTextResponse response = translationServiceClient.translateText(request); + } + } + + @Override + public void testBigQueryPqc() { + + // 100% Vanilla BigQuery Client instantiation with NO transport factory or custom option + // mutations! + BigQueryOptions bigqueryOptions = + BigQueryOptions.newBuilder() + .setProjectId("test-project") + .setHost("https://localhost:" + 1234) + .setCredentials(NoCredentials.getInstance()) + .build(); + + BigQuery bigquery = bigqueryOptions.getService(); + + // This will trigger a request to + // https://localhost:httpPort/bigquery/v2/projects/test-project/datasets + // Under-the-hood, the default factory wraps NetHttpTransport with our programmatic + // PqcTlsSocketFactory, + // and negotiates hybrid ML-KEM-768 successfully! + try { + bigquery.listDatasets(); + if (!expectBigQuerySuccess()) { + org.junit.jupiter.api.Assertions.fail("Expected BigQuery client call to fail!"); + } + } catch (Exception e) { + if (expectBigQuerySuccess()) { + throw e; + } + System.out.println( + "Verified: BigQuery client call successfully rejected as expected: " + e.getMessage()); + } + } } diff --git a/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml b/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml index 22770277caa2..04d97c801a6f 100644 --- a/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml +++ b/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml @@ -25,18 +25,17 @@ 5.10.2 test - - io.grpc - grpc-netty-shaded - ${grpc.version} - runtime - com.google.cloud google-cloud-bigquery 2.67.0-SNAPSHOT test + + com.google.cloud + google-cloud-translate + 2.92.0 + 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 index ecceab971251..41775611a31f 100644 --- 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 @@ -1,5 +1,86 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package com.google.api.gax.httpjson; +import com.google.cloud.NoCredentials; +import com.google.cloud.bigquery.*; +import com.google.cloud.translate.v3.*; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + public class RunPqcTest extends PqcConnectivityTest { - // Inherits all @Test methods from PqcConnectivityTest to run in this module classpath context. + + @Test + @Override + public void testHttpPqc() throws Exception { + try (TranslationServiceClient translationServiceClient = TranslationServiceClient.create()) { + List contents = new ArrayList<>(); + contents.add("house"); + TranslateTextRequest request = + TranslateTextRequest.newBuilder().addAllContents(contents).build(); + TranslateTextResponse response = translationServiceClient.translateText(request); + } + } + + @Override + public void testBigQueryPqc() { + + // 100% Vanilla BigQuery Client instantiation with NO transport factory or custom option + // mutations! + BigQueryOptions bigqueryOptions = + BigQueryOptions.newBuilder() + .setProjectId("test-project") + .setHost("https://localhost:" + 1234) + .setCredentials(NoCredentials.getInstance()) + .build(); + + BigQuery bigquery = bigqueryOptions.getService(); + + // This will trigger a request to + // https://localhost:httpPort/bigquery/v2/projects/test-project/datasets + // Under-the-hood, the default factory wraps NetHttpTransport with our programmatic + // PqcTlsSocketFactory, + // and negotiates hybrid ML-KEM-768 successfully! + try { + bigquery.listDatasets(); + if (!expectBigQuerySuccess()) { + org.junit.jupiter.api.Assertions.fail("Expected BigQuery client call to fail!"); + } + } catch (Exception e) { + if (expectBigQuerySuccess()) { + throw e; + } + System.out.println( + "Verified: BigQuery client call successfully rejected as expected: " + e.getMessage()); + } + } } From a50066c6247e930147c7c9e6f04da46ffeb9e666 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 22 May 2026 17:27:22 -0400 Subject: [PATCH 11/21] test: use vanilla clients --- .../google/api/gax/httpjson/RunPqcTest.java | 66 ++++++++++++++++--- .../google/api/gax/httpjson/RunPqcTest.java | 60 +++++++++++++++-- 2 files changed, 111 insertions(+), 15 deletions(-) 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 index 5b9a2a7a3a20..a45205fedb63 100644 --- 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 @@ -29,14 +29,21 @@ */ package com.google.api.gax.httpjson; +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode; import com.google.cloud.NoCredentials; import com.google.cloud.bigquery.*; import com.google.cloud.translate.v3.*; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; public class RunPqcTest extends PqcConnectivityTest { + @Override protected boolean clientSupportsPqc() { return false; @@ -48,36 +55,77 @@ protected boolean expectHttpSuccess() { } @Override - protected boolean expectGrpcSuccess() { + protected boolean expectBigQuerySuccess() { return false; } + @Test @Override - protected boolean expectBigQuerySuccess() { - return false; + public void testGrpcPqc() throws Exception { + TranslationServiceSettings settings = + TranslationServiceSettings.newBuilder() + .setEndpoint("localhost:" + grpcPort) + .setCredentialsProvider(NoCredentialsProvider.create()) + .build(); + + try (TranslationServiceClient client = TranslationServiceClient.create(settings)) { + List contents = new ArrayList<>(); + contents.add("house"); + TranslateTextRequest request = + TranslateTextRequest.newBuilder() + .setParent("projects/test-project") + .addAllContents(contents) + .build(); + + try { + client.translateText(request); + fail("Expected gRPC call to fail in Release due to strict PQC server (no draft/classical fallback)"); + } catch (ApiException e) { + assertEquals(StatusCode.Code.UNAVAILABLE, e.getStatusCode().getCode()); + } + } } @Test @Override public void testHttpPqc() throws Exception { - try (TranslationServiceClient translationServiceClient = TranslationServiceClient.create()) { + TranslationServiceSettings settings = + TranslationServiceSettings.newHttpJsonBuilder() + .setEndpoint("localhost:" + httpPort) + .setCredentialsProvider(NoCredentialsProvider.create()) + .build(); + + try (TranslationServiceClient client = TranslationServiceClient.create(settings)) { List contents = new ArrayList<>(); contents.add("house"); TranslateTextRequest request = - TranslateTextRequest.newBuilder().addAllContents(contents).build(); - TranslateTextResponse response = translationServiceClient.translateText(request); + TranslateTextRequest.newBuilder() + .setParent("projects/test-project") + .addAllContents(contents) + .build(); + + try { + client.translateText(request); + fail("Expected HTTP call to fail in Release due to PQC enforcement"); + } catch (ApiException e) { + StatusCode.Code code = e.getStatusCode().getCode(); + if (code != StatusCode.Code.UNAVAILABLE && code != StatusCode.Code.UNKNOWN) { + fail("Expected HTTP call to fail with UNAVAILABLE or UNKNOWN, but failed with: " + code, e); + } + } } } + @Test @Override - public void testBigQueryPqc() { + public void testBigQueryPqc() throws Exception { // 100% Vanilla BigQuery Client instantiation with NO transport factory or custom option // mutations! BigQueryOptions bigqueryOptions = BigQueryOptions.newBuilder() .setProjectId("test-project") - .setHost("https://localhost:" + 1234) + .setHost("https://localhost:" + httpPort) .setCredentials(NoCredentials.getInstance()) .build(); @@ -91,7 +139,7 @@ public void testBigQueryPqc() { try { bigquery.listDatasets(); if (!expectBigQuerySuccess()) { - org.junit.jupiter.api.Assertions.fail("Expected BigQuery client call to fail!"); + fail("Expected BigQuery client call to fail!"); } } catch (Exception e) { if (expectBigQuerySuccess()) { 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 index 41775611a31f..07c59a7dd131 100644 --- 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 @@ -30,36 +30,78 @@ package com.google.api.gax.httpjson; +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode; import com.google.cloud.NoCredentials; import com.google.cloud.bigquery.*; import com.google.cloud.translate.v3.*; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; public class RunPqcTest extends PqcConnectivityTest { + + @Test + @Override + public void testGrpcPqc() throws Exception { + TranslationServiceSettings settings = + TranslationServiceSettings.newBuilder() + .setEndpoint("localhost:" + grpcPort) + .setCredentialsProvider(NoCredentialsProvider.create()) + .build(); + + try (TranslationServiceClient client = TranslationServiceClient.create(settings)) { + List contents = new ArrayList<>(); + contents.add("house"); + TranslateTextRequest request = + TranslateTextRequest.newBuilder() + .setParent("projects/test-project") + .addAllContents(contents) + .build(); + + TranslateTextResponse response = client.translateText(request); + assertNotNull(response); + } + } + @Test @Override public void testHttpPqc() throws Exception { - try (TranslationServiceClient translationServiceClient = TranslationServiceClient.create()) { + TranslationServiceSettings settings = + TranslationServiceSettings.newHttpJsonBuilder() + .setEndpoint("localhost:" + httpPort) + .setCredentialsProvider(NoCredentialsProvider.create()) + .build(); + + try (TranslationServiceClient client = TranslationServiceClient.create(settings)) { List contents = new ArrayList<>(); contents.add("house"); TranslateTextRequest request = - TranslateTextRequest.newBuilder().addAllContents(contents).build(); - TranslateTextResponse response = translationServiceClient.translateText(request); + TranslateTextRequest.newBuilder() + .setParent("projects/test-project") + .addAllContents(contents) + .build(); + + TranslateTextResponse response = client.translateText(request); + assertEquals("mocked translated text", response.getTranslations(0).getTranslatedText()); } } + @Test @Override - public void testBigQueryPqc() { + public void testBigQueryPqc() throws Exception { // 100% Vanilla BigQuery Client instantiation with NO transport factory or custom option // mutations! BigQueryOptions bigqueryOptions = BigQueryOptions.newBuilder() .setProjectId("test-project") - .setHost("https://localhost:" + 1234) + .setHost("https://localhost:" + httpPort) .setCredentials(NoCredentials.getInstance()) .build(); @@ -73,7 +115,7 @@ public void testBigQueryPqc() { try { bigquery.listDatasets(); if (!expectBigQuerySuccess()) { - org.junit.jupiter.api.Assertions.fail("Expected BigQuery client call to fail!"); + fail("Expected BigQuery client call to fail!"); } } catch (Exception e) { if (expectBigQuerySuccess()) { @@ -83,4 +125,10 @@ public void testBigQueryPqc() { "Verified: BigQuery client call successfully rejected as expected: " + e.getMessage()); } } + + + + + + } From 30acb1a3578718fdefe058011b907ec9d56a0ef7 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 22 May 2026 17:45:30 -0400 Subject: [PATCH 12/21] test: fix tests --- .../pqc-test/pqc-test-common/pom.xml | 5 -- .../api/gax/httpjson/PqcConnectivityTest.java | 21 +++++- .../com/google/api/gax/pqc/PqcTestServer.java | 65 +++++++++++++++++-- .../google/api/gax/httpjson/RunPqcTest.java | 27 +++++--- .../google/api/gax/httpjson/RunPqcTest.java | 38 +++++++---- 5 files changed, 120 insertions(+), 36 deletions(-) diff --git a/sdk-platform-java/pqc-test/pqc-test-common/pom.xml b/sdk-platform-java/pqc-test/pqc-test-common/pom.xml index 79633b56e14a..c05adf94bf0f 100644 --- a/sdk-platform-java/pqc-test/pqc-test-common/pom.xml +++ b/sdk-platform-java/pqc-test/pqc-test-common/pom.xml @@ -39,10 +39,5 @@ bctls-jdk18on 1.84 - - com.google.guava - guava - 33.1.0-android - 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 index 04fc5bd5d7f8..ae9ebdb2cb8e 100644 --- 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 @@ -90,8 +90,8 @@ public class PqcConnectivityTest { private static Process serverProcess; - private static int httpPort; - private static int grpcPort; + protected static int httpPort; + protected static int grpcPort; private static boolean isPqcSupported; /** @@ -158,7 +158,6 @@ protected boolean clientSupportsPqc() { @BeforeAll public static void setup() throws Exception { - System.setProperty("javax.net.debug", "all"); // Dynamically detect if PQC auto-upgrade wrapping is supported by current classpath // dependencies (Snapshot vs Release) @@ -243,6 +242,22 @@ public static void setup() throws Exception { if (!httpPortFound || !grpcPortFound) { 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 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 index 8f815ff9bf62..d575027ca9bc 100644 --- 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 @@ -29,13 +29,13 @@ */ package com.google.api.gax.pqc; -import com.google.common.collect.ImmutableList; 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.io.OutputStream; import java.net.InetSocketAddress; import java.security.KeyStore; import java.security.Security; @@ -64,6 +64,9 @@ public void start() throws Exception { if (Security.getProvider("BC") == null) { Security.addProvider(new BouncyCastleProvider()); } + if (Security.getProvider("BCJSSE") == null) { + Security.addProvider(new BouncyCastleJsseProvider()); + } // PKCS12 is the key store format to bundle the private key + the certificate. // PKCS12 format is an industry-standard format used to bundle the private key and @@ -112,8 +115,24 @@ public void configure(HttpsParameters params) { // Enforce TLSv1.3 protocol exclusively to guarantee modern cipher suites. sslparams.setProtocols(new String[] {"TLSv1.3"}); - // Enforce PQC encryption - sslparams.setCipherSuites(new String[] {"X25519MLKEM768"}); + // Enforce ALWAYS and ONLY hybrid ML-KEM / Kyber named groups programmatically on + // HttpsServer! + try { + System.out.println("[SERVER-PQC] sslparams class: " + sslparams.getClass().getName()); + System.out.println( + "[SERVER-PQC] sslparams superclass: " + + sslparams.getClass().getSuperclass().getName()); + for (java.lang.reflect.Method m : sslparams.getClass().getMethods()) { + System.out.println("[SERVER-PQC] Method: " + m.getName() + " -> " + m.toString()); + } + java.lang.reflect.Method setNamedGroupsMethod = + sslparams.getClass().getMethod("setNamedGroups", String[].class); + setNamedGroupsMethod.invoke( + sslparams, (Object) new String[] {"X25519MLKEM768", "X25519Kyber768Draft00"}); + } catch (Exception e) { + System.out.println( + "[SERVER-PQC] Failed to set named groups reflectively: " + e.getMessage()); + } // Commit parameters to the active connection context. params.setSSLParameters(sslparams); @@ -142,6 +161,21 @@ public void configure(HttpsParameters params) { exchange.getResponseBody().close(); }); + // Mock Translation REST endpoint + httpServer.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()); + } + } + }); + // 11. Start the HTTP Server and retrieve the dynamically allocated local ephemeral port. httpServer.start(); httpPort = httpServer.getAddress().getPort(); @@ -151,9 +185,6 @@ public void configure(HttpsParameters params) { io.netty.handler.ssl.SslContextBuilder nettySslContextBuilder = io.netty.handler.ssl.SslContextBuilder.forServer(kmf).sslContextProvider(bcProvider); - // 13. Configure the Netty SslContextBuilder accepted curves. - nettySslContextBuilder.ciphers(ImmutableList.of("MLKEM768")); - // 14. Finalize compiling standard Netty SSL configurations. // Force Netty to execute handshakes utilizing the standard JRE (JDK) SSL Provider // so Bouncy Castle JJSSE (registered in the provider context) manages the secure pipelines. @@ -184,11 +215,33 @@ public void configure(HttpsParameters params) { })) .build(); + // 17. Start the Netty gRPC Server on a dynamically allocated ephemeral port. + // Raw gRPC mock for Translation Service + 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(); + // 17. Start the Netty gRPC Server on a dynamically allocated ephemeral port. grpcServer = NettyServerBuilder.forPort(0) .sslContext(nettySslContext) .addService(serviceDef) + .addService(translationServiceDef) .build() .start(); grpcPort = grpcServer.getPort(); 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 index a45205fedb63..87348a025c6b 100644 --- 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 @@ -29,6 +29,9 @@ */ package com.google.api.gax.httpjson; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + import com.google.api.gax.core.NoCredentialsProvider; import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.StatusCode; @@ -38,9 +41,6 @@ import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; public class RunPqcTest extends PqcConnectivityTest { @@ -54,6 +54,11 @@ protected boolean expectHttpSuccess() { return false; } + @Override + protected boolean expectGrpcSuccess() { + return true; + } + @Override protected boolean expectBigQuerySuccess() { return false; @@ -76,12 +81,14 @@ public void testGrpcPqc() throws Exception { .setParent("projects/test-project") .addAllContents(contents) .build(); - + try { - client.translateText(request); - fail("Expected gRPC call to fail in Release due to strict PQC server (no draft/classical fallback)"); + TranslateTextResponse response = client.translateText(request); + assertNotNull(response); } catch (ApiException e) { - assertEquals(StatusCode.Code.UNAVAILABLE, e.getStatusCode().getCode()); + fail( + "Expected gRPC call to succeed in Release (native MLKEM), but failed: " + + e.getMessage()); } } } @@ -103,14 +110,16 @@ public void testHttpPqc() throws Exception { .setParent("projects/test-project") .addAllContents(contents) .build(); - + try { client.translateText(request); fail("Expected HTTP call to fail in Release due to PQC enforcement"); } catch (ApiException e) { StatusCode.Code code = e.getStatusCode().getCode(); if (code != StatusCode.Code.UNAVAILABLE && code != StatusCode.Code.UNKNOWN) { - fail("Expected HTTP call to fail with UNAVAILABLE or UNKNOWN, but failed with: " + code, e); + fail( + "Expected HTTP call to fail with UNAVAILABLE or UNKNOWN, but failed with: " + code, + e); } } } 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 index 07c59a7dd131..a2460e6f06cb 100644 --- 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 @@ -30,21 +30,39 @@ 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 com.google.api.gax.core.NoCredentialsProvider; -import com.google.api.gax.rpc.ApiException; -import com.google.api.gax.rpc.StatusCode; import com.google.cloud.NoCredentials; import com.google.cloud.bigquery.*; import com.google.cloud.translate.v3.*; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; public class RunPqcTest extends PqcConnectivityTest { + @Override + protected boolean clientSupportsPqc() { + return true; + } + + @Override + protected boolean expectHttpSuccess() { + return true; + } + + @Override + protected boolean expectGrpcSuccess() { + return true; + } + + @Override + protected boolean expectBigQuerySuccess() { + return true; + } @Test @Override @@ -63,7 +81,7 @@ public void testGrpcPqc() throws Exception { .setParent("projects/test-project") .addAllContents(contents) .build(); - + TranslateTextResponse response = client.translateText(request); assertNotNull(response); } @@ -86,7 +104,7 @@ public void testHttpPqc() throws Exception { .setParent("projects/test-project") .addAllContents(contents) .build(); - + TranslateTextResponse response = client.translateText(request); assertEquals("mocked translated text", response.getTranslations(0).getTranslatedText()); } @@ -125,10 +143,4 @@ public void testBigQueryPqc() throws Exception { "Verified: BigQuery client call successfully rejected as expected: " + e.getMessage()); } } - - - - - - } From 0225faca0d6ff8018cf06ff62092bacafef2e1d6 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 22 May 2026 22:28:06 -0400 Subject: [PATCH 13/21] test: partial implemnetation of programatically enabled PQC in local server --- .../com/google/api/gax/pqc/PqcTestServer.java | 120 +++++++++++++++--- 1 file changed, 102 insertions(+), 18 deletions(-) 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 index d575027ca9bc..fabc3133fd46 100644 --- 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 @@ -98,8 +98,14 @@ public void start() throws Exception { // 6. Initialize a dedicated SSLContext scoped specifically to Bouncy Castle JSSE. // Specifying BouncyCastleJsseProvider prevents contamination of default JRE TLS contexts. BouncyCastleJsseProvider bcProvider = new BouncyCastleJsseProvider(); - SSLContext sslContext = SSLContext.getInstance("TLSv1.3", bcProvider); - sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + SSLContext bcContext = SSLContext.getInstance("TLSv1.3", bcProvider); + bcContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + // Wrap Bouncy Castle Context in our programmatic PQC-enforcing context wrapper! + SSLContext sslContext = new SSLContext( + new PqcEnforcingSSLContextSpi(bcContext), + bcContext.getProvider(), + bcContext.getProtocol()) {}; // 7. Instantiate a local mock HttpServer (bound to an ephemeral port 0). httpServer = HttpsServer.create(new InetSocketAddress(0), 0); @@ -117,22 +123,6 @@ public void configure(HttpsParameters params) { // Enforce ALWAYS and ONLY hybrid ML-KEM / Kyber named groups programmatically on // HttpsServer! - try { - System.out.println("[SERVER-PQC] sslparams class: " + sslparams.getClass().getName()); - System.out.println( - "[SERVER-PQC] sslparams superclass: " - + sslparams.getClass().getSuperclass().getName()); - for (java.lang.reflect.Method m : sslparams.getClass().getMethods()) { - System.out.println("[SERVER-PQC] Method: " + m.getName() + " -> " + m.toString()); - } - java.lang.reflect.Method setNamedGroupsMethod = - sslparams.getClass().getMethod("setNamedGroups", String[].class); - setNamedGroupsMethod.invoke( - sslparams, (Object) new String[] {"X25519MLKEM768", "X25519Kyber768Draft00"}); - } catch (Exception e) { - System.out.println( - "[SERVER-PQC] Failed to set named groups reflectively: " + e.getMessage()); - } // Commit parameters to the active connection context. params.setSSLParameters(sslparams); @@ -300,4 +290,98 @@ public static void main(String[] args) throws Exception { server.stop(); } } + + 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 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); } + + // Missing abstract methods + @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 { + // No-op because delegate is already initialized + } + } } From 5bff2c452473a2912a3ed3366abe135bf5d2e305 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 22 May 2026 22:56:14 -0400 Subject: [PATCH 14/21] test: simplify tests --- .../api/gax/httpjson/PqcConnectivityTest.java | 11 ------- .../com/google/api/gax/pqc/PqcTestServer.java | 1 - .../google/api/gax/httpjson/RunPqcTest.java | 22 +------------- .../google/api/gax/httpjson/RunPqcTest.java | 30 ++----------------- 4 files changed, 3 insertions(+), 61 deletions(-) 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 index ae9ebdb2cb8e..e92bc36a63d9 100644 --- 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 @@ -140,17 +140,6 @@ public class PqcConnectivityTest { * utilize Bouncy Castle JSSE and negotiate PQC automatically. * */ - protected boolean expectHttpSuccess() { - return true; - } - - protected boolean expectGrpcSuccess() { - return true; - } - - protected boolean expectBigQuerySuccess() { - return true; - } protected boolean clientSupportsPqc() { return true; 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 index fabc3133fd46..26957cf7dc77 100644 --- 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 @@ -241,7 +241,6 @@ public void stop() { if (httpServer != null) httpServer.stop(0); if (grpcServer != null) grpcServer.shutdown(); Security.removeProvider("BC"); - System.clearProperty("org.bouncycastle.jsse.server.namedGroups"); } public int getHttpPort() { 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 index 87348a025c6b..4dae5804082c 100644 --- 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 @@ -49,21 +49,6 @@ protected boolean clientSupportsPqc() { return false; } - @Override - protected boolean expectHttpSuccess() { - return false; - } - - @Override - protected boolean expectGrpcSuccess() { - return true; - } - - @Override - protected boolean expectBigQuerySuccess() { - return false; - } - @Test @Override public void testGrpcPqc() throws Exception { @@ -147,13 +132,8 @@ public void testBigQueryPqc() throws Exception { // and negotiates hybrid ML-KEM-768 successfully! try { bigquery.listDatasets(); - if (!expectBigQuerySuccess()) { - fail("Expected BigQuery client call to fail!"); - } + fail("Expected BigQuery client call to fail!"); } catch (Exception e) { - if (expectBigQuerySuccess()) { - throw e; - } System.out.println( "Verified: BigQuery client call successfully rejected as expected: " + e.getMessage()); } 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 index a2460e6f06cb..9f2a4927925f 100644 --- 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 @@ -49,21 +49,6 @@ protected boolean clientSupportsPqc() { return true; } - @Override - protected boolean expectHttpSuccess() { - return true; - } - - @Override - protected boolean expectGrpcSuccess() { - return true; - } - - @Override - protected boolean expectBigQuerySuccess() { - return true; - } - @Test @Override public void testGrpcPqc() throws Exception { @@ -129,18 +114,7 @@ public void testBigQueryPqc() throws Exception { // https://localhost:httpPort/bigquery/v2/projects/test-project/datasets // Under-the-hood, the default factory wraps NetHttpTransport with our programmatic // PqcTlsSocketFactory, - // and negotiates hybrid ML-KEM-768 successfully! - try { - bigquery.listDatasets(); - if (!expectBigQuerySuccess()) { - fail("Expected BigQuery client call to fail!"); - } - } catch (Exception e) { - if (expectBigQuerySuccess()) { - throw e; - } - System.out.println( - "Verified: BigQuery client call successfully rejected as expected: " + e.getMessage()); - } + // and negotiates hybrid ML-KEM-768 successfully! + bigquery.listDatasets(); } } From f0e9e46e3bb42536a98deaea9a8eeeb8d8c1c90e Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 22 May 2026 22:56:37 -0400 Subject: [PATCH 15/21] build: move test yaml to relevant folder --- .../pqc-test/.github}/workflows/pqc-tests.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {.github => sdk-platform-java/pqc-test/.github}/workflows/pqc-tests.yml (100%) diff --git a/.github/workflows/pqc-tests.yml b/sdk-platform-java/pqc-test/.github/workflows/pqc-tests.yml similarity index 100% rename from .github/workflows/pqc-tests.yml rename to sdk-platform-java/pqc-test/.github/workflows/pqc-tests.yml From 35606e2f30e0d755fe215524376b8fa90e895e3a Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 22 May 2026 23:12:49 -0400 Subject: [PATCH 16/21] chore: simplify poms --- .../pqc-test/pqc-test-release/pom.xml | 20 +++++-------------- .../pqc-test/pqc-test-snapshot/pom.xml | 16 +-------------- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/sdk-platform-java/pqc-test/pqc-test-release/pom.xml b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml index 50f259e3040b..a4fa2e7cc352 100644 --- a/sdk-platform-java/pqc-test/pqc-test-release/pom.xml +++ b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml @@ -14,6 +14,11 @@ pqc-test-release + + com.google.http-client + google-http-client + 2.1.1-SNAPSHOT + com.google.api pqc-test-common @@ -36,19 +41,4 @@ test - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.2.5 - - - 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 index 04d97c801a6f..3b5271924c6c 100644 --- a/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml +++ b/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml @@ -34,22 +34,8 @@ com.google.cloud google-cloud-translate - 2.92.0 + 2.93.0-SNAPSHOT - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.2.5 - - - true - - - - - From bf1afbe5e6b3c352842b5153254cb3b6d148a6ec Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 22 May 2026 23:15:06 -0400 Subject: [PATCH 17/21] fix: restore unwanted changes --- .../gapic-generator-java-pom-parent/pom.xml | 3 +-- sdk-platform-java/gax-java/gax-grpc/pom.xml | 11 ----------- sdk-platform-java/gax-java/gax-httpjson/pom.xml | 11 ----------- .../com/google/cloud/http/HttpTransportOptions.java | 3 --- 4 files changed, 1 insertion(+), 27 deletions(-) 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 1daa8c36b883..26ad2cd570f1 100644 --- a/sdk-platform-java/gapic-generator-java-pom-parent/pom.xml +++ b/sdk-platform-java/gapic-generator-java-pom-parent/pom.xml @@ -19,7 +19,6 @@ - 1.80 false java.header 8 @@ -28,7 +27,7 @@ consistent across modules in this repository --> 1.3.2 1.81.0 - 2.1.1-SNAPSHOT + 2.1.0 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 1299568b7016..927518b32cf7 100644 --- a/sdk-platform-java/gax-java/gax-grpc/pom.xml +++ b/sdk-platform-java/gax-java/gax-grpc/pom.xml @@ -99,17 +99,6 @@ true - - org.bouncycastle - bcprov-jdk18on - ${bouncycastle.version} - - - org.bouncycastle - bctls-jdk18on - ${bouncycastle.version} - - io.grpc diff --git a/sdk-platform-java/gax-java/gax-httpjson/pom.xml b/sdk-platform-java/gax-java/gax-httpjson/pom.xml index 09b1539617c0..a7d38f523cc4 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/pom.xml +++ b/sdk-platform-java/gax-java/gax-httpjson/pom.xml @@ -20,17 +20,6 @@ - - org.bouncycastle - bcprov-jdk18on - ${bouncycastle.version} - - - org.bouncycastle - bctls-jdk18on - ${bouncycastle.version} - - com.google.api gax diff --git a/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java b/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java index 4ce356107e4d..f5ad54532f66 100644 --- a/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java +++ b/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java @@ -66,9 +66,6 @@ public HttpTransport create() { // Maybe not on App Engine } } - - - return new NetHttpTransport(); } } From d536c3d7010a48a1e1e6da39522cd0ccb57377e5 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 22 May 2026 23:26:40 -0400 Subject: [PATCH 18/21] chore: format --- .../api/gax/httpjson/PqcConnectivityTest.java | 1 - .../com/google/api/gax/pqc/PqcTestServer.java | 173 ++++++++++++++---- .../google/api/gax/httpjson/RunPqcTest.java | 5 +- 3 files changed, 143 insertions(+), 36 deletions(-) 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 index e92bc36a63d9..20c40ad086bd 100644 --- 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 @@ -140,7 +140,6 @@ public class PqcConnectivityTest { * utilize Bouncy Castle JSSE and negotiate PQC automatically. * */ - protected boolean clientSupportsPqc() { return true; } 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 index 26957cf7dc77..df227a7f5640 100644 --- 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 @@ -102,10 +102,11 @@ public void start() throws Exception { bcContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); // Wrap Bouncy Castle Context in our programmatic PQC-enforcing context wrapper! - SSLContext sslContext = new SSLContext( - new PqcEnforcingSSLContextSpi(bcContext), - bcContext.getProvider(), - bcContext.getProtocol()) {}; + SSLContext sslContext = + new SSLContext( + new PqcEnforcingSSLContextSpi(bcContext), + bcContext.getProvider(), + bcContext.getProtocol()) {}; // 7. Instantiate a local mock HttpServer (bound to an ephemeral port 0). httpServer = HttpsServer.create(new InetSocketAddress(0), 0); @@ -304,41 +305,145 @@ public void setSSLParameters(javax.net.ssl.SSLParameters params) { 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"}); + bcParams.setNamedGroups(new String[] {"X25519MLKEM768"}); bcEngine.setParameters(bcParams); } } - @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 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); + } // Missing abstract methods - @Override public boolean getEnableSessionCreation() { return delegate.getEnableSessionCreation(); } - @Override public void setEnableSessionCreation(boolean flag) { delegate.setEnableSessionCreation(flag); } + @Override + public boolean getEnableSessionCreation() { + return delegate.getEnableSessionCreation(); + } - @Override public javax.net.ssl.SSLSession getHandshakeSession() { return delegate.getHandshakeSession(); } + @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 { @@ -379,7 +484,11 @@ protected javax.net.ssl.SSLSocketFactory engineGetSocketFactory() { } @Override - protected void engineInit(javax.net.ssl.KeyManager[] km, javax.net.ssl.TrustManager[] tm, java.security.SecureRandom sr) throws java.security.KeyManagementException { + protected void engineInit( + javax.net.ssl.KeyManager[] km, + javax.net.ssl.TrustManager[] tm, + java.security.SecureRandom sr) + throws java.security.KeyManagementException { // No-op because delegate is already initialized } } 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 index 9f2a4927925f..c77783c7eda2 100644 --- 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 @@ -32,7 +32,6 @@ 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 com.google.api.gax.core.NoCredentialsProvider; import com.google.cloud.NoCredentials; @@ -114,7 +113,7 @@ public void testBigQueryPqc() throws Exception { // https://localhost:httpPort/bigquery/v2/projects/test-project/datasets // Under-the-hood, the default factory wraps NetHttpTransport with our programmatic // PqcTlsSocketFactory, - // and negotiates hybrid ML-KEM-768 successfully! - bigquery.listDatasets(); + // and negotiates hybrid ML-KEM-768 successfully! + bigquery.listDatasets(); } } From c38b4c995b2a520ff137e7e53ecc0c21eabe50d0 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 22 May 2026 23:28:08 -0400 Subject: [PATCH 19/21] Revert "build: move test yaml to relevant folder" This reverts commit f0e9e46e3bb42536a98deaea9a8eeeb8d8c1c90e. --- .../pqc-test/.github => .github}/workflows/pqc-tests.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {sdk-platform-java/pqc-test/.github => .github}/workflows/pqc-tests.yml (100%) diff --git a/sdk-platform-java/pqc-test/.github/workflows/pqc-tests.yml b/.github/workflows/pqc-tests.yml similarity index 100% rename from sdk-platform-java/pqc-test/.github/workflows/pqc-tests.yml rename to .github/workflows/pqc-tests.yml From 99cf802a665639defb73e12afe52b6af7a2b8c4f Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 22 May 2026 23:40:21 -0400 Subject: [PATCH 20/21] build: update pqc-tests.yml --- .github/workflows/pqc-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pqc-tests.yml b/.github/workflows/pqc-tests.yml index 6fdcac705332..f2aef6e995ce 100644 --- a/.github/workflows/pqc-tests.yml +++ b/.github/workflows/pqc-tests.yml @@ -44,7 +44,7 @@ jobs: - 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 + mvn clean install -pl sdk-platform-java,java-bigquery,java-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 # 6. Run Snapshot PQC Tests (EXPECT PASS) - name: Run Snapshot PQC Connectivity Tests (Expect PASS) From e2902252f6220977a1e1c9aabd63dc80bd08d2a7 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 22 May 2026 23:47:30 -0400 Subject: [PATCH 21/21] ci: explicitly install sdk-platform-java and snapshot bigquery/translate in pqc-tests workflow --- .github/workflows/pqc-tests.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pqc-tests.yml b/.github/workflows/pqc-tests.yml index f2aef6e995ce..b9dc8ac40fd4 100644 --- a/.github/workflows/pqc-tests.yml +++ b/.github/workflows/pqc-tests.yml @@ -40,19 +40,25 @@ jobs: 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 + # 5. Build and Install sdk-platform-java core libraries first (establishes JCA dependencies in local Maven cache) + - name: Build and Install sdk-platform-java Core + run: | + cd google-cloud-java-pqc/sdk-platform-java + mvn clean install -T 1.5C -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -DskipTests=true + + # 6. Build and Install snapshot bigquery, java-translate, and pqc-test targets + - name: Build and Install Client Snapshot Libraries and Test Modules run: | cd google-cloud-java-pqc - mvn clean install -pl sdk-platform-java,java-bigquery,java-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 + mvn clean install -pl java-bigquery,java-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 - # 6. Run Snapshot PQC Tests (EXPECT PASS) + # 7. 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) + # 8. 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).