diff --git a/.github/workflows/pqc-tests.yml b/.github/workflows/pqc-tests.yml new file mode 100644 index 000000000000..6fdcac705332 --- /dev/null +++ b/.github/workflows/pqc-tests.yml @@ -0,0 +1,67 @@ +name: PQC Connectivity Integration Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + pqc-tests: + runs-on: ubuntu-latest + + steps: + # 1. Checkout sibling HTTP Client repository + - name: Checkout google-http-java-client + uses: actions/checkout@v4 + with: + repository: googleapis/google-http-java-client + ref: chore/pqc-poc-2 + path: google-http-java-client + + # 2. Checkout this monorepo + - name: Checkout google-cloud-java-pqc + uses: actions/checkout@v4 + with: + path: google-cloud-java-pqc + + # 3. Set up JDK 17 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' + cache-dependency-path: 'google-cloud-java-pqc/pom.xml' + + # 4. Build and install modified google-http-client SNAPSHOT locally + - name: Build and Install google-http-java-client + run: | + cd google-http-java-client + mvn clean install -DskipTests=true -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip + + # 5. Build the entire monorepo core components required by the tests + - name: Build and Install Core Dependency Reactor + run: | + cd google-cloud-java-pqc + mvn clean install -pl sdk-platform-java/pqc-test/pqc-test-snapshot,sdk-platform-java/pqc-test/pqc-test-release -am -T 1.5C -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -DskipTests=true + + # 6. Run Snapshot PQC Tests (EXPECT PASS) + - name: Run Snapshot PQC Connectivity Tests (Expect PASS) + run: | + cd google-cloud-java-pqc/sdk-platform-java/pqc-test/pqc-test-snapshot + mvn install -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -Dtest=RunPqcTest + + # 7. Run Release PQC Tests (EXPECT FAIL) + - name: Run Release PQC Connectivity Tests (Expect FAIL) + # We expect this step to fail. If it passes, it means release libraries are negotiating PQC (which is incorrect). + # Thus we run it and assert that the maven command fails (exit code != 0). + run: | + cd google-cloud-java-pqc/sdk-platform-java/pqc-test/pqc-test-release + if mvn install -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -Dtest=RunPqcTest; then + echo "Error: Release tests passed but they were expected to fail!" + exit 1 + else + echo "Success: Release tests failed-fast as expected." + exit 0 + fi diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java index 643c3dc7dc65..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 000000000000..92c74c66d3f0 Binary files /dev/null and b/sdk-platform-java/pqc-test/pqc-test-common/src/main/resources/pqctest.p12 differ diff --git a/sdk-platform-java/pqc-test/pqc-test-release/pom.xml b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml new file mode 100644 index 000000000000..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. +}