The on-the-wire format of one encrypted message, in three layers:
plaintext snapped to one of nine fixed sizes with crypto-random fill,
ChaCha20-Poly1305 with the entire Q-Ratchet header bound
as associated data, and an outer Frame Header v2 envelope that stamps
the sender's key-bundle generation so peer re-registration is detected
before decryption fails — not after.
Three layers, one message
PLAINTEXT
→ pad to next bucket (256 … 65536 B, random fill)
→ Q-Ratchet AEAD (header bound as AAD)
→ Frame Header v2 outer envelope
WIRE
Every per-message key is fresh from the Q-Ratchet chain, so a (key, nonce) pair is never reused; the 12-byte nonce strategy at the call site cannot affect collision risk.
The AAD contract
The full serialized ratchet header rides as
ChaCha20-Poly1305 associated data and is reconstructed
deterministically by the receiver from the parsed header — no extra wire
bytes. The covered set: version, flags, message number, previous chain
length, epoch, the sender's X25519 public key, the ML-KEM ciphertext and
new encapsulation key when present, the sync tag, and the QRNG
commitment. A single-bit flip anywhere in that set fails Poly1305
authentication.
Application-layer message ID and sender fields are deliberately outside the AAD: the server stamps them after ciphertext arrives, and binding them would mean trusting the server. Sender identity binds instead through the session itself, the header's ratchet key, and the KTS-logged identity key that authenticated the bootstrap — a ciphertext that decrypts under Profile A's session was encrypted by Profile A, full stop.
Frame Header v2
FH-V2 = magic(3) || sender_generation(8 BE) || bundle_fp(4)
|| extra_hdr_len(2 BE) || extra_hdr || inner_payload
The generation field is allocated from a monotonic backend sequence that advances on every fresh key-bundle upload. A receiver holding a cached bundle older than the generation stamped on an incoming frame knows to refresh before attempting decryption — removing the stale-bundle class of decrypt-failure loops. Four receiver states cover the space: match (normal decrypt), advance (invalidate and refetch), idempotent advance (record, keep session), and regression (short-circuit as a rotated-peer signal).
The 4-byte bundle fingerprint cross-references the KTS-pinned key for the sender; a mismatch escalates to a visible key-change warning. The outer envelope is routing metadata and is deliberately not AEAD-bound — the inner, authenticated ratchet header remains the authoritative source for all ratchet state.
The padding ladder
| Property | Value |
|---|---|
| Buckets | 256 · 512 · 1024 · 2048 · 4096 · 8192 · 16384 · 32768 · 65536 B |
| Snap rule | smallest bucket that fits header + plaintext; over 64 KiB rounds to the next 64 KiB multiple |
| Fill | crypto-random bytes — zero-fill would mark the boundary; random fill is indistinguishable from ciphertext |
| Header | 2 B magic + 4 B big-endian original length |
A passive observer sees nine possible sizes instead of a byte-exact length oracle. Unpadding is backward-compatible through the magic check during the migration window, after which the fallback is removed.
The full wire shape
WIRE = 0x51 0x45 0x05 // FH-V2 wrapping Q-Ratchet
|| sender_generation(8 BE)
|| bundle_fp(4)
|| extra_hdr_len(2 BE) || extra_hdr
|| 0x51 0x45 0x03 // Q-Ratchet inner
|| header_len(2 BE) || header
|| nonce(12) || ciphertext+tag(16)
Typical overhead beyond plaintext and padding: a 46-byte header on
ordinary messages, ~3,198 bytes on the one-in-a-hundred PQ-ratchet
message — amortized ≈105 bytes per message including nonce and tag.
Established sessions on current production clients show the
0x51 0x45 0x05 magic; pre-key envelopes appear only on first
contact.
What padding does not defend against
The ladder hides byte-exact length. It hides nothing else, and the residue is enumerated here rather than left for a reader to discover.
- Per-message timing. Two same-bucket messages sent at different times still leak timing; constant-rate traffic padding exists for transports that warrant it.
- Longitudinal correlation. Burst patterns survive bucketing against a determined long-term observer; source/destination unlinkability is the QLEAP layer's job, not the ladder's.
References
-
QERYX Protocol Specification §7 —
docs/SPEC/05-aead-and-aad.md; implementationcrypto-core/src/symmetric.rsand the client crypto service. - RFC 8439 — ChaCha20 and Poly1305 for IETF Protocols.
- Q-Ratchet: Four Parallel Ratchets — the header this format authenticates.
- Identity and Key Transparency — the pinning that backs the bundle fingerprint check.