Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
62a9a7e
feat(httpjson): add Conscrypt PQC support for HttpJson transports
lqiu96 Jul 21, 2026
4c405ea
refactor(httpjson): restrict ITPqc to HttpJson PQC tests and remove p…
lqiu96 Jul 21, 2026
0d7f936
refactor(httpjson): remove X25519Kyber768Draft00 from PQC groups and …
lqiu96 Jul 21, 2026
aa2c841
refactor(httpjson): group setSecurityProvider and setSslSocketConfigu…
lqiu96 Jul 21, 2026
2fb7f15
docs(pqc): add detailed explanatory comments above Conscrypt try-catc…
lqiu96 Jul 21, 2026
dde8254
docs(pqc): add Level.FINE logger to HttpTransportOptions when Conscry…
lqiu96 Jul 21, 2026
8ba0498
test(httpjson): remove reflection helper from InstantiatingHttpJsonCh…
lqiu96 Jul 21, 2026
4b38082
test(pqc): add unit tests for PQC Conscrypt transport configuration i…
lqiu96 Jul 21, 2026
02c18d1
test(pqc): clean up reflection tests from InstantiatingHttpJsonChanne…
lqiu96 Jul 21, 2026
3619e36
refactor(pqc): fallback to fresh NetHttpTransport.Builder on Conscryp…
lqiu96 Jul 21, 2026
7a56583
refactor(showcase): streamline ITPqc transport setup and remove Consc…
lqiu96 Jul 21, 2026
3b10d47
refactor(showcase): remove isConscryptFunctional fallback and restric…
lqiu96 Jul 21, 2026
5f7b1c4
refactor(showcase): address PR review comments for ITPqc.java and add…
lqiu96 Jul 21, 2026
d20a197
docs(showcase): clarify rationale for classical named groups configur…
lqiu96 Jul 21, 2026
caa15fe
refactor(http): decouple PQC constants and add defensive lambda error…
lqiu96 Jul 21, 2026
41b15b0
fix(http): update log level to FINE and clarify fallback message for …
lqiu96 Jul 21, 2026
f9aedfb
perf(http): lazily initialize and cache Conscrypt security provider i…
lqiu96 Jul 21, 2026
d32c739
refactor(http): use early returns in createHttpTransport and upgrade …
lqiu96 Jul 21, 2026
3364523
docs(http): document architectural tradeoff decision for graceful Con…
lqiu96 Jul 21, 2026
8b14718
refactor(http): centralize Conscrypt PQC transport logic in HttpJsonT…
lqiu96 Jul 21, 2026
7cf7b84
docs(http): update Conscrypt PQC Javadoc comments and method naming
lqiu96 Jul 21, 2026
4dd4108
refactor(http): remove duplicate DEFAULT_PQC_GROUPS and simplify Cons…
lqiu96 Jul 21, 2026
f35ea41
docs(http): move environment fallback explanation to ConscryptProvide…
lqiu96 Jul 21, 2026
056d3eb
docs(http): clarify caching rationale vs JDK TLS fallback in Conscryp…
lqiu96 Jul 21, 2026
1d082b6
refactor(showcase): rename ITPqc integration test to ITPostQuantumCry…
lqiu96 Jul 21, 2026
1a08829
ci(showcase): replace hardcoded sleep 2 with deterministic Bash /dev/…
lqiu96 Jul 21, 2026
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
30 changes: 30 additions & 0 deletions .github/workflows/showcase.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,22 @@ jobs:
curl --location https://github.com/googleapis/gapic-showcase/releases/download/v${SHOWCASE_VERSION}/gapic-showcase-${SHOWCASE_VERSION}-linux-amd64.tar.gz --output /usr/src/showcase/showcase-${SHOWCASE_VERSION}-linux-amd64.tar.gz
cd /usr/src/showcase/
tar -xf showcase-*
# Start standard insecure showcase server on default port 7469 for standard integration tests
./gapic-showcase run &
# Start secure TLS showcase server on port 7470 for PQC TLS integration tests
./gapic-showcase run --port 7470 --tls --ca-cert-output-file /tmp/showcase-ca.pem &
# Wait deterministically for both background showcase servers to finish binding ports 7469/7470
# and writing /tmp/showcase-ca.pem. Starting TLS requires RSA key generation and disk I/O,
# which can cause race conditions if tests start before /tmp/showcase-ca.pem is created.
for i in $(seq 1 30); do
if (echo > /dev/tcp/127.0.0.1/7469) 2>/dev/null && \
(echo > /dev/tcp/127.0.0.1/7470) 2>/dev/null && \
[ -f /tmp/showcase-ca.pem ]; then
echo "Showcase servers (ports 7469, 7470) and CA cert ready in attempt $i."
break
fi
sleep 0.2
done
cd -
- name: Showcase integration tests
working-directory: java-showcase
Expand Down Expand Up @@ -166,7 +181,22 @@ jobs:
curl --location https://github.com/googleapis/gapic-showcase/releases/download/v${SHOWCASE_VERSION}/gapic-showcase-${SHOWCASE_VERSION}-linux-amd64.tar.gz --output /usr/src/showcase/showcase-${SHOWCASE_VERSION}-linux-amd64.tar.gz
cd /usr/src/showcase/
tar -xf showcase-*
# Start standard insecure showcase server on default port 7469 for standard integration tests
./gapic-showcase run &
# Start secure TLS showcase server on port 7470 for PQC TLS integration tests
./gapic-showcase run --port 7470 --tls --ca-cert-output-file /tmp/showcase-ca.pem &
# Wait deterministically for both background showcase servers to finish binding ports 7469/7470
# and writing /tmp/showcase-ca.pem. Starting TLS requires RSA key generation and disk I/O,
# which can cause race conditions if tests start before /tmp/showcase-ca.pem is created.
for i in $(seq 1 30); do
if (echo > /dev/tcp/127.0.0.1/7469) 2>/dev/null && \
(echo > /dev/tcp/127.0.0.1/7470) 2>/dev/null && \
[ -f /tmp/showcase-ca.pem ]; then
echo "Showcase servers (ports 7469, 7470) and CA cert ready in attempt $i."
break
fi
sleep 0.2
done
cd -
- name: Showcase integration tests
working-directory: java-showcase
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
/*
* 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.gax.core.NoCredentialsProvider;
import com.google.api.gax.httpjson.HttpJsonMetadata;
import com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider;
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 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 javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManagerFactory;
import org.conscrypt.Conscrypt;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

/**
* Integration tests to verify Post-Quantum Cryptography (PQC) TLS negotiation for HTTP/JSON (REST)
* clients.
*
* <p>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:
*
* <ul>
* <li>{@code x-showcase-tls-group}: The negotiated key exchange named group (e.g.
* X25519MLKEM768).
* <li>{@code x-showcase-tls-version}: The TLS version negotiated (e.g. TLS 1.3).
* <li>{@code x-showcase-tls-cipher}: The negotiated cipher suite (e.g. TLS_AES_128_GCM_SHA256).
* <li>{@code x-showcase-tls-client-supported-groups}: The list of groups offered by the client.
* </ul>
*
* <p>Verification cases:
*
* <ol>
* <li>{@code testHttpJsonPqc}: Verifies that HTTP/JSON transport defaults to Conscrypt and
* negotiates the hybrid post-quantum group {@code X25519MLKEM768}.
* <li>{@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.
* </ol>
*/
public class ITPostQuantumCryptography {

// TLS response header names from Showcase server
private static final String TLS_GROUP_HEADER = "x-showcase-tls-group";
private static final String TLS_SUPPORTED_GROUPS_HEADER =
"x-showcase-tls-client-supported-groups";

// Expected TLS parameters
private static final String EXPECTED_PQC_GROUP = "X25519MLKEM768";

private static final String DEFAULT_CA_CERT_PATH = getCaCertPath();

/**
* Resolves the absolute path to the Showcase server's CA certificate PEM file.
*
* @return absolute path to the CA certificate file
*/
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 testHttpJsonPqc() throws Exception {

NetHttpTransport transport =
new NetHttpTransport.Builder()
.setSecurityProvider(Conscrypt.newProvider())
.setSslSocketConfigurator(
socket -> {
if (Conscrypt.isConscrypt(socket)) {
Conscrypt.setNamedGroups(
socket, InstantiatingHttpJsonChannelProvider.DEFAULT_PQC_GROUPS);
}
})
.trustCertificates(null, loadCaCert(DEFAULT_CA_CERT_PATH), "")
.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);
assertThat(negotiatedGroup).isEqualTo(EXPECTED_PQC_GROUP);

String supportedGroups = getSingleHeaderString(capturedHeaders, TLS_SUPPORTED_GROUPS_HEADER);
assertThat(supportedGroups).isNotNull();
}
}

@Test
void testHttpJsonPqc_withExplicitSecurityProviderNoPqcGroups() 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);

// This test verifies client transport behavior when PQC algorithms are not offered.
// Future Java versions (e.g. JDK 27+) will enable PQC (ML-KEM) by default in standard JDK JSSE.
// Explicitly setting named groups to classical algorithms ensures that this test reliably
// tests the non-PQC classical TLS connection path regardless of underlying JDK defaults.
NetHttpTransport transport =
new NetHttpTransport.Builder()
.setSslSocketFactory(sslContext.getSocketFactory())
.setSslSocketConfigurator(
socket -> {
try {
SSLParameters params = socket.getSSLParameters();
params.setNamedGroups(new String[] {"X25519", "SecP256r1"});
socket.setSSLParameters(params);
} catch (Exception e) {
// For JDK 8-19, SSLParameters.setNamedGroups() is unsupported, and JSSE
// naturally
// defaults to classical algorithms. Setting classical named groups is primarily
// for
// JDK 20+ when PQC becomes the default in standard JSSE.
}
})
.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 classical non-PQC configuration, negotiated group is a classical curve
assertThat(negotiatedGroup).isAnyOf("X25519", "SecP256r1", "CurveP256");
assertThat(negotiatedGroup).isNotEqualTo(EXPECTED_PQC_GROUP);
}
}

/**
* Extracts the first string value of a specified HTTP response header from metadata.
*
* @param metadata the HTTP metadata containing response headers
* @param name the case-insensitive header key name
* @return header value string, or {@code null} if not found
*/
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;
}

/**
* Loads an X.509 CA certificate file from disk into a new KeyStore instance.
*
* @param certPath path to the X.509 certificate file
* @return initialized KeyStore containing the certificate entry
* @throws Exception if reading or parsing the certificate fails
*/
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;
}
}
5 changes: 5 additions & 0 deletions sdk-platform-java/gax-java/gax-httpjson/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@
<artifactId>error_prone_annotations</artifactId>
<version>${errorprone.version}</version>
</dependency>
<dependency>
<groupId>org.conscrypt</groupId>
<artifactId>conscrypt-openjdk-uber</artifactId>
<version>${conscrypt.version}</version>
</dependency>
</dependencies>

<build>
Expand Down
Loading
Loading