Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/pqc-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: PQC Connectivity Integration Tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
pqc-tests:
runs-on: ubuntu-latest

steps:
# 1. Checkout sibling HTTP Client repository
- name: Checkout google-http-java-client
uses: actions/checkout@v4
with:
repository: googleapis/google-http-java-client
ref: chore/pqc-poc-2
path: google-http-java-client

# 2. Checkout this monorepo
- name: Checkout google-cloud-java-pqc
uses: actions/checkout@v4
with:
path: google-cloud-java-pqc

# 3. Set up JDK 17
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
cache-dependency-path: 'google-cloud-java-pqc/pom.xml'

# 4. Build and install modified google-http-client SNAPSHOT locally
- name: Build and Install google-http-java-client
run: |
cd google-http-java-client
mvn clean install -DskipTests=true -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip

# 5. Build the entire monorepo core components required by the tests
- name: Build and Install Core Dependency Reactor
run: |
cd google-cloud-java-pqc
mvn clean install -pl sdk-platform-java/pqc-test/pqc-test-snapshot,sdk-platform-java/pqc-test/pqc-test-release -am -T 1.5C -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -DskipTests=true

# 6. Run Snapshot PQC Tests (EXPECT PASS)
- name: Run Snapshot PQC Connectivity Tests (Expect PASS)
run: |
cd google-cloud-java-pqc/sdk-platform-java/pqc-test/pqc-test-snapshot
mvn install -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -Dtest=RunPqcTest

# 7. Run Release PQC Tests (EXPECT FAIL)
- name: Run Release PQC Connectivity Tests (Expect FAIL)
# We expect this step to fail. If it passes, it means release libraries are negotiating PQC (which is incorrect).
# Thus we run it and assert that the maven command fails (exit code != 0).
run: |
cd google-cloud-java-pqc/sdk-platform-java/pqc-test/pqc-test-release
if mvn install -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -Dtest=RunPqcTest; then
echo "Error: Release tests passed but they were expected to fail!"
exit 1
else
echo "Success: Release tests failed-fast as expected."
exit 0
fi
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@

package com.google.auth.oauth2;

import com.google.api.client.util.SslUtils;
import java.security.GeneralSecurityException;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
Expand Down Expand Up @@ -104,7 +108,21 @@ enum Pkcs8Algorithm {
public static final String CLOUD_PLATFORM_SCOPE =
"https://www.googleapis.com/auth/cloud-platform";

static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
private static final Logger logger = Logger.getLogger(OAuth2Utils.class.getName());

static final HttpTransport HTTP_TRANSPORT;
static {
HttpTransport transport;
try {
transport = new NetHttpTransport.Builder()
.setSslSocketFactory(SslUtils.getTlsSslContext().getSocketFactory())
.build();
} catch (GeneralSecurityException e) {
logger.log(Level.WARNING, "Failed to initialize PQC-hardened HTTP transport, falling back to default", e);
transport = new NetHttpTransport();
}
HTTP_TRANSPORT = transport;
}

public static final HttpTransportFactory HTTP_TRANSPORT_FACTORY =
new DefaultHttpTransportFactory();
Expand Down
3 changes: 2 additions & 1 deletion sdk-platform-java/gapic-generator-java-pom-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
</parent>

<properties>
<bouncycastle.version>1.80</bouncycastle.version>
<skipUnitTests>false</skipUnitTests>
<checkstyle.header.file>java.header</checkstyle.header.file>
<maven.compiler.release>8</maven.compiler.release>
Expand All @@ -27,7 +28,7 @@
consistent across modules in this repository -->
<javax.annotation-api.version>1.3.2</javax.annotation-api.version>
<grpc.version>1.81.0</grpc.version>
<google.http-client.version>2.1.0</google.http-client.version>
<google.http-client.version>2.1.1-SNAPSHOT</google.http-client.version>
Comment thread
diegomarquezp marked this conversation as resolved.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Updating google.http-client.version to a -SNAPSHOT version in a parent POM is generally discouraged for release-track projects. This can lead to unstable builds and dependency resolution issues in environments without access to the specific snapshot repository. If this is necessary for the POC, please ensure it is reverted or replaced with a stable version before merging.

<gson.version>2.13.2</gson.version>
<guava.version>33.5.0-jre</guava.version>
<protobuf.version>4.33.2</protobuf.version>
Expand Down
11 changes: 11 additions & 0 deletions sdk-platform-java/gax-java/gax-grpc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bctls-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>

<!-- test dependencies -->
<dependency>
<groupId>io.grpc</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -812,13 +812,158 @@ public ManagedChannelBuilder<?> createDecoratedChannelBuilder() throws IOExcepti
if (interceptorProvider != null) {
builder.intercept(interceptorProvider.getInterceptors());
}
// Apply PQC configuration by default as a standard feature of GAX.
builder = applyPqcConfiguration(builder);

if (channelConfigurator != null) {
builder = channelConfigurator.apply(builder);
}

return builder;
}

private static final class OpenSslReflectionHolder {
private static final Class<?> SHADED_GRPC_SSL_CONTEXTS;
private static final Class<?> SHADED_SSL_CONTEXT_BUILDER;
private static final java.lang.reflect.Method SHADED_FOR_CLIENT;
private static final Object SHADED_GROUPS_OPTION;
private static final java.lang.reflect.Method SHADED_OPTION_METHOD;
private static final java.lang.reflect.Method SHADED_BUILD_METHOD;
private static final java.lang.reflect.Method SHADED_SSL_CONTEXT_METHOD;
private static final Class<?> SHADED_SSL_CONTEXT;
private static final boolean SHADED_AVAILABLE;

private static final Class<?> UNSHADED_GRPC_SSL_CONTEXTS;
private static final Class<?> UNSHADED_SSL_CONTEXT_BUILDER;
private static final java.lang.reflect.Method UNSHADED_FOR_CLIENT;
private static final Object UNSHADED_GROUPS_OPTION;
private static final java.lang.reflect.Method UNSHADED_OPTION_METHOD;
private static final java.lang.reflect.Method UNSHADED_BUILD_METHOD;
private static final java.lang.reflect.Method UNSHADED_SSL_CONTEXT_METHOD;
private static final Class<?> UNSHADED_SSL_CONTEXT;
private static final boolean UNSHADED_AVAILABLE;

static {
// 1. Shaded Netty Lookups
Class<?> shadedGrpcSslCtx = null;
Class<?> shadedSslCtxBuilder = null;
java.lang.reflect.Method shadedForClient = null;
Object shadedGroupsOpt = null;
java.lang.reflect.Method shadedOption = null;
java.lang.reflect.Method shadedBuild = null;
java.lang.reflect.Method shadedSslCtxMethod = null;
Class<?> shadedSslCtx = null;
boolean shadedAvailable = false;
try {
String p = "io.grpc.netty.shaded.";
shadedGrpcSslCtx = Class.forName(p + "io.grpc.netty.GrpcSslContexts");
shadedSslCtxBuilder = Class.forName(p + "io.netty.handler.ssl.SslContextBuilder");
Class<?> openSslCtxOpt = Class.forName(p + "io.netty.handler.ssl.OpenSslContextOption");
Class<?> sslCtxOpt = Class.forName(p + "io.netty.handler.ssl.SslContextOption");
shadedSslCtx = Class.forName(p + "io.netty.handler.ssl.SslContext");

shadedForClient = shadedGrpcSslCtx.getMethod("forClient");
java.lang.reflect.Field groupsField = openSslCtxOpt.getDeclaredField("GROUPS");
shadedGroupsOpt = groupsField.get(null);
shadedOption = shadedSslCtxBuilder.getMethod("option", sslCtxOpt, Object.class);
shadedBuild = shadedSslCtxBuilder.getMethod("build");

Class<?> nettyBuilderClass = Class.forName(p + "io.grpc.netty.NettyChannelBuilder");
shadedSslCtxMethod = nettyBuilderClass.getMethod("sslContext", shadedSslCtx);

shadedAvailable = true;
} catch (Throwable t) {
// Ignore: Shaded Netty is not available
}
SHADED_GRPC_SSL_CONTEXTS = shadedGrpcSslCtx;
SHADED_SSL_CONTEXT_BUILDER = shadedSslCtxBuilder;
SHADED_FOR_CLIENT = shadedForClient;
SHADED_GROUPS_OPTION = shadedGroupsOpt;
SHADED_OPTION_METHOD = shadedOption;
SHADED_BUILD_METHOD = shadedBuild;
SHADED_SSL_CONTEXT_METHOD = shadedSslCtxMethod;
SHADED_SSL_CONTEXT = shadedSslCtx;
SHADED_AVAILABLE = shadedAvailable;

// 2. Unshaded Netty Lookups
Class<?> unshadedGrpcSslCtx = null;
Class<?> unshadedSslCtxBuilder = null;
java.lang.reflect.Method unshadedForClient = null;
Object unshadedGroupsOpt = null;
java.lang.reflect.Method unshadedOption = null;
java.lang.reflect.Method unshadedBuild = null;
java.lang.reflect.Method unshadedSslCtxMethod = null;
Class<?> unshadedSslCtx = null;
boolean unshadedAvailable = false;
try {
unshadedGrpcSslCtx = Class.forName("io.grpc.netty.GrpcSslContexts");
unshadedSslCtxBuilder = Class.forName("io.netty.handler.ssl.SslContextBuilder");
Class<?> openSslCtxOpt = Class.forName("io.netty.handler.ssl.OpenSslContextOption");
Class<?> sslCtxOpt = Class.forName("io.netty.handler.ssl.SslContextOption");
unshadedSslCtx = Class.forName("io.netty.handler.ssl.SslContext");

unshadedForClient = unshadedGrpcSslCtx.getMethod("forClient");
java.lang.reflect.Field groupsField = openSslCtxOpt.getDeclaredField("GROUPS");
unshadedGroupsOpt = groupsField.get(null);
unshadedOption = unshadedSslCtxBuilder.getMethod("option", sslCtxOpt, Object.class);
unshadedBuild = unshadedSslCtxBuilder.getMethod("build");

Class<?> nettyBuilderClass = Class.forName("io.grpc.netty.NettyChannelBuilder");
unshadedSslCtxMethod = nettyBuilderClass.getMethod("sslContext", unshadedSslCtx);

unshadedAvailable = true;
} catch (Throwable t) {
// Ignore: Unshaded Netty is not available
}
UNSHADED_GRPC_SSL_CONTEXTS = unshadedGrpcSslCtx;
UNSHADED_SSL_CONTEXT_BUILDER = unshadedSslCtxBuilder;
UNSHADED_FOR_CLIENT = unshadedForClient;
UNSHADED_GROUPS_OPTION = unshadedGroupsOpt;
UNSHADED_OPTION_METHOD = unshadedOption;
UNSHADED_BUILD_METHOD = unshadedBuild;
UNSHADED_SSL_CONTEXT_METHOD = unshadedSslCtxMethod;
UNSHADED_SSL_CONTEXT = unshadedSslCtx;
UNSHADED_AVAILABLE = unshadedAvailable;
}
}

private ManagedChannelBuilder<?> applyPqcConfiguration(ManagedChannelBuilder<?> builder) {
// Configure the PQ and classical hybrid named groups:
// 1. X25519MLKEM768 (codepoint 4588): Hybrid classical (X25519) + post-quantum (ML-KEM-768) key exchange.
// Provides defense-in-depth: if ML-KEM is compromised, security reverts to classical strength of X25519.
// 2. MLKEM768 (codepoint 1896): Pure post-quantum key exchange using ML-KEM-768.
// 3. X25519 (codepoint 29): Classical elliptic curve Diffie-Hellman key exchange, used as a fallback.
String[] hybridGroups = new String[] {"X25519MLKEM768", "MLKEM768", "X25519"};
String builderClassName = builder.getClass().getName();
boolean isShaded = "io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder".equals(builderClassName);
boolean isUnshaded = "io.grpc.netty.NettyChannelBuilder".equals(builderClassName);

if (isShaded && OpenSslReflectionHolder.SHADED_AVAILABLE) {
try {
Object sslContextBuilder = OpenSslReflectionHolder.SHADED_FOR_CLIENT.invoke(null);
OpenSslReflectionHolder.SHADED_OPTION_METHOD.invoke(
sslContextBuilder, OpenSslReflectionHolder.SHADED_GROUPS_OPTION, (Object) hybridGroups);
Object sslContext = OpenSslReflectionHolder.SHADED_BUILD_METHOD.invoke(sslContextBuilder);
OpenSslReflectionHolder.SHADED_SSL_CONTEXT_METHOD.invoke(builder, sslContext);
return builder;
} catch (java.lang.reflect.InvocationTargetException | IllegalAccessException | RuntimeException e) {
LOG.log(Level.WARNING, "Failed to configure shaded PQC transport fallback", e);
}
} else if (isUnshaded && OpenSslReflectionHolder.UNSHADED_AVAILABLE) {
try {
Object sslContextBuilder = OpenSslReflectionHolder.UNSHADED_FOR_CLIENT.invoke(null);
OpenSslReflectionHolder.UNSHADED_OPTION_METHOD.invoke(
sslContextBuilder, OpenSslReflectionHolder.UNSHADED_GROUPS_OPTION, (Object) hybridGroups);
Object sslContext = OpenSslReflectionHolder.UNSHADED_BUILD_METHOD.invoke(sslContextBuilder);
OpenSslReflectionHolder.UNSHADED_SSL_CONTEXT_METHOD.invoke(builder, sslContext);
return builder;
} catch (java.lang.reflect.InvocationTargetException | IllegalAccessException | RuntimeException e) {
LOG.log(Level.WARNING, "Failed to configure unshaded PQC transport fallback", e);
}
}
return builder;
}
Comment on lines +930 to +965
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The applyPqcConfiguration method contains significant code duplication between the shaded and unshaded Netty configuration paths. Additionally, checking the builder class name via string comparison (lines 938-939) is brittle and may fail if the class is renamed or wrapped.

Consider refactoring OpenSslReflectionHolder to store two instances of a helper class (e.g., NettyReflectionMetadata) that encapsulates the reflected methods and classes for each variant. You can then use Class.isInstance() for a more robust type check and a single helper method to apply the configuration.

References
  1. If code is duplicated and needs to be shared, move it to a separate helper/utility class.


private ManagedChannel createSingleChannel() throws IOException {
ManagedChannelBuilder<?> builder = createDecoratedChannelBuilder();

Expand Down
11 changes: 11 additions & 0 deletions sdk-platform-java/gax-java/gax-httpjson/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@
</properties>

<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bctls-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>

<dependency>
<groupId>com.google.api</groupId>
<artifactId>gax</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import com.google.auth.mtls.DefaultMtlsProviderFactory;
import com.google.auth.mtls.MtlsProvider;
import com.google.common.annotations.VisibleForTesting;
import javax.net.ssl.SSLContext;
import java.security.NoSuchAlgorithmException;
Comment on lines +45 to +46
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The imports for javax.net.ssl.SSLContext and java.security.NoSuchAlgorithmException are unused in this file and should be removed.

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
Expand Down Expand Up @@ -186,6 +188,8 @@ public TransportChannelProvider withCredentials(Credentials credentials) {

HttpTransport createHttpTransport() throws IOException, GeneralSecurityException {
if (mtlsProvider == null) {
// Returning null allows ManagedHttpJsonChannel to instantiate a default NetHttpTransport,
// which is automatically PQC-hardened if Bouncy Castle JSSE is available on the classpath.
return null;
}
if (certificateBasedAccess.useMtlsClientCertificate()) {
Expand Down
1 change: 1 addition & 0 deletions sdk-platform-java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<module>gapic-generator-java-bom</module>
<module>java-shared-dependencies</module>
<module>sdk-platform-java-config</module>
<module>pqc-test</module>
</modules>
<!-- Do not deploy the aggregator POM -->
<build>
Expand Down
24 changes: 24 additions & 0 deletions sdk-platform-java/pqc-test/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.google.api</groupId>
<artifactId>gapic-generator-java-pom-parent</artifactId>
<version>2.73.0-SNAPSHOT</version>
<relativePath>../gapic-generator-java-pom-parent</relativePath>
</parent>

<groupId>com.google.api</groupId>
<artifactId>pqc-test-parent</artifactId>
<packaging>pom</packaging>
<version>2.81.0-SNAPSHOT</version>

<modules>
<module>pqc-test-common</module>
<module>pqc-test-snapshot</module>
<module>pqc-test-release</module>
</modules>
</project>
Loading
Loading