v0.1 — interactive · hybrid TLS handshake · X25519MLKEM768 codepoint 0x11ec

X25519 + ML-KEM-768, in handshake

Two key exchanges, run in parallel, mixed into one shared key. Classical X25519 protects you against everything that's not a quantum computer; ML-KEM-768 protects you against quantum computers but hasn't been deployed long enough to be fully trusted on its own. So we run both. Walk through the six-message handshake, see what's on the wire, and watch HKDF combine the two secrets into one.

/01

why hybrid?

// the classical risk

Recorded today, decrypted later

A passive eavesdropper can record encrypted traffic now and store it. If a sufficiently large quantum computer arrives in 2035, the recording can be decrypted retroactively — every X25519 handshake ever performed is a vulnerable past secret.

For data that needs to stay confidential for more than a few years — health records, classified material, financial fraud history — this is a present-day attack, not a future one. The mitigation has to be deployed now, even though the quantum computer doesn't exist yet.

// the post-quantum risk

New algorithms haven't been attacked enough

Lattice cryptography is the leading candidate to resist quantum attacks, but it was standardized in 2024 — only a handful of years of public attack research. AES has had 25 years; RSA has had 45. A subtle flaw in ML-KEM that's discovered in 2030 would compromise everyone who deployed it standalone in 2026.

Meanwhile, the parameter sizes are tuned aggressively: ML-KEM-512 and -768 have failed several side-channel attacks in implementations. The math holds up; specific code paths haven't.

Hybrid key exchange is the answer to both risks at once. Run X25519 and ML-KEM-768 simultaneously, derive a single shared key from both via HKDF, and require both to be broken for the session to leak. An attacker needs a flaw in and a quantum computer.

The cost is small: a slightly larger ClientHello (~1.2 KB extra), a few hundred microseconds of additional CPU per handshake, and a small bandwidth bump. Negligible compared to the asymmetry of the threat.

The internet has been quietly converging on this since 2023. Cloudflare offered hybrid first; Chrome shipped it; AWS, Google, and most major CDNs followed. As of 2026, the X25519MLKEM768 codepoint is the most common post-quantum-touched key exchange on the public web.

/02

the two halves

// classical half

X25519 ECDH on Curve25519

A specific elliptic curve (Bernstein, 2006) chosen for speed and side-channel safety. Both parties scalar-multiply a shared generator with their private exponent; the resulting points have identical x-coordinates after a final reciprocal exchange.

private scalar32 B (256 bits)
public point32 B (compressed)
shared secret32 B
classical security~128 bits
quantum security~0 bits — Shor
// post-quantum half

ML-KEM-768 module-LWE KEM

NIST FIPS 203, formerly Kyber. The client publishes a public key derived from a noisy lattice point; the server encapsulates a random 32-byte secret into a ciphertext using that public key; the client decapsulates it with their secret matching key.

private key2400 B
public key1184 B
ciphertext1088 B
shared secret32 B
claimed PQ security192 bits (NIST cat 3)

These two halves do the same job — produce a 32-byte shared secret between two parties — but through completely different math. The hybrid trick: run them in parallel, then mix the two 32-byte secrets through HKDF-SHA-256 to produce a single 32-byte key for the session.

/03

the handshake, step by step

Below is the X25519MLKEM768 handshake walked through with toy parameters small enough to follow on paper. The classical half uses our toy curve y² = x³ + 2x + 2 (mod 17), generator G = (5, 1). The post-quantum half uses a 4-dimensional LWE problem mod 17 — the same Regev scheme as the ML-KEM-DSA app. The structure is identical to the real protocol; only the bytes are smaller.

Step 0 of 6 Idle — press Next to begin
Client
// initiator
// the wire
— silence —
Server
// responder
step 0 of 6
// what's happening at this step

The handshake before it starts

Both sides have the same public parameters: the elliptic curve equation, the generator G, the LWE modulus and dimension. They haven't exchanged anything yet.

Press Next to step through the six messages of the handshake. Each step shows what one side has computed and what gets added to the wire. The wire panel always shows what an eavesdropper could observe.

/04

HKDF — the combiner

Both halves of the handshake produce a 32-byte shared secret. To get a single key for AES-GCM, we mix them through HKDF-SHA-256 (RFC 5869) — the standard key-derivation function used everywhere in TLS 1.3.

HKDF has two steps: extract (HMAC the input keying material under a salt to get a uniformly-distributed pseudorandom key) and expand (use that PRK to produce as many bytes of output as you need, with a context label).

The result is computed below using your browser's real Web Crypto implementation — the same SHA-256 + HMAC code that runs your TLS handshakes. The toy hybrid handshake above feeds its outputs in; you can also try your own inputs.

// derived key
awaiting derivation
// why HKDF?

Why not just use the secrets as keys directly?

A shared secret out of ECDH or ML-KEM is uniformly random bytes — but only "uniform" in a specific cryptographic sense. The output of x·y·G on a curve isn't pure randomness; it's a curve point with structure. The output of ML-KEM has a specific distribution. Using these directly as AES keys would be fine in most cases but not always — and certainly not when you have two different shared secrets from different schemes that you need to combine.

HKDF normalizes everything. It takes the concatenation ECDH_shared || KEM_shared, hashes it through HMAC, and produces a uniformly-random 32-byte key with proper randomness properties — even if one of the inputs turns out to have hidden structure. The salt and info parameters bind the key to the protocol context, preventing cross-protocol reuse.

For TLS 1.3 specifically, the key schedule in RFC 8446 §7.1 already uses HKDF at every transition. Hybrid PQ KEX simply substitutes the concatenated secret in place of the classical-only secret at the first step.

/05

wire format and deployment

The X25519MLKEM768 codepoint (0x11ec) is a single TLS 1.3 NamedGroup that the client offers in supported_groups and supplies a key share for in key_share. The wire format concatenates the post-quantum key first, then the classical public key — a deliberate ordering chosen so that PQ migration is easy to track in packet captures.

// ClientHello.key_share for X25519MLKEM768
fieldcontentssize
NamedGroup0x11ec — X25519MLKEM7682 B
key_exchange.length12162 B
ML-KEM-768 public key1184 bytes of polynomial-encoded lattice data1184 B
X25519 public key32 bytes — compressed Montgomery x-coordinate32 B
total1220 B
// ServerHello.key_share for X25519MLKEM768
fieldcontentssize
NamedGroup0x11ec — X25519MLKEM7682 B
key_exchange.length11202 B
ML-KEM-768 ciphertext1088 bytes — the encapsulation1088 B
X25519 public key32 bytes — server's ephemeral public point32 B
total1124 B

Compared to plain X25519 (each side's key_share is 32 bytes), hybrid adds about 2.3 KB per handshake. On a slow connection that's an extra round-trip's worth of bytes, but TLS 1.3 sends ClientHello and ServerHello in the first packet anyway. In practice, the extra latency is unmeasurable for most users.

// X25519+ML-KEM-768 deployment timeline
2022
NIST selects Kyber as one of four post-quantum standards. Cloudflare announces X25519Kyber768Draft00 codepoint experimental support.
2023
Chrome 124 enables X25519Kyber768Draft00 by default for compatible servers. Cloudflare reports ~3% of TLS handshakes go post-quantum by end of year.
2024
FIPS 203 published; Kyber renamed to ML-KEM. New codepoint X25519MLKEM768 (0x11ec) standardized in draft-kwiatkowski-tls-ecdhe-mlkem. Migration from draft codepoint underway.
2025
Apple iMessage PQ3 ships. OpenSSH 9.9 makes sntrup761x25519 default; ML-KEM hybrid follows. Signal Protocol moves to PQXDH (X25519 + ML-KEM-1024). Chrome, Firefox, Safari all ship X25519MLKEM768.
2026
Estimated >40% of TLS 1.3 handshakes on the public web use X25519MLKEM768. AWS, Google Cloud, Cloudflare, Fastly all default to hybrid. Common Linux distributions enable by default in OpenSSH and OpenSSL.
2033
CNSA 2.0 deadline — U.S. national security systems must use ML-KEM-1024 (not the -768 variant), and CNSA 2.0 specifies ML-KEM only, no hybrid: classical and PQ side-by-side is overkill for NSS, where the threat model assumes a CRQC by then.
// hybrid vs. pure-PQ — when each makes sense

Two camps, two threat models

General-purpose TLS — public web, e-commerce, messaging — uses hybrid because the cost of an undiscovered ML-KEM flaw is catastrophic at scale. Adding 1.2 KB to a handshake to keep X25519 as a safety net is an obvious trade.

U.S. national security systems under CNSA 2.0 are moving to pure ML-KEM-1024 with no classical fallback. The threat model assumes a quantum-capable adversary by 2033; keeping a classical algorithm in the chain just means there's an algorithm still vulnerable to that adversary in your system. Cleaner to drop it.

Both positions are coherent. The choice is about which of two failure modes you're more worried about — undiscovered PQ flaws (favor hybrid) or maintaining a known-broken algorithm in the protocol (favor pure PQ). Most of the internet falls in the first camp; the U.S. government falls in the second.