diff --git a/.github/workflows/pqc-tests.yml b/.github/workflows/pqc-tests.yml new file mode 100644 index 000000000000..364340a3d2c7 --- /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: + # 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 (using monorepo root reactor to resolve siblings) + - name: Build and Install sdk-platform-java Core + run: | + cd google-cloud-java-pqc + mvn clean install -pl :gax,:gax-grpc,:gax-httpjson,:google-cloud-shared-dependencies,:first-party-dependencies,:third-party-dependencies -am -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -DskipTests=true + + # 6. Build and Install snapshot bigquery, java-translate, and pqc-test targets (specifying actual sub-modules) + - name: Build and Install Client Snapshot Libraries and Test Modules + run: | + cd google-cloud-java-pqc + mvn clean install -pl java-bigquery/google-cloud-bigquery,java-translate/google-cloud-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 + mvn install -pl sdk-platform-java/pqc-test/pqc-test-snapshot -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -Dtest=RunPqcTest + + # 8. Run Release PQC Tests (Expect PASS because tests assert negative behavior and pass) + - name: Run Release PQC Connectivity Tests + run: | + cd google-cloud-java-pqc + mvn install -pl sdk-platform-java/pqc-test/pqc-test-release -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -Dtest=RunPqcTest diff --git a/sdk-platform-java/gax-java/gax-grpc/pom.xml b/sdk-platform-java/gax-java/gax-grpc/pom.xml index 927518b32cf7..da457fdb5762 100644 --- a/sdk-platform-java/gax-java/gax-grpc/pom.xml +++ b/sdk-platform-java/gax-java/gax-grpc/pom.xml @@ -70,7 +70,6 @@ io.grpc grpc-netty-shaded - runtime io.grpc @@ -131,6 +130,16 @@ com.google.protobuf protobuf-java + + org.bouncycastle + bcprov-jdk18on + 1.84 + + + org.bouncycastle + bctls-jdk18on + 1.84 + 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..c359cdddfe8b 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 @@ -82,6 +82,11 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslProvider; import javax.annotation.Nullable; import javax.net.ssl.KeyManagerFactory; @@ -812,6 +817,8 @@ public ManagedChannelBuilder createDecoratedChannelBuilder() throws IOExcepti if (interceptorProvider != null) { builder.intercept(interceptorProvider.getInterceptors()); } + configurePqc(builder); + if (channelConfigurator != null) { builder = channelConfigurator.apply(builder); } @@ -829,6 +836,34 @@ private ManagedChannel createSingleChannel() throws IOException { return managedChannel; } + private void configurePqc(ManagedChannelBuilder builder) { + NettyChannelBuilder nettyBuilder = (NettyChannelBuilder) builder; + try { + ApplicationProtocolConfig apn = + new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + "h2"); + + java.security.Provider bcProvider = new org.bouncycastle.jce.provider.BouncyCastleProvider(); + java.security.Provider bcJsseProvider = + new org.bouncycastle.jsse.provider.BouncyCastleJsseProvider(bcProvider); + + SslContext shadedSslContext = + SslContextBuilder.forClient() + .sslProvider(SslProvider.JDK) + .sslContextProvider(bcJsseProvider) + .protocols("TLSv1.3") + .applicationProtocolConfig(apn) + .build(); + + nettyBuilder.sslContext(shadedSslContext); + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to configure shaded gRPC Netty channel for PQC", e); + } + } + /* Remove provided headers that will also get set by {@link com.google.auth.ApiKeyCredentials}. They will be added as part of the grpc call when performing auth * {@link io.grpc.auth.GoogleAuthLibraryCallCredentials#applyRequestMetadata}. GRPC does not dedup headers {@link https://github.com/grpc/grpc-java/blob/a140e1bb0cfa662bcdb7823d73320eb8d49046f1/api/src/main/java/io/grpc/Metadata.java#L504} so we must before initiating the call. * 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..7f0b833adcd4 --- /dev/null +++ b/sdk-platform-java/pqc-test/pom.xml @@ -0,0 +1,39 @@ + + + 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 + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + -Djavax.net.ssl.trustStore=${project.basedir}/../pqc-test-common/target/classes/pqctest.p12 + -Djavax.net.ssl.trustStorePassword=password + -Djavax.net.ssl.trustStoreType=PKCS12 + + + + + + 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..927bf757003e --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-common/pom.xml @@ -0,0 +1,73 @@ + + + 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 + + + com.google.api + gax + 2.81.0-SNAPSHOT + + + com.google.api + gax-httpjson + 2.81.0-SNAPSHOT + + + com.google.api + gax-grpc + 2.81.0-SNAPSHOT + + + com.google.http-client + google-http-client + 2.1.1-SNAPSHOT + + + com.google.cloud + google-cloud-bigquery + 2.67.0-SNAPSHOT + + + com.google.cloud + google-cloud-translate + 2.93.0-SNAPSHOT + + + 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..16baefb249c8 --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/httpjson/PqcConnectivityTest.java @@ -0,0 +1,198 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.fail; + +import java.io.InputStream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public abstract class PqcConnectivityTest { + + private static Process serverProcess; + protected static int grpcPqcPort; + protected static int grpcClassicalPort; + + protected boolean clientSupportsPqc() { + return true; + } + + protected abstract boolean grpcTestShouldSucceed(); + + @BeforeAll + public static void setup() throws Exception { + + // 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 grpcPqcFound = false; + boolean grpcClassicalFound = 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("GRPC_PQC_PORT: ")) { + grpcPqcPort = Integer.parseInt(line.substring(15).trim()); + grpcPqcFound = true; + } else if (line.startsWith("GRPC_CLASSICAL_PORT: ")) { + grpcClassicalPort = Integer.parseInt(line.substring(21).trim()); + grpcClassicalFound = true; + } + + if (grpcPqcFound && grpcClassicalFound) { + 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 (!grpcPqcFound || !grpcClassicalFound) { + 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(); + } + } + + private void runGrpcTest(int port, boolean shouldSucceed) throws Exception { + com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.Builder channelProviderBuilder = + com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.newBuilder() + .setEndpoint("localhost:" + port) + .setHeaderProvider(() -> java.util.Collections.emptyMap()); + + + com.google.api.gax.grpc.InstantiatingGrpcChannelProvider channelProvider = channelProviderBuilder.build(); + com.google.api.gax.rpc.TransportChannel transportChannel = channelProvider.getTransportChannel(); + io.grpc.Channel channel = ((com.google.api.gax.grpc.GrpcTransportChannel) transportChannel).getChannel(); + + try { + io.grpc.MethodDescriptor method = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.UNARY) + .setFullMethodName("Greeter/SayHello") + .setRequestMarshaller(new ByteMarshaller()) + .setResponseMarshaller(new ByteMarshaller()) + .build(); + + byte[] response = io.grpc.stub.ClientCalls.blockingUnaryCall(channel, method, io.grpc.CallOptions.DEFAULT, "request".getBytes()); + if (!shouldSucceed) { + fail("Expected gRPC call to fail!"); + } + assertNotNull(response); + assertEquals("PQC gRPC OK", new String(response)); + } catch (Exception e) { + if (shouldSucceed) { + fail("Expected gRPC call to succeed, but failed: " + e.getMessage(), e); + } + } finally { + if (channel instanceof io.grpc.ManagedChannel) { + ((io.grpc.ManagedChannel) channel).shutdownNow(); + } + } + } + + + + @Test + public void testGrpcPqcServerEnforced() throws Exception { + runGrpcTest(grpcPqcPort, grpcTestShouldSucceed()); + } + + @Test + public void testGrpcClassicalServer() throws Exception { + runGrpcTest(grpcClassicalPort, true); + } + + 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..5008213bcc3c --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/pqc/PqcTestServer.java @@ -0,0 +1,505 @@ +/* + * 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 io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.IdentityCipherSuiteFilter; +import io.netty.handler.ssl.JdkSslContext; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.security.KeyStore; +import java.util.List; +import java.util.function.BiFunction; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +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 httpServerPqc; + private HttpsServer httpServerClassical; + private Server grpcServerPqc; + private Server grpcServerClassical; + private int httpPqcPort; + private int httpClassicalPort; + private int grpcPqcPort; + private int grpcClassicalPort; + + public void start() throws Exception { + + KeyStore ks = KeyStore.getInstance("PKCS12"); + try (InputStream is = getClass().getResourceAsStream("/pqctest.p12")) { + if (is == null) { + throw new RuntimeException("pqctest.p12 not found in classpath"); + } + ks.load(is, "password".toCharArray()); + } + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, "password".toCharArray()); + + javax.net.ssl.TrustManagerFactory tmf = + javax.net.ssl.TrustManagerFactory.getInstance( + javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + + BouncyCastleProvider bcProvider = new BouncyCastleProvider(); + BouncyCastleJsseProvider bcJsseProvider = new BouncyCastleJsseProvider(bcProvider); + SSLContext bcContext = SSLContext.getInstance("TLSv1.3", bcJsseProvider); + bcContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + SSLContext pqcEnforcingSslContext = + new SSLContext( + new PqcEnforcingSSLContextSpi(bcContext), + bcContext.getProvider(), + bcContext.getProtocol()) {}; + + SSLContext classicalSslContext = bcContext; + + httpServerPqc = HttpsServer.create(new InetSocketAddress(0), 0); + configureHttpServer(httpServerPqc, pqcEnforcingSslContext); + httpServerPqc.start(); + httpPqcPort = httpServerPqc.getAddress().getPort(); + + httpServerClassical = HttpsServer.create(new InetSocketAddress(0), 0); + configureHttpServer(httpServerClassical, classicalSslContext); + httpServerClassical.start(); + httpClassicalPort = httpServerClassical.getAddress().getPort(); + + ApplicationProtocolConfig apn = + new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + "h2"); + + io.netty.handler.ssl.SslContext nettySslContextPqc = + new JdkSslContext( + pqcEnforcingSslContext, false, null, IdentityCipherSuiteFilter.INSTANCE, apn, ClientAuth.NONE); + + io.netty.handler.ssl.SslContext nettySslContextClassical = + new JdkSslContext( + classicalSslContext, false, null, IdentityCipherSuiteFilter.INSTANCE, apn, ClientAuth.NONE); + + grpcServerPqc = createGrpcServer(nettySslContextPqc); + grpcServerPqc.start(); + grpcPqcPort = grpcServerPqc.getPort(); + + grpcServerClassical = createGrpcServer(nettySslContextClassical); + grpcServerClassical.start(); + grpcClassicalPort = grpcServerClassical.getPort(); + } + + private void configureHttpServer(HttpsServer server, SSLContext sslContext) { + server.setHttpsConfigurator( + new HttpsConfigurator(sslContext) { + @Override + public void configure(HttpsParameters params) { + SSLParameters sslparams = getSSLContext().getDefaultSSLParameters(); + sslparams.setProtocols(new String[] {"TLSv1.3"}); + params.setSSLParameters(sslparams); + } + }); + + server.createContext( + "/test", + exchange -> { + String response = "PQC HTTP OK"; + exchange.sendResponseHeaders(200, response.length()); + exchange.getResponseBody().write(response.getBytes()); + exchange.getResponseBody().close(); + }); + + server.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(); + }); + + server.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()); + } + } + }); + } + + private Server createGrpcServer(io.netty.handler.ssl.SslContext nettySslContext) { + 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(); + + 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(); + + return NettyServerBuilder.forPort(0) + .sslContext(nettySslContext) + .addService(serviceDef) + .addService(translationServiceDef) + .build(); + } + + public void stop() { + if (httpServerPqc != null) httpServerPqc.stop(0); + if (httpServerClassical != null) httpServerClassical.stop(0); + if (grpcServerPqc != null) grpcServerPqc.shutdown(); + if (grpcServerClassical != null) grpcServerClassical.shutdown(); + } + + public int getHttpPqcPort() { + return httpPqcPort; + } + + public int getHttpClassicalPort() { + return httpClassicalPort; + } + + public int getGrpcPqcPort() { + return grpcPqcPort; + } + + public int getGrpcClassicalPort() { + return grpcClassicalPort; + } + + 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(); + + System.out.println("HTTP_PQC_PORT: " + server.getHttpPqcPort()); + System.out.println("HTTP_CLASSICAL_PORT: " + server.getHttpClassicalPort()); + System.out.println("GRPC_PQC_PORT: " + server.getGrpcPqcPort()); + System.out.println("GRPC_CLASSICAL_PORT: " + server.getGrpcClassicalPort()); + System.out.flush(); + + try { + while (System.in.read() != -1) { + Thread.sleep(1000); + } + } catch (Exception e) { + } finally { + server.stop(); + } + } + + @org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + 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 void setHandshakeApplicationProtocolSelector( + BiFunction, String> selector) { + delegate.setHandshakeApplicationProtocolSelector( + (engine, protocols) -> selector.apply(this, protocols)); + } + + @Override + public BiFunction, String> getHandshakeApplicationProtocolSelector() { + return delegate.getHandshakeApplicationProtocolSelector(); + } + + @Override + public String getApplicationProtocol() { + return delegate.getApplicationProtocol(); + } + + @Override + public String getHandshakeApplicationProtocol() { + return delegate.getHandshakeApplicationProtocol(); + } + + @Override + public javax.net.ssl.SSLParameters getSSLParameters() { + return delegate.getSSLParameters(); + } + + @Override + public void beginHandshake() throws javax.net.ssl.SSLException { + delegate.beginHandshake(); + } + + @Override + public void closeInbound() throws javax.net.ssl.SSLException { + delegate.closeInbound(); + } + + @Override + public void closeOutbound() { + delegate.closeOutbound(); + } + + @Override + public java.lang.Runnable getDelegatedTask() { + return delegate.getDelegatedTask(); + } + + @Override + public java.lang.String[] getEnabledCipherSuites() { + return delegate.getEnabledCipherSuites(); + } + + @Override + public java.lang.String[] getEnabledProtocols() { + return delegate.getEnabledProtocols(); + } + + @Override + public javax.net.ssl.SSLEngineResult.HandshakeStatus getHandshakeStatus() { + return delegate.getHandshakeStatus(); + } + + @Override + public boolean getNeedClientAuth() { + return delegate.getNeedClientAuth(); + } + + @Override + public javax.net.ssl.SSLSession getSession() { + return delegate.getSession(); + } + + @Override + public java.lang.String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public java.lang.String[] getSupportedProtocols() { + return delegate.getSupportedProtocols(); + } + + @Override + public boolean getUseClientMode() { + return delegate.getUseClientMode(); + } + + @Override + public boolean getWantClientAuth() { + return delegate.getWantClientAuth(); + } + + @Override + public boolean isInboundDone() { + return delegate.isInboundDone(); + } + + @Override + public boolean isOutboundDone() { + return delegate.isOutboundDone(); + } + + @Override + public void setEnabledCipherSuites(java.lang.String[] suites) { + delegate.setEnabledCipherSuites(suites); + } + + @Override + public void setEnabledProtocols(java.lang.String[] protocols) { + delegate.setEnabledProtocols(protocols); + } + + @Override + public void setNeedClientAuth(boolean need) { + delegate.setNeedClientAuth(need); + } + + @Override + public void setUseClientMode(boolean mode) { + delegate.setUseClientMode(mode); + } + + @Override + public void setWantClientAuth(boolean want) { + delegate.setWantClientAuth(want); + } + + @Override + public javax.net.ssl.SSLEngineResult unwrap( + java.nio.ByteBuffer src, java.nio.ByteBuffer[] dsts, int offset, int length) + throws javax.net.ssl.SSLException { + return delegate.unwrap(src, dsts, offset, length); + } + + @Override + public javax.net.ssl.SSLEngineResult wrap( + java.nio.ByteBuffer[] srcs, int offset, int length, java.nio.ByteBuffer dst) + throws javax.net.ssl.SSLException { + return delegate.wrap(srcs, offset, length, dst); + } + + @Override + public 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 {} + } +} 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..2ed7e408754c --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-release/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java @@ -0,0 +1,43 @@ +/* + * 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; + +public class RunPqcTest extends PqcConnectivityTest { + + @Override + protected boolean clientSupportsPqc() { + return false; + } + + @Override + protected boolean grpcTestShouldSucceed() { + return false; + } +} diff --git a/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml b/sdk-platform-java/pqc-test/pqc-test-snapshot/pom.xml 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..5454cd4df934 --- /dev/null +++ b/sdk-platform-java/pqc-test/pqc-test-snapshot/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java @@ -0,0 +1,44 @@ +/* + * 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; + +public class RunPqcTest extends PqcConnectivityTest { + + @Override + protected boolean clientSupportsPqc() { + return true; + } + + @Override + protected boolean grpcTestShouldSucceed() { + return true; + } +}