TL;DR — Signal’s Double Ratchet Algorithm combines a DH ratchet and a symmetric-key ratchet to ensure that every message uses a unique encryption key. Even if a long-term key is compromised, past messages remain unreadable (forward secrecy), and session keys self-heal after a single compromise event (break-in recovery).
The Problem: Why Forward Secrecy Matters
Most encryption systems rely on a static key pair. Alice and Bob generate a long-term identity key, exchange public components, and derive a shared secret. This model works—until the key is compromised. If an adversary obtains Alice’s private key at any point in time, every message she has ever sent or received becomes decryptable.
In a messaging system like Signal, this is catastrophic. A seized device, a memory dump, or a forensic image should not expose an entire conversation history. The requirement is forward secrecy: the compromise of long-term keys at time t must not compromise messages sent before t.
Even stronger is break-in recovery: after a key compromise at time t, the system must automatically generate fresh keys such that messages sent after t remain protected, without any user intervention or out-of-band action.
The Double Ratchet Algorithm, introduced by Trevor Perrin and Moxie Marlinspike in the Signal Protocol (originally the Axolotl Protocol), was designed to solve exactly these two properties. It does so through a mechanism that is deceptively simple in concept but intricate in implementation.
Core Concepts: Ratchets and Ephemeral Keys
A ratchet is a one-way state machine. It advances in one direction, consuming state to produce new state, and the previous state is irrecoverably destroyed. There are two fundamental types at play:
The DH Ratchet (Asymmetric Ratchet)
The DH ratchet uses elliptic curve Diffie-Hellman key exchanges to evolve the root key each time a new ephemeral key pair is introduced. Every time Alice sends a message, she generates a fresh ephemeral key pair and includes her new public key in the message header. Bob, upon receiving it, performs a DH computation between his current ratchet key and Alice’s new ephemeral public key, producing a new shared secret that feeds into the root key.
This creates a chain of DH outputs where each link depends on a fresh ephemeral contribution. The key insight is that the chain only moves forward: you cannot compute a previous DH output without the corresponding private ephemeral key, which has been discarded.
The Symmetric-Key Ratchet (KDF Ratchet)
The symmetric ratchet uses a Key Derivation Function (KDF) chain. Starting from a root key, each message is encrypted with a key derived by applying the KDF to the current chain key, and the chain key is then updated. The old chain key is deleted immediately after use.
import hashlib
import hmac
def kdf_chain(key: bytes, label: bytes) -> bytes:
"""
A simple KDF chain modeled after HKDF-style construction.
In production, Signal uses a custom construction based on HMAC-SHA256.
"""
return hmac.new(key, label, hashlib.sha256).digest()
class SymmetricRatchet:
def __init__(self, root_key: bytes):
self.chain_key = root_key
def next_key(self) -> bytes:
"""Derive the next message key and advance the chain."""
message_key = kdf_chain(self.chain_key, b"message_key")
self.chain_key = kdf_chain(self.chain_key, b"chain_key")
return message_key
Each call to next_key produces a unique message key and advances the chain. The critical property: given message_key_n, an adversary cannot compute message_key_{n+1} without knowing the current chain_key, and given chain_key_n, they cannot reverse to recover chain_key_{n-1}.
Architecture of the Double Ratchet
The “double” in Double Ratchet refers to the two ratchets operating simultaneously and orthogonally:
The DH ratchet provides asynchronous key evolution. It introduces fresh entropy whenever a party sends a message with a new ephemeral public key. This means that even if the symmetric chain is exhausted or compromised, new DH exchanges inject fresh randomness.
The symmetric ratchet provides per-message key derivation. Every single message gets its own encryption key, and keys are consumed one-way.
Together, they achieve two complementary guarantees:
- Forward secrecy through the symmetric ratchet: once a message key is used and the chain key is advanced, the previous state is gone. An adversary who captures the current state cannot decrypt past messages.
- Break-in recovery through the DH ratchet: if an adversary compromises the current root key and chain keys, the next DH exchange injects new entropy that they do not possess, immediately restoring secrecy for all subsequent messages.
Here is the high-level state diagram:
Root Key ──► [DH Ratchet] ──► New Root Key ──► [Symmetric Ratchet] ──► Message Key
▲ │
│ ▼
New Ephemeral Encrypted Message
Key Pair Generated Sent / Received
The Key Derivation Flow
When Alice initiates a session and sends her first message, the key derivation proceeds as follows:
- Alice and Bob perform a Triple Diffie-Hellman (3DH) handshake, producing an initial shared secret.
- This shared secret is processed through a Key Derivation Function to produce the initial root key and chain keys.
- For each subsequent message, the sender:
- Derives a message key from the current symmetric chain.
- If a new ephemeral key pair is being sent, performs a DH computation and updates the root key.
- Encrypts the plaintext with the message key using AES-256 in CBC mode (or a stream cipher).
class DoubleRatchetState:
def __init__(self, root_key: bytes, sending_chain: bytes, receiving_chain: bytes):
self.root_key = root_key
self.sending_chain = SymmetricRatchet(sending_chain)
self.receiving_chain = SymmetricRatchet(receiving_chain)
self.dh_sending_private = None
self.dh_sending_public = None
self.dh_receiving_public = None
def encrypt(self, plaintext: bytes, new_ephemeral_pub: bytes = None) -> tuple:
"""Encrypt a message using the double ratchet."""
# Derive message key from the sending symmetric chain
message_key = self.sending_chain.next_key()
# If a new DH ratchet step is triggered, update root key
if new_ephemeral_pub is not None:
shared_secret = self._dh_compute(
self.dh_sending_private, new_ephemeral_pub
)
self.root_key = kdf_chain(self.root_key, shared_secret + b"dh_ratchet")
self.dh_receiving_public = new_ephemeral_pub
# Encrypt with the message key (AES-256-CBC in production)
ciphertext = self._aes_encrypt(message_key, plaintext)
return (ciphertext, message_key)
def decrypt(self, ciphertext: bytes, sender_ephemeral_pub: bytes) -> bytes:
"""Decrypt a message, performing DH ratchet step if needed."""
# If we received a new ephemeral key, perform DH ratchet step
if sender_ephemeral_pub != self.dh_receiving_public:
shared_secret = self._dh_compute(
self.dh_receiving_private, sender_ephemeral_pub
)
self.root_key = kdf_chain(self.root_key, shared_secret + b"dh_ratchet")
self.dh_receiving_public = sender_ephemeral_pub
# Re-initialize the receiving symmetric chain
self.receiving_chain = SymmetricRatchet(self.root_key)
message_key = self.receiving_chain.next_key()
return self._aes_decrypt(message_key, ciphertext)
Note: This is a simplified pedagogical implementation. The actual Signal Protocol uses a more elaborate construction involving HKDF with specific info labels, Curve25519 for DH operations, and AES-256-CBC with HMAC-SHA256 for message authentication. The production code lives in libsignal.
Patterns in Production: How Signal Implements This
The X3DH Handshake
Before the double ratchet begins, Signal uses the Extended Triple Diffie-Hellman (X3DH) protocol to establish an initial shared secret. X3DH combines up to four DH computations between the initiator and the responder, incorporating:
- The initiator’s ephemeral key
- The initiator’s signed pre-key
- The responder’s identity key
- The responder’s signed pre-key
This allows the initiator to establish a shared secret even if the responder is offline, as long as the responder has previously uploaded pre-keys to the server. The initial shared secret is then processed through a KDF to produce the root key and the initial chains for the double ratchet.
Sender Chains and Receiver Chains
In the double ratchet, each party maintains a sending chain (used to encrypt outgoing messages) and a receiving chain (used to decrypt incoming messages). When Alice sends a message to Bob, she uses her sending chain. When Bob receives it, he uses his receiving chain.
Crucially, when Bob replies, he sends a message that triggers a new DH ratchet step—he includes a new ephemeral public key. This causes Alice to create a new receiving chain and Bob to create a new sending chain. The chains never overlap between parties, which simplifies the state management and ensures that each party independently ratchets forward.
The “Skip” Problem
One of the trickiest aspects of the double ratchet in production is handling skipped messages. If Bob misses a message from Alice, he hasn’t advanced his receiving chain to the correct position. When the next message arrives, he must “catch up” by deriving all the intermediate chain keys until he reaches the correct one.
In practice, this is handled by caching a window of recent chain keys or by using a technique called symmetric-key ratchet with a hash-tree approach (similar to the TreeKEM construction used in MLS). Signal’s actual implementation uses a pragmatic approach: it caches a small number of recent message keys and relies on the sender to retransmit or on the protocol’s reliability layer to handle message loss.
Post-Compromise Security in Practice
The break-in recovery property is what makes the Double Ratchet truly remarkable from an operational standpoint. Consider this scenario:
- An adversary compromises Alice’s device at time t and extracts all current cryptographic state.
- Alice sends a message at time t+1 using the compromised state. The adversary can decrypt this message.
- At time t+2, Alice receives a message from Bob that includes a new ephemeral public key. The DH ratchet step generates a new root key that the adversary does not know.
- From t+2 onward, all messages are protected by keys the adversary cannot derive.
This means the “window of exposure” after a compromise is bounded by exactly one message in each direction—assuming the adversary does not compromise the device again before the next DH ratchet step occurs.
Key Takeaways
- Forward secrecy is not optional in modern messaging. The Double Ratchet makes it practical by ensuring every message uses a unique, ephemeral key that is discarded after use.
- The two ratchets serve complementary purposes. The DH ratchet injects fresh entropy asynchronously; the symmetric ratchet provides fine-grained per-message key derivation. Neither alone achieves both forward secrecy and break-in recovery.
- Break-in recovery is automatic and requires no user action. After a single compromise event, the next DH exchange self-heals the session. This is a property that most other protocols (including TLS with static keys) lack.
- The skip problem is the hardest engineering challenge. Implementing a production-grade double ratchet requires careful handling of missed messages, message reordering, and state synchronization across devices.
- The Signal Protocol builds on well-studied primitives. Curve25519, AES-256-CBC, and HMAC-SHA256 are the building blocks; the innovation is in the composition of these primitives into a coherent ratchet mechanism.
- The protocol is not without limitations. It requires asynchronous pre-key management, does not provide post-quantum security, and the skip problem creates memory overhead in high-latency environments.
Further Reading
- Signal Protocol Specification — Double Ratchet — The official specification from the Signal Foundation, covering the complete algorithm with precise state transitions and KDF definitions.
- X3DH Key Agreement Protocol — The extended Triple Diffie-Hellman handshake that establishes the initial shared secret before the double ratchet begins.
- The Signal Protocol: A Formal Security Analysis — A formal cryptographic proof of the protocol’s security properties by Cohn-Gordon, Cremers, and Garratt.
- libsignal — Signal’s Open-Source Implementation — The production-grade C library that powers Signal, Signal Desktop, and numerous third-party messaging applications.
- MLS: The Messaging Layer Security Standard — The IETF standard that extends the ratchet concept to group messaging, addressing many of the limitations the Double Ratchet faces in multi-party scenarios.