Add named groups (supported_groups) to ConnectionSpec for post-quantum key exchange#9517
Add named groups (supported_groups) to ConnectionSpec for post-quantum key exchange#9517yschimke wants to merge 14 commits into
Conversation
Allow configuring the TLS 1.3 supported_groups extension on a ConnectionSpec, so callers can require or prefer post-quantum hybrid key exchange such as X25519MLKEM768. - New NamedGroup enum modelling the PQC hybrids, classical curves and ffdhe groups, with raw-string escape hatches on the builder. - ConnectionSpec.Builder.namedGroups(...) / allEnabledNamedGroups(). - Applied via SSLParameters.setNamedGroups reflectively, available on Java 20+ and recent Conscrypt; silently ignored on older platforms. - Defaults unchanged (null = defer to the platform). Refs square#9497
…WebServer Replace the reflective SSLParameters.setNamedGroups call with a multi-release JAR variant: - okhttp3.internal.platform.NamedGroups (compiled from NamedGroups.kt) is a no-op base used on Android and Java < 20. - src/jvmMain/java20 provides a Java 20 override packaged into META-INF/versions/20, calling SSLParameters.setNamedGroups directly. No reflection, no runtime version checks. Also add MockWebServer.namedGroups so the server side can require a named group (e.g. a PQC hybrid) during the handshake, enabling end-to-end tests.
Two gated tests in container-tests, where OkHttp is consumed as the multi-release jar so the Java 20 setNamedGroups variant is active: - PostQuantumMockWebServerTest: loopback MockWebServer restricted to X25519MLKEM768. A PQC client connects (positive) and a classical-only client is rejected (negative), proving the server restriction. - PostQuantumContainerTest: openssl s_server (-groups X25519MLKEM768) in a container; a successful HTTPS response proves PQC negotiation. Both skip unless the client provider supports PQC key exchange (JDK 27+ / Conscrypt 2.6+).
Conscrypt 2.6 adds the X25519MLKEM768 hybrid named group and SSLParameters.setNamedGroups support, so the post-quantum tests can run on the conscrypt platform lane (JDK 20+) rather than only on JDK 27. Register PlatformRule in the PQC container tests so Conscrypt is installed as the security provider on that lane, and widen the capability gate to JDK 27+ OR Conscrypt.
A PQC-only server runs as a Docker container on the runner host; the emulator reaches it at 10.0.2.2. PostQuantumAndroidTest installs the bundled Conscrypt 2.6 provider and requires X25519MLKEM768, skipping unless the pqcServerUrl instrumentation arg is supplied. The android-containers workflow starts the openssl s_server container, then runs the test on the emulator. Emulator config is copied from the android job in build.yml as a starting point. Applying ConnectionSpec.namedGroups on Android needs the Android implementation of applyNamedGroups, which lands separately.
Conscrypt only gained X25519MLKEM768 and SSLParameters.setNamedGroups in 2.6, so gate the post-quantum tests on Conscrypt.version() >= 2.6 rather than assuming any Conscrypt provider supports it.
Split the Android PQC test into two: one that installs the bundled conscrypt-android (2.6+), and a systemProviderNegotiatesPostQuantumGroup probe that uses the device's system TLS stack with no bundled provider. The probe runs only on API 37+ (in the experimental, non-blocking emulator lane) to empirically answer whether the platform itself negotiates X25519MLKEM768.
systemProviderDoesNotYetNegotiatePostQuantumGroup now asserts the handshake FAILS: we expect the API 37 system TLS stack not to negotiate X25519MLKEM768. If it unexpectedly succeeds the test fails loudly, signalling that platform PQC has landed and OkHttp's Android handling should be updated.
Conscrypt 2.6-alpha4 registers an XDH KeyPairGenerator whose initialize(AlgorithmParameterSpec) always throws, but SunJSSE's XDHKeyExchange passes a NamedParameterSpec. So when the alpha is a registered JSSE provider it breaks classical X25519 (e.g. Robolectric's Maven fetch in :android-test:testDebugUnitTest, and the conscrypt jvmTest lane). Revert the global org-conscrypt to the stable 2.5.2 and add a separate conscrypt-pqc (2.6-alpha4) used only where Conscrypt drives the whole handshake via its own SSLContext (the instrumented PQC test and the conscrypt container-tests lane), which bypasses the broken JCA path.
The default container-tests run is JDK 21 + SunJSSE, where the PQC tests skip. Add a Conscrypt-platform pass that re-runs just PostQuantum* so X25519MLKEM768 is actually negotiated. Non-blocking, since it depends on a pre-release Conscrypt.
Prior art: how other HTTP/TLS stacks configure named groupsNamed-group / post-quantum key-exchange configuration varies a lot across ecosystems — from per-client (Go), to per-provider (rustls), to a global JVM property (Netty/JDK), to no app-level API at all (Python). OkHttp's Rust — use rustls::crypto::{aws_lc_rs, CryptoProvider};
let provider = CryptoProvider {
kx_groups: vec![
rustls_post_quantum::X25519MLKEM768,
aws_lc_rs::kx_group::X25519,
aws_lc_rs::kx_group::SECP256R1,
],
..aws_lc_rs::default_provider()
};
provider.install_default().unwrap(); // process-wide; or .with_crypto_provider() per ClientConfigDocs: Go — client := &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{
CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.X25519},
},
}}
Netty (JVM) — no first-class API For the OpenSSL/ Python — import ssl
ctx = ssl.create_default_context()
ctx.set_ecdh_curve("x25519") # one curve onlyA multi-group |
Should OkHttp set named groups by default (and how does
|
Move the android-pqc emulator job into the containers workflow and delete the standalone android-containers.yml. It's gated like test_containers (master or the 'containers' label) and adds workflow_dispatch for manual runs.
| sudo udevadm trigger --name-match=kvm | ||
|
|
||
| - name: Setup Gradle | ||
| uses: gradle/actions/setup-gradle@v5 |
|
|
||
| - name: Create AVD and generate snapshot for caching | ||
| if: steps.avd-cache.outputs.cache-hit != 'true' | ||
| uses: reactivecircus/android-emulator-runner@v2 |
| # The emulator reaches the host container at 10.0.2.2; pass the endpoint to the test and run | ||
| # only the PQC test class. Boot options must match the snapshot step above. | ||
| - name: Run post-quantum Android test | ||
| uses: reactivecircus/android-emulator-runner@v2 |
| sudo udevadm trigger --name-match=kvm | ||
|
|
||
| - name: Setup Gradle | ||
| uses: gradle/actions/setup-gradle@v5 |
|
|
||
| - name: Create AVD and generate snapshot for caching | ||
| if: steps.avd-cache.outputs.cache-hit != 'true' | ||
| uses: reactivecircus/android-emulator-runner@v2 |
| # The emulator reaches the host container at 10.0.2.2; pass the endpoint to the test and run | ||
| # only the PQC test class. Boot options must match the snapshot step above. | ||
| - name: Run post-quantum Android test | ||
| uses: reactivecircus/android-emulator-runner@v2 |
Replace the NamedGroup enum with a class that wraps javaName, exposes the
known groups as constants, and interns via forJavaName(). forJavaName now
accepts arbitrary names, so unknown groups round-trip through the typed
ConnectionSpec.namedGroups view instead of being dropped.
Drop the redundant Builder.namedGroups(vararg String) overload: callers
pass NamedGroup.forJavaName("...") for groups not enumerated here.
Drop the interning map and synchronized forJavaName; equality/hashCode are based on javaName, so two NamedGroups with the same name are equal without a shared cache.
Resolves #9497.
Summary
Adds the ability to configure TLS 1.3 named groups (the
supported_groupsextension) on aConnectionSpec, so callers can require or prefer post-quantum hybrid key exchange such asX25519MLKEM768. PreviouslyConnectionSpeconly exposed cipher suites and TLS versions, and the key-exchange algorithm is negotiated separately viasupported_groups— so there was no way to require, restrict, or prefer PQC key exchange.