Crosslink can mix an ephemeral ML-KEM-768 shared secret into the existing
CLX1 X25519 key schedule (packages/core/src/handshake.ts, using
@noble/post-quantum/ml-kem.js). Both algorithms must succeed: the classical
authenticated exchange preserves current security guarantees, and the KEM adds
protection against store-now-decrypt-later attacks if ML-KEM remains secure
when a future quantum computer arrives.
How the exchange is layered onto CLX1
During clientBeginSession, when hybridPq isn’t disabled, the client
generates an ML-KEM-768 keypair and includes the encapsulation key in the
sinit frame:
The host encapsulates against that key in hostCompleteSession and returns
the ciphertext in the sack accept frame. Both sides then derive the same
pqShared secret — the client via ml_kem768.decapsulate, the host as the
direct output of ml_kem768.encapsulate — and mix it into the key schedule:
sharedE (ephemeral X25519 ECDH) and sharedS (static X25519 ECDH) are exactly
what a classical CLX1 handshake already derives — see
Encryption. When PQ is active, pqShared is
concatenated on the end and the HKDF info string changes from
crosslink-session-keys-v1 to crosslink-session-keys-v1+ml-kem-768, so a
hybrid-derived key can never collide with a classical one even if pqShared
happened to be all zero bytes.
The client’s KEM public key, the host’s ciphertext, and the negotiated suite
name are all included in the signed handshake transcript (transcript() in
handshake.ts) before either side has a shared secret. An on-path
intermediary cannot strip, substitute, or downgrade the PQ offer without
invalidating the Ed25519 signature over that transcript — the same
tamper-evidence property the classical exchange already has, extended to
cover the new fields.
Validation and failure modes
Both sides validate KEM material lengths and the suite name before use, and
reject a mismatched negotiation rather than silently falling back:
An encapsulation key or ciphertext of the wrong byte length is rejected before
it reaches ml_kem768.encapsulate/decapsulate, with
ErrorCodes.INVALID_MESSAGE. The client’s KEM secret key and the derived
pqShared bytes are zeroed (.fill(0)) as soon as the traffic keys are
derived, whether or not the derivation succeeds.
preferred permits an authenticated legacy peer to negotiate classical mode —
that’s what makes it safe to roll out gradually. Use required only once
every deployed peer supports the hybrid extension; it intentionally fails
closed during a downgrade attempt or a rollout mismatch, so turning it on
before every client and host are updated will lock out the ones that aren’t.
Choosing a mode
disabled — current default. No behavior change, smallest handshake.
preferred — turn this on first. It adds PQ protection wherever both
peers support it, and degrades gracefully everywhere else, so it’s safe to
ship before every device is updated.
required — turn this on only after preferred has been deployed to
every host and client in your fleet for long enough that no legacy peer
remains. It closes the classical-fallback path entirely.
This is hybrid key establishment, not post-quantum identity
authentication. Long-term device identities and transcript signatures remain
Ed25519 — a future quantum computer that breaks Ed25519 would still be able to
forge a handshake signature even with hybridPq: "required" set. Hybrid PQ
protects the confidentiality of the derived session keys against a
harvest-now-decrypt-later adversary; it does not change the trust model
described in Threat model.