The Q-Ratchet is the per-message key ratchet that runs after the
handshake bootstraps a session. It is a four-chain design — a
classical X25519 ratchet, an ML-KEM-1024
epoch ratchet, a symmetric HKDF-SHA-512 chain, and an
entropy re-injection lane — composed so that compromise of any one
chain leaves the others standing. Its security in revision 1 rests on
the symmetric chain and the post-quantum establishment root; the
remaining chains activate in protocol revision 2. Source:
crypto-core/src/quadruple_ratchet.rs.
The four ratchets
Classical double-ratchet designs advance two chains: a Diffie–Hellman ratchet and a symmetric chain. Both are classical — a cryptographically-relevant quantum computer that breaks the discrete-log assumption reads all past and future output. The Q-Ratchet keeps both, then adds two more.
Execution status (v1.0): the symmetric chain and its sync guard execute on every message in revision 1; the DH, KEM-epoch and entropy chains are carried in the ratchet state and activate in protocol revision 2. The tables below describe the full design.
| Chain | Primitive | Cadence | What it buys |
|---|---|---|---|
| 1 — Classical DH | X25519 |
every message (revision 2) | per-message forward secrecy and classical PCS |
| 2 — Post-quantum KEM | ML-KEM-1024 |
every 100 messages (revision 2) | post-quantum FS + PCS at epoch granularity |
| 3 — Symmetric chain | HKDF-SHA-512 |
every message | one-way key evolution between DH steps |
| 4 — Entropy re-injection | OS CSPRNG + conditioned pool | Design cadence: every 5 messages (text), every 250 packets (voice). In revision 1 the text chain advances deterministically from the session secret with no external entropy; the voice cadence is declared and not executed. Both activate with peer-agreed entropy re-injection in revision 2. | disconnects post-compromise chains from pre-compromise state |
Bootstrap from the handshake
The ratchet roots are bound to the exact handshake transcript that produced them. Any tweak to either side's key bundle or handshake nonce yields different roots.
classical_root = HKDF-SHA-512(ikm = ss, salt = none, info = "QERYX-v1-" ‖ "CLASSICAL-RATCHET" ‖ "ROOT-KEY")
pq_root = HKDF-SHA-512(ikm = ss, salt = none, info = "QERYX-v1-" ‖ "PQ-RATCHET" ‖ "ROOT-KEY")
combined_chain = HKDF-SHA-512(ikm = classical_root ‖ pq_root, salt = none,
info = "QERYX-v1-" ‖ "COMBINED-CHAIN" ‖ "CHAIN-KEY")
Source: crypto-core/src/quadruple_ratchet.rs. This is the
transcript-free rendering the path in code runs; the transcript is bound
one layer up, under QERYX-QRATCHET-BOOT-v3. The label and the
context are concatenated without a delimiter — a boundary ambiguity we
published as F-005 and closed for new derivation slots with
labeled_hkdf_strict, which injects a reserved 0xFF
byte. Existing slots stay byte-locked.
Each chain, precisely
Classical X25519
Every message header carries the sender's current X25519 public key. When
a receiver sees a new one — compared in constant time — a DH ratchet step
fires and re-derives the classical root and chain
(quadruple_ratchet.rs:2173-2191).
Post-quantum ML-KEM-1024
In revision 2, every 100 messages the sender generates a fresh ML-KEM-1024 keypair, encapsulates to the peer's current PQ key, and folds the shared secret into a new PQ root. An 8-byte sync tag derived from the PQ root rides the header, so a diverged PQ chain is detected at header-parse time — not N messages later as an opaque authentication failure. Decapsulation always runs the full implicit-rejection path, in constant time.
Symmetric chain
ChainKey.advance():
new_ck = HKDF(ck, "CHAIN-ADVANCE", info = "CHAIN-KEY", 32)
msg_key = HKDF(ck, "MESSAGE-DERIVE", info = "MESSAGE-KEY", 32)
Chain advance and message-key derivation use distinct labels: a leaked
message key does not reveal the next chain key, and a leaked chain key
does not reveal prior message keys. Intermediate heap copies are
explicitly zeroized before drop — Rust's Drop alone does not
zero allocator memory.
Entropy re-injection
In revision 2 the fourth chain absorbs a fresh 32-byte contribution into the symmetric chain every 5 messages (text) or 250 packets (voice), sourced from the OS CSPRNG and the conditioned server-relayed pool — defense in depth, never relied on alone. An 8-byte commitment on the header lets the receiver detect divergence without sending the entropy itself. Injection disconnects post-compromise chains from pre-compromise state: an attacker who later steals the post-injection state cannot re-derive what came before it.
This is not QKD. Not quantum networking. Not quantum teleportation. We do not violate the no-communication theorem. Keys come from the operating system's CSPRNG; server-relayed entropy is conditioned in as defense in depth and never relied on alone.
Header binding
The entire serialized ratchet header — version, flags, message number,
previous chain length, epoch, X25519 public key, ML-KEM ciphertext and
new encapsulation key, sync tag, QRNG commitment — is bound as
ChaCha20-Poly1305 AAD
(quadruple_ratchet.rs:2302-2342). A single-bit flip anywhere
in that set fails authentication at the receiver, even when the ratchet
would still derive a syntactically valid key for the claimed position.
Out-of-order delivery
- Per-session skip cap of 2,000 keys (raised to 5,000 for the messenger) — sized for mesh and jammed transports that drop 30%+ of packets in bursts.
- A process-wide cap of ~52,428 cached entries (2 MiB) bounds memory against an adversary spinning up thousands of sessions with forged high counters.
- Skipped message keys are retained for twelve post-quantum epochs behind the highest epoch the receiver has applied, then dropped; a frame older than that is refused as a regression rather than served from a cache. Eviction zeroizes the key bytes and decrements the global counter atomically.
Forward secrecy and post-compromise security
Forward secrecy. Compromise of any single chain at time t does not reveal messages before t: every derivation flows forward only, under labels that separate chain keys from message keys, and per-message keys are zeroized immediately after use.
Post-compromise security. After a complete state compromise, the next PQ epoch — revision 2 — generates a fresh ML-KEM-1024 keypair that is independent of everything the attacker holds. Recovering confidentiality then requires breaking ML-KEM-1024 itself — a property that holds against a quantum adversary, where a classical-only DH ratchet recovers nothing.
| Compromise | Recovery mechanism | Bound |
|---|---|---|
| Classical secret only | PQ ratchet re-keys independently (revision 2) | ≤ 100 messages (revision 2) |
| PQ secret only | classical DH keeps advancing per message | next message |
| Full ratchet state | fresh ML-KEM epoch + entropy re-injection (revision 2) | revision 2: ≤ 100 messages; v1: re-establish the session |
What the Q-Ratchet does not defend against
Four ratchets bound the window a compromise stays open. They do not close a compromise that never ends, and these limits say exactly where that boundary falls.
- Continuous endpoint compromise. An attacker with persistent read/write on an unlocked device sees plaintext as it is typed. No ratchet defends against this; the threat model names it out of scope.
- A pre-handshake adversary. An attacker holding the full session root before the first message reads message 1; PCS begins at the first PQ epoch.
- Double-initiator desync. Two peers initiating simultaneously can desync chains; the mitigation is the generation-monotonic Frame Header v2 counter plus eager re-handshake, documented in the AEAD paper.
References
-
QERYX Protocol Specification §6 —
docs/SPEC/04-q-ratchet.md, source snapshot 2026-05-01; implementationcrypto-core/src/quadruple_ratchet.rs. - NIST FIPS 203, Module-Lattice-Based Key-Encapsulation Mechanism Standard (ML-KEM), 2024.
- RFC 5869 — HMAC-based Extract-and-Expand Key Derivation Function (HKDF), 2010.
- RFC 8439 — ChaCha20 and Poly1305 for IETF Protocols, 2018.
- The Hybrid Handshake — the ESv1 bootstrap this ratchet consumes.
- Message AEAD, Frame Header v2, and the Padding Ladder — the wire format that binds this header.