diff --git a/.github/workflows/pqc-tests.yml b/.github/workflows/pqc-tests.yml new file mode 100644 index 000000000000..b9dc8ac40fd4 --- /dev/null +++ b/.github/workflows/pqc-tests.yml @@ -0,0 +1,73 @@ +name: PQC Connectivity Integration Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + pqc-tests: + runs-on: ubuntu-latest + + steps: + # 1. Checkout sibling HTTP Client repository + - name: Checkout google-http-java-client + uses: actions/checkout@v4 + with: + repository: googleapis/google-http-java-client + ref: chore/pqc-poc-2 + path: google-http-java-client + + # 2. Checkout this monorepo + - name: Checkout google-cloud-java-pqc + uses: actions/checkout@v4 + with: + path: google-cloud-java-pqc + + # 3. Set up JDK 17 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' + cache-dependency-path: 'google-cloud-java-pqc/pom.xml' + + # 4. Build and install modified google-http-client SNAPSHOT locally + - name: Build and Install google-http-java-client + run: | + cd google-http-java-client + mvn clean install -DskipTests=true -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip + + # 5. Build and Install sdk-platform-java core libraries first (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 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 + + # 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 + + # 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). + 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/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..1fe163c6f16e --- /dev/null +++ b/sdk-platform-java/pqc-test/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + + com.google.cloud + google-cloud-shared-config + 1.17.0 + + + com.google.api + pqc-test-parent + pom + 2.81.0-SNAPSHOT + + + pqc-test-common + pqc-test-snapshot + pqc-test-release + + 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..c05adf94bf0f --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-common/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + com.google.api + pqc-test-parent + 2.81.0-SNAPSHOT + ../pom.xml + + + pqc-test-common + + + + org.junit.jupiter + junit-jupiter-api + 5.10.2 + + + io.grpc + grpc-netty + 1.81.0 + + + io.grpc + grpc-stub + 1.81.0 + + + org.bouncycastle + bcprov-jdk18on + 1.84 + + + org.bouncycastle + bctls-jdk18on + 1.84 + + + 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..20c40ad086bd --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/httpjson/PqcConnectivityTest.java @@ -0,0 +1,295 @@ +/* + * 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 java.io.InputStream; +import java.security.Security; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * 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]
+ * 
+ * + * + */ +public class PqcConnectivityTest { + + private static Process serverProcess; + protected static int httpPort; + protected static int grpcPort; + 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. Configures JVM standard truststore system properties (javax.net.ssl.trustStore + * ) to point to the extracted certificate, enabling clean default SSLContext + * verification. + *
  3. Inspects the runtime classpath to determine if PQC wrapper auto-upgrades are active. + *
  4. If PQC is supported, registers BouncyCastleJsseProvider at position 1. This + * automatically causes all standard vanilla clients instantiating default SSLContext + * to negotiate PQC. + *
  5. 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: + * + *

+ */ + protected boolean clientSupportsPqc() { + return true; + } + + @BeforeAll + public static void setup() throws Exception { + + // 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 = 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. + 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. + // 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"); + + // 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!"); + } + + // Start a background thread to continuously drain the server's stdout + Thread drainThread = + new Thread( + () -> { + try { + String l; + while ((l = reader.readLine()) != null) { + System.out.println("[SERVER-OUT] " + l); + } + } catch (java.io.IOException e) { + // Ignore stream closed + } + }); + drainThread.setDaemon(true); + drainThread.start(); + } + + @AfterAll + public static void teardown() { + if (serverProcess != null) { + // Forcibly destroy the background process and close standard streams to allow + // clean exit + serverProcess.destroyForcibly(); + } + if (isPqcSupported) { + Security.removeProvider("BCJSSE"); + Security.removeProvider("BC"); + } + } + + public void runTests() throws Exception { + assertEquals(isPqcSupported, clientSupportsPqc()); + testHttpPqc(); + testGrpcPqc(); + testBigQueryPqc(); + } + + @Test + public void testHttpPqc() throws Exception {} + + @Test + public void testGrpcPqc() throws Exception {} + + @Test + 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 { + 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..df227a7f5640 --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/pqc/PqcTestServer.java @@ -0,0 +1,495 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.pqc; + +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsParameters; +import com.sun.net.httpserver.HttpsServer; +import io.grpc.Server; +import io.grpc.netty.NettyServerBuilder; +import java.io.InputStream; +import java.io.OutputStream; +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.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; + +/** + * PqcTestServer is a specialized test harness designed to validate Post-Quantum Cryptography (PQC) + * transport enforcement in the Google Cloud Java SDK. + */ +public class PqcTestServer { + + private HttpsServer httpServer; + private Server grpcServer; + private int httpPort; + private int grpcPort; + + public void start() throws Exception { + + // 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()); + } + 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 + // 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. + 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. + 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()); + tmf.init(ks); + + // 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 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); + + // 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"}); + + // Enforce ALWAYS and ONLY hybrid ML-KEM / Kyber named groups programmatically on + // HttpsServer! + + // 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(); + }); + + // 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(); + + // 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); + + // 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(); + + // 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(); + + // 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(); + + // 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(); + } + + public void stop() { + if (httpServer != null) httpServer.stop(0); + if (grpcServer != null) grpcServer.shutdown(); + 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); + } + } + } + + 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(); + } + } + + 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 + } + } +} diff --git a/sdk-platform-java/pqc-test/pqc-test-common/src/main/resources/pqctest.p12 b/sdk-platform-java/pqc-test/pqc-test-common/src/main/resources/pqctest.p12 new file mode 100644 index 000000000000..92c74c66d3f0 Binary files /dev/null and b/sdk-platform-java/pqc-test/pqc-test-common/src/main/resources/pqctest.p12 differ diff --git a/sdk-platform-java/pqc-test/pqc-test-release/pom.xml b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml new file mode 100644 index 000000000000..a4fa2e7cc352 --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-release/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + + com.google.api + pqc-test-parent + 2.81.0-SNAPSHOT + ../pom.xml + + + pqc-test-release + + + + com.google.http-client + google-http-client + 2.1.1-SNAPSHOT + + + com.google.api + pqc-test-common + 2.81.0-SNAPSHOT + + + com.google.cloud + google-cloud-bigquery + 2.66.0 + + + com.google.cloud + google-cloud-translate + 2.92.0 + + + org.junit.jupiter + junit-jupiter-engine + 5.10.2 + test + + + diff --git a/sdk-platform-java/pqc-test/pqc-test-release/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java b/sdk-platform-java/pqc-test/pqc-test-release/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java new file mode 100644 index 000000000000..4dae5804082c --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-release/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java @@ -0,0 +1,141 @@ +/* + * 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.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; + +public class RunPqcTest extends PqcConnectivityTest { + + @Override + protected boolean clientSupportsPqc() { + return false; + } + + @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(); + + try { + TranslateTextResponse response = client.translateText(request); + assertNotNull(response); + } catch (ApiException e) { + fail( + "Expected gRPC call to succeed in Release (native MLKEM), but failed: " + + e.getMessage()); + } + } + } + + @Test + @Override + public void testHttpPqc() throws Exception { + 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() + .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() 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:" + httpPort) + .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(); + fail("Expected BigQuery client call to fail!"); + } catch (Exception 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 new file mode 100644 index 000000000000..3b5271924c6c --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + com.google.api + pqc-test-parent + 2.81.0-SNAPSHOT + ../pom.xml + + + pqc-test-snapshot + + + + com.google.api + pqc-test-common + 2.81.0-SNAPSHOT + + + org.junit.jupiter + junit-jupiter-engine + 5.10.2 + test + + + com.google.cloud + google-cloud-bigquery + 2.67.0-SNAPSHOT + test + + + com.google.cloud + google-cloud-translate + 2.93.0-SNAPSHOT + + + + diff --git a/sdk-platform-java/pqc-test/pqc-test-snapshot/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java b/sdk-platform-java/pqc-test/pqc-test-snapshot/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java new file mode 100644 index 000000000000..c77783c7eda2 --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-snapshot/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.httpjson; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.google.api.gax.core.NoCredentialsProvider; +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 { + + @Override + protected boolean clientSupportsPqc() { + return true; + } + + @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 { + 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() + .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() 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:" + httpPort) + .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! + bigquery.listDatasets(); + } +}