diff --git a/.github/workflows/showcase.yaml b/.github/workflows/showcase.yaml index 3325dfadcc3a..b391625878b1 100644 --- a/.github/workflows/showcase.yaml +++ b/.github/workflows/showcase.yaml @@ -40,6 +40,18 @@ jobs: java-version: 11 distribution: temurin cache: maven + - name: Checkout google-http-java-client (pqc-support-conscrypt branch) + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + repository: googleapis/google-http-java-client + ref: pqc-support-conscrypt + path: google-http-java-client + persist-credentials: false + - name: Build and install local google-http-java-client snapshot + working-directory: google-http-java-client + run: | + mvn install -DskipTests -Dmaven.javadoc.skip=true -Dclirr.skip=true -B -ntp + cd .. && rm -rf google-http-java-client - name: Install all modules using Java 11 shell: bash run: .kokoro/build.sh @@ -62,6 +74,8 @@ jobs: cd /usr/src/showcase/ tar -xf showcase-* ./gapic-showcase run & + ./gapic-showcase run --port 7470 --tls --ca-cert-output-file /tmp/showcase-ca.pem & + sleep 2 cd - - name: Showcase integration tests working-directory: java-showcase @@ -118,6 +132,18 @@ jobs: java-version: ${{ matrix.java }} distribution: temurin - run: mvn -version + - name: Checkout google-http-java-client (pqc-support-conscrypt branch) + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + repository: googleapis/google-http-java-client + ref: pqc-support-conscrypt + path: google-http-java-client + persist-credentials: false + - name: Build and install local google-http-java-client snapshot + working-directory: google-http-java-client + run: | + mvn install -DskipTests -Dmaven.javadoc.skip=true -Dclirr.skip=true -B -ntp + cd .. && rm -rf google-http-java-client - name: Install Maven modules shell: bash run: .kokoro/build.sh @@ -167,6 +193,8 @@ jobs: cd /usr/src/showcase/ tar -xf showcase-* ./gapic-showcase run & + ./gapic-showcase run --port 7470 --tls --ca-cert-output-file /tmp/showcase-ca.pem & + sleep 2 cd - - name: Showcase integration tests working-directory: java-showcase @@ -225,6 +253,18 @@ jobs: java-version: 17 distribution: temurin cache: maven + - name: Checkout google-http-java-client (pqc-support-conscrypt branch) + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + repository: googleapis/google-http-java-client + ref: pqc-support-conscrypt + path: google-http-java-client + persist-credentials: false + - name: Build and install local google-http-java-client snapshot + working-directory: google-http-java-client + run: | + mvn install -DskipTests -Dmaven.javadoc.skip=true -Dclirr.skip=true -B -ntp + cd .. && rm -rf google-http-java-client - name: Install Maven modules shell: bash run: .kokoro/build.sh diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 92e714cb451e..f1e81ea43912 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -33,6 +33,8 @@ if [ -f "${KOKORO_GFILE_DIR}/secret_manager/java-bigqueryconnection-samples-secr source "${KOKORO_GFILE_DIR}/secret_manager/java-bigqueryconnection-samples-secrets" fi +install_http_client_snapshot + RETURN_CODE=0 case ${JOB_TYPE} in diff --git a/.kokoro/client-library-check-doclet.sh b/.kokoro/client-library-check-doclet.sh index 3c774c62582f..2d4138b0b8be 100755 --- a/.kokoro/client-library-check-doclet.sh +++ b/.kokoro/client-library-check-doclet.sh @@ -75,6 +75,8 @@ scriptDir=$(realpath $(dirname "${BASH_SOURCE[0]}")) ## cd to the parent directory, i.e. the root of the git repo cd ${scriptDir}/.. +install_http_client_snapshot + # Make artifacts available for 'mvn validate' at the bottom pushd java-shared-config mvn install -DskipTests=true -Dmaven.javadoc.skip=true -Dgcloud.download.skip=true -B -V -q --no-transfer-progress diff --git a/.kokoro/client-library-check.sh b/.kokoro/client-library-check.sh index 0eb9f817bb61..862ef3ce7ddf 100755 --- a/.kokoro/client-library-check.sh +++ b/.kokoro/client-library-check.sh @@ -86,6 +86,8 @@ scriptDir=$(realpath $(dirname "${BASH_SOURCE[0]}")) ## cd to the parent directory, i.e. the root of the git repo cd ${scriptDir}/.. +install_http_client_snapshot + # Make artifacts available for 'mvn validate' at the bottom pushd java-shared-config mvn install -DskipTests=true -Dmaven.javadoc.skip=true -Dgcloud.download.skip=true -B -V -q diff --git a/.kokoro/common.sh b/.kokoro/common.sh index a1dad45d963d..62d3e6626006 100644 --- a/.kokoro/common.sh +++ b/.kokoro/common.sh @@ -46,6 +46,20 @@ excluded_modules=( 'java-iam' ) +function install_http_client_snapshot { + if [ -d "${HOME}/.m2/repository/com/google/http-client/google-http-client-bom/2.1.2-SNAPSHOT" ]; then + echo "google-http-client 2.1.2-SNAPSHOT already exists in local cache." + return 0 + fi + echo "Installing local snapshot of google-http-java-client (pqc-support-conscrypt branch)..." + HTTP_CLIENT_TMP_DIR=$(mktemp -d) + git clone --depth 1 --branch pqc-support-conscrypt https://github.com/googleapis/google-http-java-client.git "${HTTP_CLIENT_TMP_DIR}" + pushd "${HTTP_CLIENT_TMP_DIR}" + mvn install -DskipTests -Dmaven.javadoc.skip=true -Dclirr.skip=true -B -ntp + popd + rm -rf "${HTTP_CLIENT_TMP_DIR}" +} + function retry_with_backoff { attempts_left=$1 sleep_seconds=$2 diff --git a/.kokoro/dependencies.sh b/.kokoro/dependencies.sh index f341016b5033..408d314d6dbd 100755 --- a/.kokoro/dependencies.sh +++ b/.kokoro/dependencies.sh @@ -48,6 +48,8 @@ function determineMavenOpts() { export MAVEN_OPTS=$(determineMavenOpts) +install_http_client_snapshot + if [[ -n "${BUILD_SUBDIR}" ]] then echo "Compiling and building all modules for ${BUILD_SUBDIR}" diff --git a/.kokoro/presubmit/downstream-build.sh b/.kokoro/presubmit/downstream-build.sh index aa6781b1ffee..0ca99b94509e 100755 --- a/.kokoro/presubmit/downstream-build.sh +++ b/.kokoro/presubmit/downstream-build.sh @@ -31,6 +31,9 @@ scriptDir=$(realpath "$(dirname "${BASH_SOURCE[0]}")") ## cd to the parent directory, i.e. the root of the git repo cd "${scriptDir}/../.." +source "${scriptDir}/../common.sh" +install_http_client_snapshot + # Build and install the entire monorepo to local cache (including the under-test java-shared-config) mvn -B -ntp install -Dcheckstyle.skip -Dfmt.skip -DskipTests diff --git a/build-with-local-http-client.sh b/build-with-local-http-client.sh new file mode 100755 index 000000000000..3b39d5f2c5d8 --- /dev/null +++ b/build-with-local-http-client.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set -e + +# Find the directory of this script (root of google-cloud-java) +MONOREPO_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PARENT_DIR="$(cd "${MONOREPO_DIR}/.." && pwd)" + +# Path to the google-http-java-client repository +HTTP_CLIENT_DIR="${HTTP_CLIENT_DIR:-${PARENT_DIR}/google-http-java-client}" +HTTP_CLIENT_BRANCH="${HTTP_CLIENT_BRANCH:-pqc-support-conscrypt}" + + +HTTP_CLIENT_VERSION="${HTTP_CLIENT_VERSION:-2.1.2-SNAPSHOT}" + +echo "=========================================================================" +echo "Building and installing google-http-java-client snapshot..." +echo "Using path: ${HTTP_CLIENT_DIR}" +echo "=========================================================================" + +if [ ! -d "${HTTP_CLIENT_DIR}" ]; then + echo "Error: google-http-java-client directory not found at: ${HTTP_CLIENT_DIR}" + echo "You can specify its location by setting the HTTP_CLIENT_DIR environment variable." + exit 1 +fi + +# Check if the snapshot jar is already built in the local maven repository +M2_JAR_PATH="${HOME}/.m2/repository/com/google/http-client/google-http-client/${HTTP_CLIENT_VERSION}/google-http-client-${HTTP_CLIENT_VERSION}.jar" + +if [ -f "${M2_JAR_PATH}" ] && [ "${FORCE_REBUILD}" != "true" ]; then + echo "Found existing google-http-client snapshot at ${M2_JAR_PATH}." + echo "Skipping build. (To force rebuild, run with FORCE_REBUILD=true)" +else + # Store current directory and build http client + pushd "${HTTP_CLIENT_DIR}" + echo "Switching to branch ${HTTP_CLIENT_BRANCH} in google-http-java-client..." + git checkout "${HTTP_CLIENT_BRANCH}" + + echo "Running maven install..." + mvn clean install -pl google-http-client -am -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dclirr.skip=true + popd +fi + +SHOWCASE_DIR="${SHOWCASE_DIR:-${PARENT_DIR}/gapic-showcase}" +SHOWCASE_BIN="${SHOWCASE_DIR}/gapic-showcase" + +if [ -f "${SHOWCASE_BIN}" ]; then + echo "=========================================================================" + echo "Starting Showcase TLS server in background..." + echo "=========================================================================" + + # Start showcase in Auto-TLS mode on port 7470 + "${SHOWCASE_BIN}" run \ + --port 7470 \ + --tls \ + --ca-cert-output-file /tmp/showcase-ca.pem > showcase-server.log 2>&1 & + SHOWCASE_PID=$! + + # Ensure we kill the background process on script exit + trap "echo 'Stopping Showcase...'; kill ${SHOWCASE_PID} 2>/dev/null || true; wait ${SHOWCASE_PID} 2>/dev/null || true; rm -f /tmp/showcase-ca.pem" EXIT + + # Wait a bit for the server to initialize and write the cert + sleep 2 +else + echo "=========================================================================" + echo "Warning: gapic-showcase binary not found at: ${SHOWCASE_BIN}" + echo "Please ensure Showcase is running manually in TLS mode on port 7470," + echo "and its CA certificate is written to /tmp/showcase-ca.pem." + echo "=========================================================================" +fi + +echo "=========================================================================" +echo "Building and verifying gapic-showcase with PQC in google-cloud-java..." +echo "=========================================================================" + +# Run the showcase tests using the secure endpoint and cert path +mvn test -pl java-showcase/gapic-showcase -Dtest=ITPqc \ + -Dshowcase.secure.endpoint=localhost:7470 \ + -Dshowcase.ca.cert.path=/tmp/showcase-ca.pem diff --git a/google-auth-library-java/pom.xml b/google-auth-library-java/pom.xml index cda5c649a97d..089d11629af8 100644 --- a/google-auth-library-java/pom.xml +++ b/google-auth-library-java/pom.xml @@ -481,4 +481,40 @@ + + + + sonatype-central-snapshots + Sonatype Central Snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + sonatype-snapshots + Sonatype Snapshots + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + sonatype-s01-snapshots + Sonatype S01 Snapshots + https://s01.oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + diff --git a/google-cloud-pom-parent/pom.xml b/google-cloud-pom-parent/pom.xml index 54cc170ea86a..a5a38d8cd501 100644 --- a/google-cloud-pom-parent/pom.xml +++ b/google-cloud-pom-parent/pom.xml @@ -137,6 +137,39 @@ Maven Central https://repo1.maven.org/maven2 + + sonatype-central-snapshots + Sonatype Central Snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + sonatype-snapshots + Sonatype Snapshots + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + sonatype-s01-snapshots + Sonatype S01 Snapshots + https://s01.oss.sonatype.org/content/repositories/snapshots + + false + + + true + + diff --git a/java-showcase/gapic-showcase/pom.xml b/java-showcase/gapic-showcase/pom.xml index e36caea4a8a5..6c2ac99bd52f 100644 --- a/java-showcase/gapic-showcase/pom.xml +++ b/java-showcase/gapic-showcase/pom.xml @@ -78,6 +78,42 @@ + + + sonatype-central-snapshots + Sonatype Central Snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + sonatype-snapshots + Sonatype Snapshots + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + sonatype-s01-snapshots + Sonatype S01 Snapshots + https://s01.oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + io.grpc @@ -136,6 +172,12 @@ + + org.conscrypt + conscrypt-openjdk-uber + 2.6.0 + test + org.junit.jupiter junit-jupiter-engine diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java new file mode 100644 index 000000000000..f83a33bc5ebc --- /dev/null +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java @@ -0,0 +1,426 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.showcase.v1beta1.it; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.util.SslUtils; +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.grpc.GrpcTransportChannel; +import com.google.api.gax.httpjson.HttpJsonMetadata; +import com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider; +import com.google.api.gax.rpc.FixedTransportChannelProvider; +import com.google.api.gax.rpc.TransportChannel; +import com.google.showcase.v1beta1.EchoClient; +import com.google.showcase.v1beta1.EchoRequest; +import com.google.showcase.v1beta1.EchoResponse; +import com.google.showcase.v1beta1.EchoSettings; +import com.google.showcase.v1beta1.it.util.HttpJsonCapturingClientInterceptor; +import io.grpc.Channel; +import io.grpc.ChannelCredentials; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ForwardingClientCallListener; +import io.grpc.Grpc; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.TlsChannelCredentials; +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.Provider; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Integration tests to verify Post-Quantum Cryptography (PQC) TLS negotiation for both gRPC and + * HTTP/JSON (REST) clients. + * + *

These tests execute calls against a local secure (TLS-enabled) Showcase server. During the TLS + * handshake, the client and server negotiate cipher suites and key exchange groups. Showcase + * injects information about the negotiated TLS connection parameters into custom headers: + * + *

+ * + *

To enable PQC, Conscrypt must be available on the classpath. + * + *

+ * + * Consequently, these tests do not explicitly register Conscrypt in the global JVM provider list + * during setup. + * + *

Verification cases: + * + *

    + *
  1. {@code testGrpcPqc}: Verifies that gRPC (Netty-shaded) uses Conscrypt and successfully + * negotiates the hybrid post-quantum group {@code X25519MLKEM768}. + *
  2. {@code testHttpJsonPqc}: Verifies that HTTP/JSON transport defaults to Conscrypt and + * negotiates the hybrid post-quantum group {@code X25519MLKEM768}. + *
  3. {@code testHttpJsonPqc_withExplicitSecurityProvider}: Verifies that overriding the + * transport's SSLSocketFactory to explicitly use standard JDK JSSE provider (SunJSSE) falls + * back gracefully to classical key exchange ({@code X25519}) instead of crashing. + *
+ */ +public class ITPqc { + + // TLS response header names from Showcase server + private static final String TLS_GROUP_HEADER = "x-showcase-tls-group"; + private static final String TLS_CIPHER_HEADER = "x-showcase-tls-cipher"; + private static final String TLS_SUPPORTED_GROUPS_HEADER = + "x-showcase-tls-client-supported-groups"; + + // Expected TLS parameters + private static final String EXPECTED_TLS_GROUP = "X25519MLKEM768"; + + private static final String DEFAULT_CA_CERT_PATH = getCaCertPath(); + + private static String getCaCertPath() { + String prop = System.getProperty("showcase.ca.cert.path"); + if (prop != null) { + return prop; + } + if (new File("/tmp/showcase-ca.pem").isFile()) { + return "/tmp/showcase-ca.pem"; + } + return "target/showcase-ca.pem"; + } + + private static final String SECURE_ENDPOINT = + System.getProperty("showcase.secure.endpoint", "localhost:7470"); + + @BeforeAll + static void setUp() { + File certFile = new File(DEFAULT_CA_CERT_PATH); + assertWithMessage("CA certificate file not found at " + DEFAULT_CA_CERT_PATH) + .that(certFile.isFile()) + .isTrue(); + } + + @Test + void testGrpcPqc() throws Exception { + + // Create channel credentials trusting the custom CA + ChannelCredentials creds = + TlsChannelCredentials.newBuilder().trustManager(new File(DEFAULT_CA_CERT_PATH)).build(); + + ManagedChannel channel = Grpc.newChannelBuilder(SECURE_ENDPOINT, creds).build(); + try { + TransportChannel transportChannel = GrpcTransportChannel.create(channel); + + GrpcHeaderCapturingInterceptor interceptor = new GrpcHeaderCapturingInterceptor(); + + EchoSettings settings = + EchoSettings.newBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider(FixedTransportChannelProvider.create(transportChannel)) + .build(); + + // Add interceptor to capture headers + ManagedChannel interceptedChannel = new InterceptedManagedChannel(channel, interceptor); + TransportChannel interceptedTransportChannel = + GrpcTransportChannel.create(interceptedChannel); + + settings = + settings.toBuilder() + .setTransportChannelProvider( + FixedTransportChannelProvider.create(interceptedTransportChannel)) + .build(); + + try (EchoClient client = EchoClient.create(settings)) { + EchoResponse response = + client.echo(EchoRequest.newBuilder().setContent("pqc-grpc-test").build()); + assertThat(response.getContent()).isEqualTo("pqc-grpc-test"); + + Metadata capturedHeaders = interceptor.getCapturedHeaders(); + assertThat(capturedHeaders).isNotNull(); + + Metadata.Key groupKey = + Metadata.Key.of(TLS_GROUP_HEADER, Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key supportedGroupsKey = + Metadata.Key.of(TLS_SUPPORTED_GROUPS_HEADER, Metadata.ASCII_STRING_MARSHALLER); + + String expectedGroup = isConscryptFunctional() ? "X25519MLKEM768" : "X25519"; + assertThat(capturedHeaders.get(groupKey)).isEqualTo(expectedGroup); + assertThat(capturedHeaders.get(supportedGroupsKey)).isNotNull(); + } + } finally { + channel.shutdown(); + channel.awaitTermination(10, TimeUnit.SECONDS); + } + } + + @Test + void testHttpJsonPqc() throws Exception { + + Provider conscryptProvider = null; + try { + conscryptProvider = org.conscrypt.Conscrypt.newProvider(); + } catch (Throwable t) { + // Conscrypt JNI is not available on this platform/runner + } + + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); + if (conscryptProvider != null) { + builder.setSecurityProvider(conscryptProvider); + } + + SSLContext sslContext = SslUtils.getTlsSslContext(conscryptProvider); + TrustManagerFactory tmf = SslUtils.getDefaultTrustManagerFactory(conscryptProvider); + tmf.init(loadCaCert(DEFAULT_CA_CERT_PATH)); + sslContext.init(null, tmf.getTrustManagers(), null); + builder.setSslSocketFactory(sslContext.getSocketFactory()); + + if (conscryptProvider != null) { + com.google.api.gax.httpjson.ConscryptPqcConfiguratorHelper.configure(builder); + } else { + builder.setSslSocketConfigurator( + new com.google.api.client.http.javanet.JreNamedGroupsSslSocketConfigurator( + new String[] {"X25519"})); + } + + NetHttpTransport transport = builder.build(); + + HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor(); + + InstantiatingHttpJsonChannelProvider transportChannelProvider = + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport(transport) + .setEndpoint("https://" + SECURE_ENDPOINT) + .setInterceptorProvider(() -> Collections.singletonList(interceptor)) + .build(); + + EchoSettings settings = + EchoSettings.newHttpJsonBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider(transportChannelProvider) + .build(); + + try (EchoClient client = EchoClient.create(settings)) { + EchoResponse response = + client.echo(EchoRequest.newBuilder().setContent("pqc-httpjson-test").build()); + assertThat(response.getContent()).isEqualTo("pqc-httpjson-test"); + + HttpJsonMetadata capturedHeaders = interceptor.metadata; + assertThat(capturedHeaders).isNotNull(); + + String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER); + String expectedGroup = isConscryptFunctional() ? "X25519MLKEM768" : "X25519"; + assertThat(negotiatedGroup).isEqualTo(expectedGroup); + + String supportedGroups = getSingleHeaderString(capturedHeaders, TLS_SUPPORTED_GROUPS_HEADER); + assertThat(supportedGroups).isNotNull(); + } + } + + @Test + void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception { + // Explicitly use SunJSSE (JDK default) instead of Conscrypt + Provider sunJsseProvider = Security.getProvider("SunJSSE"); + assertThat(sunJsseProvider).isNotNull(); + + // Initialize SSLContext and TrustManagerFactory explicitly with SunJSSE provider to trust the + // CA + SSLContext sslContext = SSLContext.getInstance("TLS", sunJsseProvider); + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm(), sunJsseProvider); + tmf.init(loadCaCert(DEFAULT_CA_CERT_PATH)); + sslContext.init(null, tmf.getTrustManagers(), null); + + // Build NetHttpTransport using the SunJSSE socket factory + NetHttpTransport transport = + new NetHttpTransport.Builder().setSslSocketFactory(sslContext.getSocketFactory()).build(); + + HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor(); + + InstantiatingHttpJsonChannelProvider transportChannelProvider = + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport(transport) + .setEndpoint("https://" + SECURE_ENDPOINT) + .setInterceptorProvider(() -> Collections.singletonList(interceptor)) + .build(); + + EchoSettings settings = + EchoSettings.newHttpJsonBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider(transportChannelProvider) + .build(); + + try (EchoClient client = EchoClient.create(settings)) { + EchoResponse response = + client.echo( + EchoRequest.newBuilder().setContent("pqc-httpjson-explicit-provider-test").build()); + assertThat(response.getContent()).isEqualTo("pqc-httpjson-explicit-provider-test"); + + HttpJsonMetadata capturedHeaders = interceptor.metadata; + assertThat(capturedHeaders).isNotNull(); + + String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER); + // Under SunJSSE (JDK default), PQC curves are unsupported, so it falls back to a classical + // curve (either X25519 or CurveP256 depending on JDK / Go negotiation) + assertThat(negotiatedGroup).isAnyOf("X25519", "CurveP256"); + assertThat(negotiatedGroup).isNotEqualTo(EXPECTED_TLS_GROUP); + } + } + + /** + * Captures initial TLS response headers (e.g. x-showcase-tls-group) from the gRPC stream. This is + * required because showcase TLS headers are sent as initial headers rather than trailing metadata + * (trailers), which means the shared utility GrpcCapturingClientInterceptor cannot be used (as it + * only intercepts trailers). + */ + private static class GrpcHeaderCapturingInterceptor implements ClientInterceptor { + private Metadata capturedHeaders; + + @Override + public ClientCall interceptCall( + MethodDescriptor method, io.grpc.CallOptions callOptions, Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + super.start( + new ForwardingClientCallListener.SimpleForwardingClientCallListener( + responseListener) { + @Override + public void onHeaders(Metadata headers) { + capturedHeaders = headers; + super.onHeaders(headers); + } + }, + headers); + } + }; + } + + public Metadata getCapturedHeaders() { + return capturedHeaders; + } + } + + /** + * Helper class to wrap a standard ManagedChannel with gRPC client interceptors. Since EchoClient + * requires a ManagedChannel (which handles shutdown and awaitTermination lifecycles), but + * ClientInterceptors.intercept() only returns a generic Channel, this class bridges the two by + * forwarding call creation to the intercepted channel, and routing lifecycle calls to the base + * channel. + */ + private static class InterceptedManagedChannel extends ManagedChannel { + private final ManagedChannel delegate; + private final Channel intercepted; + + InterceptedManagedChannel(ManagedChannel delegate, ClientInterceptor... interceptors) { + this.delegate = delegate; + this.intercepted = io.grpc.ClientInterceptors.intercept(delegate, interceptors); + } + + @Override + public ClientCall newCall( + MethodDescriptor methodDescriptor, io.grpc.CallOptions callOptions) { + return intercepted.newCall(methodDescriptor, callOptions); + } + + @Override + public String authority() { + return delegate.authority(); + } + + @Override + public ManagedChannel shutdown() { + delegate.shutdown(); + return this; + } + + @Override + public boolean isShutdown() { + return delegate.isShutdown(); + } + + @Override + public boolean isTerminated() { + return delegate.isTerminated(); + } + + @Override + public ManagedChannel shutdownNow() { + delegate.shutdownNow(); + return this; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return delegate.awaitTermination(timeout, unit); + } + } + + private static String getSingleHeaderString(HttpJsonMetadata metadata, String name) { + Object valueObj = metadata.getHeaders().get(name); + if (valueObj instanceof List) { + List list = (List) valueObj; + if (!list.isEmpty()) { + return String.valueOf(list.get(0)); + } + } else if (valueObj != null) { + return String.valueOf(valueObj); + } + return null; + } + + private static KeyStore loadCaCert(String certPath) throws Exception { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + try (InputStream is = Files.newInputStream(Paths.get(certPath))) { + Certificate cert = cf.generateCertificate(is); + trustStore.setCertificateEntry("showcase-ca", cert); + } + return trustStore; + } + + private static boolean isConscryptFunctional() { + try { + org.conscrypt.Conscrypt.newProvider(); + return true; + } catch (Throwable t) { + return false; + } + } +} diff --git a/java-showcase/pom.xml b/java-showcase/pom.xml index c90fb72c78fb..532ca3a02e8e 100644 --- a/java-showcase/pom.xml +++ b/java-showcase/pom.xml @@ -29,8 +29,28 @@ true + + + sonatype-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + true + + + false + + + + + + io.grpc + grpc-bom + 1.83.0-SNAPSHOT + pom + import + com.google.cloud google-cloud-shared-dependencies diff --git a/pom.xml b/pom.xml index 6471f9af7af0..e97a138d0aef 100644 --- a/pom.xml +++ b/pom.xml @@ -344,4 +344,40 @@ + + + + sonatype-central-snapshots + Sonatype Central Snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + sonatype-snapshots + Sonatype Snapshots + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + sonatype-s01-snapshots + Sonatype S01 Snapshots + https://s01.oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + \ No newline at end of file diff --git a/pqc-verification/README.md b/pqc-verification/README.md new file mode 100644 index 000000000000..704777a65304 --- /dev/null +++ b/pqc-verification/README.md @@ -0,0 +1,100 @@ +# GAPIC Post-Quantum Cryptography (PQC) Support & Verification + +This directory contains verification tools and samples to test, trace, and verify Post-Quantum Cryptography (PQC) support in Google Cloud Java client libraries, covering both gRPC and HttpJson (REST) transports. + +--- + +## 1. Prerequisites & Dependencies + +### Java Version +* To perform PQC handshakes, JDK 11+ is required for compiling Conscrypt. JDK 17+ or JDK 21+ is highly recommended. +* Conscrypt acts as the security provider providing hybrid group `X25519MLKEM768`. + +### Core Snapshot Artifacts +The PQC verification depends on local SNAPSHOT builds of libraries containing our PQC enhancements: +1. **`google-http-java-client`** (`pqc-support-conscrypt` branch): Enforces and wraps standard HTTP connections to prefer Conscrypt PQC sockets. +2. **`gRPC-Java`** (`1.83.0-SNAPSHOT`): Enables Netty 4.2 support which negotiates hybrid key exchange by default. + +--- + +## 2. Setting Up Showcase (Local TLS Server) + +The `ITPqc` test suite runs integration tests against the local secure **GAPIC Showcase** server. + +### Step 2.1: Download & Build Showcase with TLS Support +Clone the showcase server and checkout the PQC TLS support branch: +```shell +git clone https://github.com/googleapis/gapic-showcase.git +cd gapic-showcase +git checkout feat-pqc-tls +go build ./cmd/gapic-showcase +``` + +### Step 2.2: Run the Showcase Server with Auto-TLS (Recommended) +Start the Showcase server in Auto-TLS mode. This automatically generates a CA certificate in-memory at startup and saves it to the target directory: + +```shell +# Run on secure port 7470 +./gapic-showcase run \ + --port 7470 \ + --tls \ + --ca-cert-output-file /tmp/showcase-ca.pem +``` + +*Note: The helper script `build-with-local-http-client.sh` will automatically launch and clean up this Showcase server if it finds the `gapic-showcase` repository cloned next to the `google-cloud-java` monorepo.* +If running manually, execute the tests using: +`mvn test -pl java-showcase/gapic-showcase -Dtest=ITPqc -Dshowcase.ca.cert.path=/tmp/showcase-ca.pem` + +--- + +## 3. Running Local Verification Tests + +Use the helper script `build-with-local-http-client.sh` to automatically build/install `google-http-java-client` as a local snapshot, compile the monorepo, and execute Showcase PQC integration tests: + +```shell +# Set path to the google-http-java-client repository +export HTTP_CLIENT_DIR=~/IdeaProjects/google-http-java-client + +# Run the verification script +./build-with-local-http-client.sh +``` + +If successful, you will see `BUILD SUCCESS` and both `testGrpcPqc` and `testHttpJsonPqc` passing. + +--- + +## 4. Standalone BigQuery PQC Verification Sample + +The class `BqPqcTest` runs a live connection to Google Cloud BigQuery using the default client settings. Since the PQC changes are enabled by default in the underlying HTTP client transport, this sample does not require any custom PQC configurations. + +### Run the Sample +You can run the sample directly from your IDE, or via Maven. The project ID is resolved automatically via Application Default Credentials (ADC), and TLS/SSL handshake tracing is configured programmatically: + +```shell +cd pqc-verification + +# Run using exec-maven-plugin +mvn clean compile exec:java +``` + +### Expected Output +The program will automatically intercept the TLS handshake and assert on the negotiated curve. If successful, you will see a validation summary at the end of execution: + +``` +[DEBUG] Java Version: 17.0.19 +[DEBUG] Java Runtime: 17.0.19+10 +[DEBUG] Java VM : OpenJDK 64-Bit Server VM (17.0.19+10) +Initializing default BigQuery client for project: your-gcp-project-id +Listing datasets using default BigQuery Client... +- my_dataset1 +- my_dataset2 +Success! BigQuery datasets retrieved successfully. + +================================================== +TLS Handshake Verification Results: + Protocol : TLSv1.3 + Cipher Suite : TLS_AES_128_GCM_SHA256 + Negotiated KEX: X25519MLKEM768 +================================================== +VERIFICATION SUCCESS: PQC Hybrid key exchange negotiated successfully! +``` diff --git a/pqc-verification/pom.xml b/pqc-verification/pom.xml new file mode 100644 index 000000000000..8b56f265be0d --- /dev/null +++ b/pqc-verification/pom.xml @@ -0,0 +1,96 @@ + + + + 4.0.0 + + com.google.cloud.pqc + pqc-verification + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + 2.69.0-SNAPSHOT + 2.1.2-SNAPSHOT + 2.6.0 + + + + + + com.google.cloud + google-cloud-bigquery + ${bigquery.version} + + + + + com.google.http-client + google-http-client + ${http-client.version} + + + + + org.conscrypt + conscrypt-openjdk-uber + ${conscrypt.version} + + + + + + sonatype-central-snapshots + Sonatype Central Snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + sonatype-snapshots + Sonatype Snapshots + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + com.google.cloud.pqc.BqPqcTest + + + + + diff --git a/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java b/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java new file mode 100644 index 000000000000..bf618dbb8373 --- /dev/null +++ b/pqc-verification/src/main/java/com/google/cloud/pqc/BqPqcTest.java @@ -0,0 +1,279 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.pqc; + +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.cloud.bigquery.BigQuery; +import com.google.cloud.bigquery.BigQueryOptions; +import com.google.cloud.http.HttpTransportOptions; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.concurrent.atomic.AtomicReference; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import org.conscrypt.OpenSSLSocketImpl; + +/** + * A verification sample to programmatically trace and assert TLS handshake details (protocol, + * cipher suite, and negotiated curve) for Google Cloud BigQuery client calls, verifying that PQC + * (X25519MLKEM768) is negotiated. + */ +public class BqPqcTest { + + private static final String EXPECTED_PQC_CURVE = "X25519MLKEM768"; + private static final AtomicReference negotiatedCurve = new AtomicReference<>(); + private static final AtomicReference negotiatedProtocol = new AtomicReference<>(); + private static final AtomicReference negotiatedCipherSuite = new AtomicReference<>(); + + public static void main(String[] args) throws Exception { + System.out.println("[DEBUG] Java Version: " + System.getProperty("java.version")); + System.out.println("[DEBUG] Java Runtime: " + System.getProperty("java.runtime.version")); + System.out.println( + "[DEBUG] Java VM : " + + System.getProperty("java.vm.name") + + " (" + + System.getProperty("java.vm.version") + + ")"); + + String projectId = System.getProperty("project.id"); + if (projectId == null || projectId.isEmpty()) { + projectId = System.getenv("GOOGLE_CLOUD_PROJECT"); + } + if (projectId == null || projectId.isEmpty()) { + try { + projectId = BigQueryOptions.getDefaultInstance().getProjectId(); + } catch (Exception e) { + // Ignore if defaults are not configured + } + } + if (projectId == null || projectId.isEmpty()) { + System.err.println("Error: Google Cloud Project ID could not be resolved automatically."); + System.err.println( + "Please set the GOOGLE_CLOUD_PROJECT environment variable, or configure Application" + + " Default Credentials."); + System.exit(1); + } + + boolean usePqc = isConscryptFunctional(); + HttpTransportOptions transportOptions; + + if (usePqc) { + System.out.println("Conscrypt is functional. Configuring TLS hybrid PQC tracing..."); + // 1. Get the default HttpTransport from standard HttpTransportOptions + HttpTransportOptions defaultOptions = HttpTransportOptions.newBuilder().build(); + HttpTransport defaultTransport = defaultOptions.getHttpTransportFactory().create(); + + // 2. Reflectively extract the SSLSocketFactory configured by gax/core-http + SSLSocketFactory defaultFactory = + (SSLSocketFactory) getPrivateField(defaultTransport, "sslSocketFactory"); + if (defaultFactory == null) { + defaultFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + } + + // 3. Wrap it in our tracing socket factory + SSLSocketFactory tracingFactory = new TracingSSLSocketFactory(defaultFactory); + + // 4. Build a tracing transport using this factory + HttpTransport tracingTransport = + new NetHttpTransport.Builder().setSslSocketFactory(tracingFactory).build(); + + // 5. Configure BigQuery client to use this tracing transport + transportOptions = + HttpTransportOptions.newBuilder().setHttpTransportFactory(() -> tracingTransport).build(); + } else { + System.out.println( + "Conscrypt is not functional. Using default transport options (clean fallback check)..."); + transportOptions = HttpTransportOptions.newBuilder().build(); + } + + System.out.println("Initializing default BigQuery client for project: " + projectId); + BigQuery bigquery = + BigQueryOptions.newBuilder() + .setProjectId(projectId) + .setTransportOptions(transportOptions) + .setCredentials(com.google.cloud.NoCredentials.getInstance()) + .build() + .getService(); + + System.out.println("Executing API call to trigger TLS handshake..."); + try { + bigquery.listDatasets(); + } catch (com.google.cloud.bigquery.BigQueryException e) { + if (e.getCode() == 401 + || e.getMessage().contains("missing required authentication credential")) { + System.out.println("TLS connection established. Proceeding with verification..."); + } else { + System.err.println("API call failed with unexpected error code."); + e.printStackTrace(); + System.exit(1); + } + } catch (Exception e) { + System.err.println("Unexpected exception during connection."); + e.printStackTrace(); + System.exit(1); + } + + if (usePqc) { + // Wait a brief moment for asynchronous JSSE listener thread to execute + Thread.sleep(300); + + // Perform Programmatic Assertion on the Negotiated Curve + String curve = negotiatedCurve.get(); + String protocol = negotiatedProtocol.get(); + String cipherSuite = negotiatedCipherSuite.get(); + + System.out.println("\n=================================================="); + System.out.println("TLS Handshake Verification Results:"); + System.out.println(" Protocol : " + protocol); + System.out.println(" Cipher Suite : " + cipherSuite); + System.out.println(" Negotiated KEX: " + curve); + System.out.println("=================================================="); + + if (curve == null) { + System.err.println("ERROR: No TLS handshake was intercepted!"); + System.exit(1); + } + + String expectedCurve = "X25519MLKEM768"; + if (expectedCurve.equalsIgnoreCase(curve)) { + System.out.println( + "VERIFICATION SUCCESS: Key exchange (" + expectedCurve + ") negotiated successfully!"); + } else { + System.err.println( + "VERIFICATION FAILED: Expected Key Exchange " + + expectedCurve + + " but negotiated: " + + curve); + System.exit(1); + } + } else { + System.out.println("\n=================================================="); + System.out.println( + "VERIFICATION SUCCESS: Clean fallback to default JSSE provider verified successfully!"); + System.out.println("=================================================="); + } + } + + private static Object getPrivateField(Object obj, String fieldName) throws Exception { + Class clazz = obj.getClass(); + while (clazz != null) { + try { + java.lang.reflect.Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(obj); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + throw new NoSuchFieldException( + "Field " + fieldName + " not found in class hierarchy of " + obj.getClass()); + } + + private static class TracingSSLSocketFactory extends SSLSocketFactory { + private final SSLSocketFactory delegate; + + public TracingSSLSocketFactory(SSLSocketFactory delegate) { + this.delegate = delegate; + } + + private Socket wrap(Socket socket) { + System.out.println( + "[DEBUG] TracingSSLSocketFactory wrapped socket: " + + (socket == null ? "null" : socket.getClass().getName())); + if (socket instanceof SSLSocket) { + SSLSocket sslSocket = (SSLSocket) socket; + sslSocket.addHandshakeCompletedListener( + event -> { + try { + System.out.println("[DEBUG] HandshakeCompletedListener triggered asynchronously!"); + negotiatedCipherSuite.set(event.getCipherSuite()); + negotiatedProtocol.set(event.getSession().getProtocol()); + Socket rawSocket = event.getSocket(); + + String curve = null; + // Direct Conscrypt check since it is a direct dependency + if (rawSocket instanceof OpenSSLSocketImpl) { + curve = ((OpenSSLSocketImpl) rawSocket).getCurveNameForTesting(); + } + + if (curve != null) { + negotiatedCurve.set(curve); + } + } catch (Exception e) { + System.err.println("Failed to log TLS handshake: " + e.getMessage()); + } + }); + } + return socket; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) + throws IOException { + return wrap(delegate.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket() throws IOException { + return wrap(delegate.createSocket()); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return wrap(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException, UnknownHostException { + return wrap(delegate.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return wrap(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket( + InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return wrap(delegate.createSocket(address, port, localAddress, localPort)); + } + } + + private static boolean isConscryptFunctional() { + try { + org.conscrypt.Conscrypt.newProvider(); + return true; + } catch (Throwable t) { + return false; + } + } +} 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 76fff743425f..87c03f7310e6 100644 --- a/sdk-platform-java/gapic-generator-java-pom-parent/pom.xml +++ b/sdk-platform-java/gapic-generator-java-pom-parent/pom.xml @@ -28,7 +28,8 @@ consistent across modules in this repository --> 1.3.2 1.81.0 - 2.1.1 + 2.1.2-SNAPSHOT + 2.6.0 2.13.2 33.5.0-jre 4.33.2 diff --git a/sdk-platform-java/gax-java/gax-httpjson/pom.xml b/sdk-platform-java/gax-java/gax-httpjson/pom.xml index de839fe6e074..d70dbfe4459b 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/pom.xml +++ b/sdk-platform-java/gax-java/gax-httpjson/pom.xml @@ -104,6 +104,11 @@ error_prone_annotations ${errorprone.version} + + org.conscrypt + conscrypt-openjdk-uber + ${conscrypt.version} + diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ConscryptPqcConfiguratorHelper.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ConscryptPqcConfiguratorHelper.java new file mode 100644 index 000000000000..135b8d4a6328 --- /dev/null +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ConscryptPqcConfiguratorHelper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.httpjson; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.http.javanet.SslSocketConfigurator; +import com.google.api.core.InternalApi; +import javax.net.ssl.SSLSocket; +import org.conscrypt.Conscrypt; + +@InternalApi +public final class ConscryptPqcConfiguratorHelper { + private static final String[] DEFAULT_PQC_GROUPS = + new String[] {"X25519MLKEM768", "SecP256r1MLKEM768", "X25519Kyber768Draft00", "X25519"}; + + public static void configure(NetHttpTransport.Builder builder) { + builder.setSslSocketConfigurator( + new SslSocketConfigurator() { + @Override + public void configure(SSLSocket socket) { + if (Conscrypt.isConscrypt(socket)) { + Conscrypt.setNamedGroups(socket, DEFAULT_PQC_GROUPS); + } + } + }); + } +} 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 495fec1ed450..4fdb9a5d5781 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 @@ -51,6 +51,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; +import org.conscrypt.Conscrypt; /** * InstantiatingHttpJsonChannelProvider is a TransportChannelProvider which constructs a {@link @@ -191,16 +192,20 @@ public TransportChannelProvider withCredentials(Credentials credentials) { } HttpTransport createHttpTransport() throws IOException, GeneralSecurityException { - if (mtlsProvider == null) { - return null; + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); + try { + builder.setSecurityProvider(Conscrypt.newProvider()); + ConscryptPqcConfiguratorHelper.configure(builder); + } catch (Throwable t) { + LOG.log(Level.FINE, "Conscrypt native libraries not available. Falling back to JDK TLS.", t); } - if (certificateBasedAccess.useMtlsClientCertificate()) { + if (mtlsProvider != null && certificateBasedAccess.useMtlsClientCertificate()) { KeyStore mtlsKeyStore = mtlsProvider.getKeyStore(); if (mtlsKeyStore != null) { - return new NetHttpTransport.Builder().trustCertificates(null, mtlsKeyStore, "").build(); + builder.trustCertificates(null, mtlsKeyStore, ""); } } - return null; + return builder.build(); } private HttpJsonTransportChannel createChannel() throws IOException, GeneralSecurityException { @@ -364,7 +369,8 @@ public InstantiatingHttpJsonChannelProvider build() { "DefaultMtlsProviderFactory encountered unexpected IOException: " + e.getMessage()); LOG.log( Level.WARNING, - "mTLS configuration was detected on the device, but mTLS failed to initialize. Falling back to non-mTLS channel."); + "mTLS configuration was detected on the device, but mTLS failed to initialize." + + " Falling back to non-mTLS channel."); } } } diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java index 17ad9f2cbf2b..f4cc4e92e4bd 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java @@ -32,6 +32,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.gax.rpc.HeaderProvider; import com.google.api.gax.rpc.TransportChannelProvider; import com.google.api.gax.rpc.mtls.AbstractMtlsTransportChannelTest; @@ -192,6 +193,41 @@ protected Object getMtlsObjectFromTransportChannel( .setHeaderProvider(Mockito.mock(HeaderProvider.class)) .setExecutor(Mockito.mock(Executor.class)) .build(); - return channelProvider.createHttpTransport(); + NetHttpTransport transport = (NetHttpTransport) channelProvider.createHttpTransport(); + return (transport != null && transport.isMtls()) ? transport : null; + } + + @Test + void testCreateHttpTransport_pqcConfigured() throws Exception { + boolean conscryptLoaded = false; + try { + org.conscrypt.Conscrypt.newProvider(); + conscryptLoaded = true; + } catch (Throwable t) { + // Conscrypt JNI cannot load on this test runner, skipping assertion + } + InstantiatingHttpJsonChannelProvider channelProvider = + InstantiatingHttpJsonChannelProvider.newBuilder() + .setEndpoint("localhost:8080") + .setHeaderProvider(Mockito.mock(HeaderProvider.class)) + .setExecutor(Mockito.mock(Executor.class)) + .build(); + NetHttpTransport transport = (NetHttpTransport) channelProvider.createHttpTransport(); + Object factory = getPrivateField(transport, "sslSocketFactory"); + if (conscryptLoaded) { + assertThat(factory.getClass().getName()) + .isEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); + Object configurator = getPrivateField(factory, "configurator"); + assertThat(configurator).isNotNull(); + } else { + assertThat(factory.getClass().getName()) + .isNotEqualTo("com.google.api.client.http.javanet.ConfigurableSSLSocketFactory"); + } + } + + private static Object getPrivateField(Object obj, String fieldName) throws Exception { + java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(obj); } } diff --git a/sdk-platform-java/gax-java/pom.xml b/sdk-platform-java/gax-java/pom.xml index d7f9f6841fb6..6cfc2bb49cb0 100644 --- a/sdk-platform-java/gax-java/pom.xml +++ b/sdk-platform-java/gax-java/pom.xml @@ -181,6 +181,11 @@ ${awaitility.version} test
+ + org.conscrypt + conscrypt-openjdk-uber + ${conscrypt.version} +
diff --git a/sdk-platform-java/java-core/google-cloud-core-http/pom.xml b/sdk-platform-java/java-core/google-cloud-core-http/pom.xml index 9bcac047678b..ee8beb09cbf0 100644 --- a/sdk-platform-java/java-core/google-cloud-core-http/pom.xml +++ b/sdk-platform-java/java-core/google-cloud-core-http/pom.xml @@ -75,6 +75,11 @@ error_prone_annotations + + org.conscrypt + conscrypt-openjdk-uber + + org.junit.platform diff --git a/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java b/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java index f5ad54532f66..e524ac84057a 100644 --- a/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java +++ b/sdk-platform-java/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java @@ -24,6 +24,7 @@ import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.gax.core.GaxProperties; +import com.google.api.gax.httpjson.ConscryptPqcConfiguratorHelper; import com.google.api.gax.httpjson.HttpHeadersUtils; import com.google.api.gax.httpjson.HttpJsonStatusCode; import com.google.api.gax.rpc.ApiClientHeaderProvider; @@ -41,6 +42,7 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.util.Objects; +import org.conscrypt.Conscrypt; /** Class representing service options for those services that use HTTP as the transport layer. */ public class HttpTransportOptions implements TransportOptions { @@ -66,6 +68,21 @@ public HttpTransport create() { // Maybe not on App Engine } } + + // If Conscrypt is available on the classpath, instantiate it as the security provider + // and configure the HTTP client to enable Post-Quantum Cryptography (PQC) named groups + // by default. This ensures that client connections automatically benefit from + // quantum-resistant + // key exchange when Conscrypt is present. + try { + NetHttpTransport.Builder builder = + new NetHttpTransport.Builder().setSecurityProvider(Conscrypt.newProvider()); + ConscryptPqcConfiguratorHelper.configure(builder); + return builder.build(); + } catch (Throwable t) { + // Fallback to standard NetHttpTransport if Conscrypt is not available or failed to load + } + return new NetHttpTransport(); } } diff --git a/sdk-platform-java/java-shared-dependencies/third-party-dependencies/pom.xml b/sdk-platform-java/java-shared-dependencies/third-party-dependencies/pom.xml index 266cbfb8c8e9..dd9fb855b0dd 100644 --- a/sdk-platform-java/java-shared-dependencies/third-party-dependencies/pom.xml +++ b/sdk-platform-java/java-shared-dependencies/third-party-dependencies/pom.xml @@ -46,10 +46,16 @@ 1.16.0 1.45.0-alpha 20250517 + 2.6.0 + + org.conscrypt + conscrypt-openjdk-uber + ${conscrypt.version} + org.bouncycastle bcprov-jdk18on diff --git a/sdk-platform-java/pom.xml b/sdk-platform-java/pom.xml index b14a458db938..2a50e283e830 100644 --- a/sdk-platform-java/pom.xml +++ b/sdk-platform-java/pom.xml @@ -98,4 +98,39 @@ + + + sonatype-central-snapshots + Sonatype Central Snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + sonatype-snapshots + Sonatype Snapshots + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + sonatype-s01-snapshots + Sonatype S01 Snapshots + https://s01.oss.sonatype.org/content/repositories/snapshots + + false + + + true + + +