-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(gax-httpjson): Add Post Quantum Cryptography (PQC) Support by default via Conscrypt #13853
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
708dad6
e1e6de1
8bf75d3
d371aa4
d1ce9cd
086faf0
94d825b
5078cb9
8d936f7
baf5769
afbae79
3f0bec3
e334f80
f17b615
273a3f9
46aaed3
bc541e0
ee8b99d
36f7601
f30f266
a52c88e
7e5b30d
b310138
8e31771
3f3fcd4
18cb2c3
5e0e429
384f672
5004ec3
c28ce89
50e0e64
44ce5a3
6bd0eea
e2f2456
9f6ff2e
c555d29
b236086
d57beae
9b2b287
68b7ee2
615a258
116e4e8
39ebb42
4dafb68
6055878
56eb548
ed50fed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,258 @@ | ||
| /* | ||
| * 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.HttpJsonConscryptUtils; | ||
| 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.cert.Certificate; | ||
| import java.security.cert.CertificateFactory; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import javax.net.ssl.SSLContext; | ||
| import javax.net.ssl.TrustManagerFactory; | ||
| import org.conscrypt.Conscrypt; | ||
| import org.junit.jupiter.api.AfterAll; | ||
| 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"); | ||
|
|
||
| private static SSLContext originalSslContext; | ||
|
|
||
| @BeforeAll | ||
| static void setUp() throws Exception { | ||
| File certFile = new File(DEFAULT_CA_CERT_PATH); | ||
| assertWithMessage("CA certificate file not found at " + DEFAULT_CA_CERT_PATH) | ||
| .that(certFile.isFile()) | ||
| .isTrue(); | ||
|
|
||
| try { | ||
| originalSslContext = SSLContext.getDefault(); | ||
| } catch (Throwable t) { | ||
| // Ignore if default SSLContext cannot be retrieved | ||
| } | ||
|
|
||
| // Register local Showcase CA cert in default SSLContext so the default NetHttpTransport trusts | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this step needed for PQC testing?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for TLS negotation, we need to establish a TLS connection. The showcase cert is a local, self-signed (I think) certificate and we need the ensure that the SSLContext can trust it. Essentially we are adding the local ca cert into our truststore |
||
| // the server | ||
| KeyStore trustStore = loadCaCert(DEFAULT_CA_CERT_PATH); | ||
| TrustManagerFactory tmf = | ||
| TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); | ||
| tmf.init(trustStore); | ||
| SSLContext sslContext = SSLContext.getInstance("TLS"); | ||
| sslContext.init(null, tmf.getTrustManagers(), null); | ||
| SSLContext.setDefault(sslContext); | ||
| } | ||
|
lqiu96 marked this conversation as resolved.
|
||
|
|
||
| @AfterAll | ||
| static void tearDown() { | ||
| if (originalSslContext != null) { | ||
| try { | ||
| SSLContext.setDefault(originalSslContext); | ||
| } catch (Throwable t) { | ||
| // Ignore during test cleanup | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| void testHttpJsonPqc() throws Exception { | ||
| HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor(); | ||
|
|
||
| // Test that the Java client creates a PQC-compliant NetHttpTransport by default (no custom | ||
| // transport provided) | ||
| InstantiatingHttpJsonChannelProvider transportChannelProvider = | ||
| EchoSettings.defaultHttpJsonTransportProviderBuilder() | ||
| .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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do these two headers mean? Are they just a way to verify correct algorithms are used?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From Alex's guide: https://github.com/googleapis/gapic-showcase/blob/8d881c63e8fddb82db17d56dcf2130400602d25a/TLS_GUIDE.md?plain=1#L89-L90 group header is the actual negotiated group selected |
||
| assertThat(negotiatedGroup).isEqualTo(EXPECTED_PQC_GROUP); | ||
|
|
||
| String supportedGroups = getSingleHeaderString(capturedHeaders, TLS_SUPPORTED_GROUPS_HEADER); | ||
| assertThat(supportedGroups).isNotNull(); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| void testHttpJsonPqc_withExplicitClassicalGroupsNoPqc() throws Exception { | ||
| // This test explicitly configures non-PQC classical groups (X25519, SecP256r1) on Conscrypt | ||
| // to verify that the client falls back gracefully to classical TLS key exchange. | ||
| NetHttpTransport transport = | ||
| HttpJsonConscryptUtils.configureConscryptSecurityProvider(new NetHttpTransport.Builder()) | ||
| .setSslSocketConfigurator( | ||
| socket -> { | ||
| if (Conscrypt.isConscrypt(socket)) { | ||
| Conscrypt.setNamedGroups(socket, new String[] {"X25519", "SecP256r1"}); | ||
| } | ||
| }) | ||
|
Comment on lines
+182
to
+187
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In .setSslSocketConfigurator(
socket -> {
if (Conscrypt.isConscrypt(socket)) {
Conscrypt.setNamedGroups(socket, new String[] {"X25519", "SecP256r1"});
} else {
try {
javax.net.ssl.SSLParameters params = socket.getSSLParameters();
java.lang.reflect.Method setNamedGroupsMethod =
javax.net.ssl.SSLParameters.class.getMethod("setNamedGroups", String[].class);
setNamedGroupsMethod.invoke(params, (Object) new String[] {"X25519", "SecP256r1"});
socket.setSSLParameters(params);
} catch (Exception e) {
// Ignore if setNamedGroups is not supported on this JDK version
}
}
}) |
||
| .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; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use the secure TLS server for regular showcase tests?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, I believe we can. We will just need to update all of the showcase clients to trust the local CA cert generated for the showcase server (since right now it trusts the well known public CA certs by default).
I think it's a bit more overhead unless you need to specifically test a TLS aspect (e.g. PQC TLS negotiation)