From 64be800b0f2ae3e7f310c868ace562c1b18b81d2 Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 14 May 2026 17:26:36 -0400 Subject: [PATCH 1/3] 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 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 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 2/3] 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 3/3] 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).