-
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
3c3791d
ddb9c45
fc99684
aef31bb
39e81d0
1bda19d
41974ff
5c62b5b
ecd7e3e
68d88b1
1789e01
d3b04b7
474f5a1
e98469b
1a82a7c
b7ab212
363cc2c
656ee66
0df8266
f399419
4d00949
375edf8
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,269 @@ | ||
| /* | ||
| * 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.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| 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_withExplicitNonPqcGroup}: Verifies that explicitly configuring | ||
| * classical non-PQC key exchange groups (e.g. {@code X25519}) forces classical key exchange. | ||
| * Explicitly setting the group ensures test compatibility across all JDK versions, including | ||
| * future JDK releases (such as JDK 27+) where PQC will be enabled by default. | ||
| * </ol> | ||
| */ | ||
| 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 CLASSICAL_X25519_GROUP = "X25519"; | ||
| private static final String[] EXPLICIT_NON_PQC_GROUPS = new String[] {CLASSICAL_X25519_GROUP}; | ||
|
|
||
| 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() 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(); | ||
| } | ||
|
|
||
| @Test | ||
| void testHttpJsonPqc() throws Exception { | ||
| HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor(); | ||
|
|
||
| // Construct a dedicated NetHttpTransport configured with Conscrypt security provider | ||
| // and explicitly trusted Showcase CA certificate. This avoids modifying the global JVM | ||
| // SSLContext (via SSLContext.setDefault) and ensures Conscrypt's TLS engine is used. | ||
| NetHttpTransport transport = | ||
| HttpJsonConscryptUtils.configureConscryptSecurityProvider(new NetHttpTransport.Builder()) | ||
| .trustCertificates(loadCaCert(DEFAULT_CA_CERT_PATH)) | ||
| .build(); | ||
|
|
||
| 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); | ||
|
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
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. Thanks! IIUC, supportedGroups would be the
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. Pretty much. I'm going to use the constant in the Util class. Looks like AI made the check non-null when I removed the visibility. Since showcase needs to reference it, I'll mark the cosntant as public
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. Cool, I don't see this showcase test reference it yet though. Also I don't think we should expose a field as public just for 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. It's supposed to be compared for the
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. Removed the constant and made it package-private |
||
| assertThat(negotiatedGroup).isEqualTo(EXPECTED_PQC_GROUP); | ||
|
|
||
| // Assert that supported groups offered by Conscrypt include the primary PQC group and | ||
| // classical fallback group. We use containsAtLeast instead of exact list equality because | ||
| // Go's crypto/tls library only recognizes standard Curve IDs (e.g. X25519MLKEM768 and | ||
| // X25519), which is not a 1:1 mapping with the full list of named groups that Conscrypt | ||
| // supports (see | ||
| // https://github.com/google/conscrypt/blob/2.6.0/CAPABILITIES.md#supported-named-groups). | ||
| // Draft/standalone groups like X25519Kyber768Draft00 and MLKEM1024 are formatted as | ||
| // "Unknown-Curve-25497" and "Unknown-Curve-514" by the Showcase server. | ||
| List<String> supportedGroups = | ||
| getHeaderStringList(capturedHeaders, TLS_SUPPORTED_GROUPS_HEADER); | ||
| assertThat(supportedGroups).containsAtLeast(EXPECTED_PQC_GROUP, CLASSICAL_X25519_GROUP); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| void testHttpJsonPqc_withExplicitNonPqcGroup() throws Exception { | ||
| HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor(); | ||
|
|
||
| // Explicitly configure Conscrypt socket with classical X25519 group. This verifies that | ||
| // custom socket configurators can override default PQC groups with non-PQC groups. | ||
| // Explicitly passing a non-PQC curve ensures that the test deterministically verifies | ||
| // classical key exchange fallback across all JDK versions, including future JDK 27+ releases | ||
| // where PQC algorithms will be enabled by default in standard JDK security providers. | ||
| NetHttpTransport transport = | ||
| HttpJsonConscryptUtils.configureConscryptSecurityProvider(new NetHttpTransport.Builder()) | ||
| .setSslSocketConfigurator( | ||
| socket -> { | ||
| if (Conscrypt.isConscrypt(socket)) { | ||
| try { | ||
| Conscrypt.setNamedGroups(socket, EXPLICIT_NON_PQC_GROUPS); | ||
| } catch (Exception ignored) { | ||
| } | ||
| } | ||
| }) | ||
| .trustCertificates(loadCaCert(DEFAULT_CA_CERT_PATH)) | ||
| .build(); | ||
|
|
||
| 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-jsse-test").build()); | ||
| assertThat(response.getContent()).isEqualTo("pqc-httpjson-jsse-test"); | ||
|
|
||
| HttpJsonMetadata capturedHeaders = interceptor.metadata; | ||
| assertThat(capturedHeaders).isNotNull(); | ||
|
|
||
| String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER); | ||
| assertThat(negotiatedGroup).isEqualTo(CLASSICAL_X25519_GROUP); | ||
|
|
||
| List<String> supportedGroups = | ||
| getHeaderStringList(capturedHeaders, TLS_SUPPORTED_GROUPS_HEADER); | ||
| assertThat(supportedGroups).containsExactlyElementsIn(Arrays.asList(EXPLICIT_NON_PQC_GROUPS)); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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; | ||
| } | ||
|
|
||
| /** | ||
| * Extracts a list of string values from a comma-separated HTTP response header. | ||
| * | ||
| * @param metadata the HTTP metadata containing response headers | ||
| * @param name the case-insensitive header key name | ||
| * @return list of header string tokens, or empty list if not found | ||
| */ | ||
| private static List<String> getHeaderStringList(HttpJsonMetadata metadata, String name) { | ||
| String value = getSingleHeaderString(metadata, name); | ||
| if (value == null || value.trim().isEmpty()) { | ||
| return Collections.emptyList(); | ||
| } | ||
| return Arrays.asList(value.split(",")); | ||
| } | ||
|
|
||
| /** | ||
| * 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)