skip to content
QUANTUM NEXUM

← vault

Post-quantum TLS.

PQ-TLS means running a TLS 1.3 handshake where the key exchange step uses a hybrid algorithm — a classical ECDH component combined with an ML-KEM component. Either component alone is sufficient to protect the session key; an attacker must break both to recover it.

This page covers the TLS mechanics. For the hybrid construction itself — key sizes, the combiner pseudocode, and the broader use case outside TLS — see hybrid key exchange.

Why TLS 1.3 specifically

Hybrid key exchange requires the TLS 1.3 key share extension (RFC 8446 §4.2.8). TLS 1.2 has no equivalent mechanism — it negotiates a key exchange algorithm by name but cannot carry an arbitrary-length key share in the same handshake. PQ-TLS therefore requires TLS 1.3 on both ends.

Named groups

The three hybrid groups in active deployment are registered in draft-ietf-tls-ecdhe-mlkem (an IETF draft; not yet an RFC as of mid-2026).

Name                  Group ID  Classical        PQ             Security
X25519MLKEM768        0x11EC    X25519           ML-KEM-768     ~128-bit classical + NIST Level 3
SecP256r1MLKEM768     0x11EB    ECDH P-256       ML-KEM-768     P-256 (FIPS-approved curve) + Level 3
SecP384r1MLKEM1024    0x11ED    ECDH P-384       ML-KEM-1024    P-384 (FIPS-approved curve) + Level 5

X25519MLKEM768 is the browser default. The P-256 and P-384 variants use NIST-approved curves and are preferred in environments that restrict the classical component to FIPS-approved curves.

Key share sizes on the wire

Group Client key share Server key share (response) On wire (server, incl. 4 B header)
X25519MLKEM768 32 + 1,184 = 1,216 B 32 (X25519) + 1,088 (ML-KEM ciphertext) = 1,120 B 1,124 B
SecP256r1MLKEM768 65 + 1,184 = 1,249 B 65 + 1,088 = 1,153 B 1,157 B
SecP384r1MLKEM1024 97 + 1,568 = 1,665 B 97 + 1,568 = 1,665 B 1,669 B

The +4 B header is the TLS extension type (2 B) and length (2 B) fields. The diagnostic line TLS server extension "key share" (id=51), len=1124 from -tlsextdebug confirms X25519MLKEM768 was negotiated; len=32 means classical-only.

Handshake flow

The hybrid key exchange fits entirely within the standard TLS 1.3 handshake — no extra round trips.

Client                                       Server
  |                                            |
  |--- ClientHello (key_share: MLKEM group) -->|
  |    supported_groups includes 0x11EC        |
  |    key_share carries 1,216 B               |
  |                                            |
  |<-- ServerHello (key_share response) -------|
  |    chosen group: X25519MLKEM768            |
  |    server key share: 1,120 B               |
  |    (X25519 ephemeral + ML-KEM ciphertext)  |
  |                                            |
  |    Both sides derive:                      |
  |      x25519_ss  from the X25519 exchange   |
  |      mlkem_ss   from ML-KEM Encaps/Decaps  |
  |      combined = HKDF(mlkem_ss || x25519_ss)|
  |                                            |
  |<-- {EncryptedExtensions, Certificate,      |
  |     CertificateVerify, Finished} ----------|
  |--- {Finished} ---------------------------->|
  |                                            |
  |<=== Application data (TLS_AES_256_GCM) ===>|

The shared secret concatenation order for X25519MLKEM768 per draft-ietf-tls-ecdhe-mlkem is mlkem_ss || classical_ss. The server certificate uses a classical signature algorithm for authentication; hybrid key exchange protects only the session key derivation.

Local testing with OpenSSL

The most reliable way to verify your OpenSSL installation supports hybrid groups is a local loopback test — no external server required. OpenSSL 3.5+ includes native ML-KEM support in its default provider. For older OpenSSL 3.x, install the OQS provider (see below).

Check for ML-KEM support

openssl list -kem-algorithms | grep -i mlkem
openssl version

Start a local test server

# Generate a self-signed certificate for the loopback server
openssl req -x509 -newkey ML-DSA-65 -keyout key.pem -out cert.pem \
    -days 365 -nodes -subj "/CN=localhost"

# Start s_server advertising X25519MLKEM768
openssl s_server -accept 14433 -cert cert.pem -key key.pem \
    -groups X25519MLKEM768 -www -tls1_3

Connect from a second terminal

openssl s_client -connect localhost:14433 \
    -groups X25519MLKEM768 \
    -tlsextdebug </dev/null 2>&1 | grep -E "key share|Negotiated|Cipher"

A successful hybrid negotiation shows key share (id=51), len=1124 and Negotiated TLS1.3 group: X25519MLKEM768. If you see len=32, the server or client fell back to classical X25519 only.

Test all three groups

# X25519 + ML-KEM-768 (default, widely supported)
openssl s_client -connect localhost:14433 -groups X25519MLKEM768 -brief

# P-256 + ML-KEM-768 (FIPS-approved curve for classical component)
openssl s_client -connect localhost:14433 -groups SecP256r1MLKEM768 -brief

# P-384 + ML-KEM-1024 (highest security, less widely deployed)
openssl s_client -connect localhost:14433 -groups SecP384r1MLKEM1024 -brief

Third-party public endpoints

Cloudflare operates a public PQ research endpoint at pq.cloudflareresearch.com that can be used to test client-side hybrid support against a production server. Output will vary; do not rely on any specific response format.

OpenSSL: native vs. OQS provider

OpenSSL 3.5+ (released April 2025) includes ML-KEM, ML-DSA, and SLH-DSA natively in its default provider. No OQS provider needed for any of these algorithms if you are on 3.5 or later. The OQS provider is still useful for older OpenSSL 3.x installs or for accessing experimental algorithms not yet in OpenSSL upstream.

OpenSSL 3.5+      native ML-KEM, ML-DSA, SLH-DSA in default provider
                  use: openssl s_client -groups X25519MLKEM768 ...

OpenSSL 3.x       requires oqs-provider for ML-KEM hybrid groups
                  use: OPENSSL_MODULES=/path/to/ossl-modules \
                       openssl s_client -provider oqsprovider -provider default \
                       -groups X25519MLKEM768 ...

If you need to build the OQS provider, use the latest releases (liboqs 0.15.x / oqs-provider 0.11.x as of early 2026) rather than pinning to older version tags.

Diagnosing fallback

Symptom Likely cause Resolution
no shared group Client or server does not recognise the hybrid group ID Upgrade to OpenSSL 3.5+ or install the OQS provider on whichever side is older
key share (id=51), len=32 Classical X25519 was negotiated instead of hybrid Verify -groups X25519MLKEM768 on both sides; check the server config lists the hybrid group first
Connection refused on port Server not started or firewall blocking the port Confirm openssl s_server is running; use localhost for loopback tests
Certificate verify error Self-signed certificate not trusted by client Expected for loopback tests; add -CAfile cert.pem to s_client to suppress, or ignore for key-exchange testing
Large key shares blocked Firewall or WAF drops packets larger than ~1 KB Reconfigure the middlebox; hybrid shares are ~1.1–1.7 KB vs 32 B for classical X25519

Browser support

Browser Hybrid PQ-TLS Default group Notes
Chrome 131+ Enabled by default X25519MLKEM768 Shipped final ML-KEM (FIPS 203). Chrome 124 shipped an earlier pre-standard Kyber draft — not the same codepoint.
Firefox 132+ Enabled by default X25519MLKEM768
Edge Follows Chrome X25519MLKEM768 Chromium-based.
Safari 26+ Enabled by default X25519MLKEM768

curl PQ support depends on which TLS backend it was compiled against (OpenSSL, GnuTLS, BoringSSL, etc.), not the curl version number itself. Check curl --version for the linked library.

Server configuration

To advertise hybrid groups on your server, add the hybrid group name to the curves / groups list. Placing it first signals that you prefer it, but the client's preference wins in TLS 1.3.

# Nginx (requires OpenSSL 3.5+ or OQS provider)
ssl_protocols TLSv1.3;
ssl_ecdh_curve X25519MLKEM768:X25519:P-256;

# Apache
SSLProtocol TLSv1.3
SSLOpenSSLConfCmd Groups X25519MLKEM768:X25519

# HAProxy
ssl-default-bind-ciphersuites TLS_AES_256_GCM_SHA384
ssl-default-bind-curves X25519MLKEM768:X25519

# Caddy 2.9+ (Go 1.24 crypto/tls)
tls {
    curves x25519mlkem768 x25519
}

Caddy 2.9+ is required for hybrid group support; that release moved to Go 1.24 which added the hybrid curves to crypto/tls.

Obsolete draft groups

Before the final ML-KEM standard was published, some software shipped provisional codepoints for pre-standard Kyber. The most common was x25519_kyber768 (codepoint 0x6399), used by older OQS provider releases. This group is not interoperable with X25519MLKEM768 (0x11EC): the codepoints differ, the KEM itself is pre-standard Kyber rather than ML-KEM, and the share concatenation order is reversed. Do not configure both groups on the same server expecting them to interop.

Further reading