Skip to main content
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.