Signal Double Ratchet
X3DH, symmetric chain, DH step, forward secrecy.
Signal Double Ratchet
Signal's design is end-to-end encryption that stays secure even if part of your key state later leaks. The protocol does it by layering two independent "ratchet" mechanisms: one advances every message, the other advances every round-trip. Together they give both forward secrecy (old messages stay safe if today's key leaks) and post-compromise security (after a leak, future messages return to safe).
Analogy
Picture two neighbours who share a safe-deposit box with a disposable combination lock. After every message they drop in, they set a new combination and burn the old one on the way out — so a thief who snatches today's combination can't retroactively open yesterday's deposits, because the combinations that opened those locks no longer exist anywhere on Earth. And every time the neighbours meet in person to sip coffee, they also swap to an entirely new brand of lock chosen on the spot. If a burglar somehow copies today's combination, the moment the neighbours next meet and switch locks, the burglar is locked out again. The two ratchets give two overlapping protections — one forward, one reset — and together they keep the box safe through almost any single breach.
Step 1 — X3DH bootstrap
Before the first message there is no shared state. Signal solves chicken-and-egg with X3DH: an asynchronous key agreement where Alice can start a conversation with Bob while Bob is offline.
Bob publishes a bundle to the server in advance:
- Identity key (IK) — long-term, never changes.
- Signed prekey (SPK) — rotated monthly, signed by IK.
- One-time prekeys (OPK) — batch of ~100, consumed one per new session.
Alice downloads a bundle, generates an ephemeral key (EK), and computes four DHs:
DH1 = DH(IK_A, SPK_B) // proves Alice has her long-term key
DH2 = DH(EK_A, IK_B) // proves Bob has his long-term key
DH3 = DH(EK_A, SPK_B) // freshness
DH4 = DH(EK_A, OPK_B) // one-time uniqueness
SK = HKDF(DH1 || DH2 || DH3 || DH4)
Bob gets Alice's first message (which contains EK_A and identifies which OPK was used), does the same maths, and gets the same SK. That's the shared secret that seeds the double ratchet.
Step 2 — the symmetric chain
From SK each party derives a root key and two chain keys — one for sending, one for receiving. Every time Alice sends a message:
messageKey = KDF(sendChainKey, "message")
sendChainKey = KDF(sendChainKey, "chain")
The message key encrypts the one message and is then discarded. The chain key is replaced by a one-way function of itself. You cannot go backwards: given today's chain key, yesterday's is gone. That's forward secrecy. Even if an attacker compromises the current chain key, every past message stays secret because the keys that encrypted them no longer exist.
Step 3 — the DH ratchet
The symmetric chain is one-way but doesn't recover from compromise. If someone steals your current chain key they can decrypt every future message. Signal patches this with a second ratchet running in parallel.
Every time the conversation turns around — Alice sends, then Bob replies — Bob generates a fresh ephemeral DH key and includes its public half in the reply header. Alice combines her ephemeral key with Bob's new one, derives a fresh root key, and from that fresh chain keys for sending and receiving. Old chain keys are discarded.
rootKey, chainKey = KDF(rootKey, DH(myEphemeral, theirEphemeral))
This is the "DH step". Every DH step injects new randomness into the key tree, making the old state cryptographically unreachable. That's break-in recovery: even if the attacker had your keys a minute ago, they lose access after the next turn.
Out-of-order messages
Real messaging networks deliver packets out of order. Signal handles this by allowing messages to reference which chain position they came from (header: previous chain length + index within current chain). The receiver can look back, derive the needed message keys on-demand, and skip ahead through the chain without having to replay earlier messages.
A single message can drop and the conversation still decrypts. Drop too many — thousands, not dozens — and the key-ratchet lookahead budget is exceeded and the session must reset.
What this buys in practice
Signal (the app), WhatsApp, Meta Messenger, Google Messages over RCS, and Matrix's Olm/Megolm all use the double-ratchet pattern. The shared security story is:
- Server breach — the server sees opaque ciphertext; it cannot read content, past or future.
- Device seizure — messages already in the chat log are decrypted on-device; they can be read. Messages you haven't received yet are safe as soon as the next DH step happens (a minute at most).
- Key leak — the attacker reads messages until the next turn, then loses access.
- Long-term identity key leak — an attacker can MITM future new sessions (hence the safety-number / QR-code identity verification ritual) but not existing ones.
Where this falls down
- Metadata. Who talked to whom, and when, is not protected by the ratchet. Signal adds sealed-sender and private-contact-discovery to reduce this, but it's a hard problem.
- Multi-device. The textbook ratchet is 1-to-1. Real deployments do multi-device sync with secondary device provisioning and group-message derivation (Sender Keys). This is why WhatsApp Web doesn't read your old messages when you pair it — they're on your phone, not the server.
- Groups. A group chat is not N pairwise ratchets (that would be O(n²) messages per broadcast). Signal uses Sender Keys: each sender has their own hash-ratcheted key, distributed to group members pairwise. It's fast but gives weaker forward secrecy than a pairwise chat.
Don't roll your own
Implementing this correctly means handling skipped-message key caches, key zeroization, header encryption, and a dozen timing subtleties. libsignal-client is the audited reference. MLS (RFC 9420) is the IETF's attempt at a standardised group-scale alternative — use MLS if you're building new.
What the playground tests
You advance the ratchet a few steps and look at the resulting state: how many messages went each direction, how many DH rotations have fired, what the current root key fingerprint is. The deep lesson is that forward secrecy and break-in recovery both fall out of one-way functions plus fresh DH — no magic, just a careful arrangement of KDFs.