Signal protocol — How Signal and WhatsApp keep secrets from their own servers
The Signal Protocol keeps chat messages private for billions of people. Signal, WhatsApp and Facebook Messenger use it to encrypt messages end to end. Google Messages uses it too, for chats sent over RCS, the newer repla
The Signal Protocol keeps chat messages private for billions of people. Signal, WhatsApp and Facebook Messenger use it to encrypt messages end to end. Google Messages uses it too, for chats sent over RCS, the newer replacement for SMS. End to end means that only the people in a chat can read it, not the company that runs the app.
This is a 15-minute explainer on how this protocol works, based on the specifications at signal.org/docs. We describe the protocol as the Signal app runs it. Other apps share the same core design, but have different implementation details.
The summary
- The server cannot read messages. It stores public keys and passes encrypted messages along. Only the sending and receiving devices hold the keys that decrypt them.
- The receiver can be offline. Bob uploads public "prekeys" in advance. Alice uses them to set up a shared secret without waiting for Bob to reply.
- Every message has its own key. A stolen key does not unlock earlier messages, and the conversation becomes secure again soon after a theft.
- It plans for quantum computers. Since 2023, setting up a chat resists future quantum computers. In 2025, Signal began adding the same protection to the keys for ongoing messages.
Terms used here
- Key pair: A private key that never leaves the device, and a public key that anyone may see.
- Diffie-Hellman exchange: Each side combines its own private key with the other side's public key. Both get the same secret. Someone who sees only the public keys cannot work it out. Signal uses the X25519 curve, so each public key is 32 bytes.
- Key encapsulation: The sender uses the receiver's public key to make a random secret and a sealed copy of it. Only the receiver's private key can open the copy. Signal uses ML-KEM, a standard based on Kyber, which is designed to resist quantum computers.
- Key derivation function (KDF): A one-way function that turns secrets into new keys. You cannot work back from a new key to the secrets that made it.
- Forward secrecy: Stealing a device's keys today does not reveal messages sent before the theft.
- Post-compromise security: After a theft, the conversation becomes secure again once the two sides exchange fresh keys. This is also called "self-healing".
Who holds what
flowchart LR
subgraph AL["Alice's phone"]
AK["Private keys and<br/>session state"]
end
subgraph SV["Signal server"]
KD["Key directory<br/>(public keys only)"]
MB["Mailboxes<br/>(one per device)"]
end
subgraph BO["Bob's devices"]
BP[Bob's phone]
BL[Bob's laptop]
end
AK -- "2 Fetch prekeys" --> KD
AK -- "3 Encrypted copies" --> MB
MB -- "4 Deliver" --> BP
MB -- "4 Deliver" --> BL
KD ~~~ BP
KD ~~~ BL
BP -- "1 Upload prekeys" --> KD
BL -- "1 Upload prekeys" --> KD
The protocol has four jobs. The rest of this note follows them in order:
- Publish prekeys so that others can start a chat.
- Start a session with someone who is offline.
- Change keys with every message, and keep doing so in a quantum-safe way.
- Reach every device a person owns.
Step 1: Bob publishes keys in advance
Each device makes several key pairs. It keeps the private halves and uploads the public halves to the server.
| Key | How many | How long it lasts | Signed by the identity key | What it is for |
|---|---|---|---|---|
| Identity key IKB | 1 | Long term | — | Identifies Bob's device and signs the other keys |
| Signed prekey SPKB | 1 | Replaced regularly, for example weekly | Yes | The main key for new sessions |
| One-time prekeys OPKB | Many | Used once, then deleted | No | Extra forward secrecy |
| One-time quantum-safe prekeys | Many | Used once, then deleted | Yes | A secret that resists quantum computers |
| Last-resort quantum-safe prekey | 1 | Replaced regularly | Yes | Used only when the one-time quantum-safe prekeys run out |
The signatures let Alice check that the prekeys really come from the holder of
IKB
, and not from the server.
%%{init: {"sequence": {"width": 130, "actorMargin": 40, "diagramMarginX": 20}}}%%
sequenceDiagram
participant B as Bob's phone
participant S as Signal server
B->>B: Make key pairs,<br/>keep the private halves
B->>S: Identity key, signed prekey,<br/>last-resort prekey, signatures
B->>S: A batch of one-time prekeys
Note over S: Stores public keys only
B->>S: Later: how many one-time<br/>prekeys are left?
S-->>B: A count
B->>S: Upload more when the count is low
Step 2: Alice starts a session while Bob is offline
This step is called PQXDH: post-quantum extended Diffie-Hellman.1 Alice fetches a "prekey bundle" for Bob and works out a shared secret. The first message carries everything Bob needs to work out the same secret.
%%{init: {"sequence": {"width": 130, "actorMargin": 40, "diagramMarginX": 20}}}%%
sequenceDiagram
participant A as Alice's phone
participant S as Signal server
participant B as Bob's phone
A->>S: Ask for Bob's prekey bundle
S-->>A: IK_B, SPK_B,<br/>a quantum-safe prekey,<br/>signatures, and OPK_B<br/>if any are left
Note over S: Deletes the<br/>one-time prekeys<br/>it handed out
Note over A: Checks signatures,<br/>computes SK
A->>S: IK_A, EK_A, CT,<br/>prekey IDs, and the<br/>encrypted message
Note over B: Later, Bob<br/>comes online
B->>S: Fetch new messages
S-->>B: Alice's first message
Note over B: Computes SK<br/>and decrypts
Alice computes four Diffie-Hellman results and one encapsulated secret. DH(X,Y) is the shared secret of key pairs X and Y . Each side computes it from its own private key and the other side's public key.
∥ means "joined end to end". CT is the sealed copy of SS that Alice sends to Bob. PQPKB is whichever quantum-safe prekey the bundle held. Each part protects against a different attack:
| Part | What it adds |
|---|---|
| DH1 | Needs Alice's identity private key, so Bob knows the message came from Alice |
| DH2 | Needs Bob's identity private key, so only Bob can read the message |
| DH3 | Uses Alice's temporary key, so old sessions stay safe once Bob replaces SPKB and deletes its private key |
| DH4 | Uses a key that works once, so this session stays safe as soon as Bob deletes OPKB |
| SS | Quantum-safe, so an attacker who records traffic now and gets a quantum computer later still cannot find SK |
Why does the first message list prekey IDs?Warning: PQXDH keeps the secret safe from future quantum computers, but not the proof of identity. That still depends on Diffie-Hellman. An attacker with a working quantum computer during the exchange could pretend to be Alice or Bob.
Bob has uploaded many one-time prekeys. The IDs tell Bob's phone which private keys to use, so it can compute the same secret and then delete those keys.
What if Bob's one-time prekeys run out?
The bundle then has no one-time Diffie-Hellman prekey, and it carries the last-resort quantum-safe prekey instead. The session still works, but its forward secrecy is weaker until Bob uploads new prekeys.
Step 3: A new key for every message — the Double Ratchet
The secret SK from Step 2 starts the Double Ratchet.2 A ratchet turns only one way: each step makes a new key, and no one can turn it back to find an old key. The protocol uses two kinds of ratchet.
The symmetric ratchet: one step per message
Each side keeps a chain key for sending and another for receiving. Every message moves the chain one step forward:
MKn
is the message key. It encrypts exactly one message and is then deleted. The KDF is one-way, so a stolen
CKn+1
does not reveal
MKn
or any earlier key. In the diagram, each chain key goes through the KDF once to make the next chain key and one message key.
flowchart LR
C0[Chain key 0] --> C1[Chain key 1]
C1 --> C2[Chain key 2]
C2 --> C3["…"]
C0 --> M0[Message key 0]
C1 --> M1[Message key 1]
C2 --> M2[Message key 2]
The specification suggests HMAC with a different constant for each output:
import hashlib
import hmac
def chain_step(chain_key: bytes) -> tuple[bytes, bytes]:
message_key = hmac.new(chain_key, b"\x01", hashlib.sha256).digest()
next_chain_key = hmac.new(chain_key, b"\x02", hashlib.sha256).digest()
return next_chain_key, message_key
The Diffie-Hellman ratchet: new secrets on every reply
The symmetric ratchet protects the past, but not the future. An attacker who steals a chain key can follow it forward. To fix this, each message carries the sender's current ratchet public key. When the conversation changes direction, the receiver combines it with its own ratchet key and mixes the result into a root key. The root key then produces fresh chain keys:
The first root key comes from
SK
. Bob's first ratchet key is the signed prekey
SPKB
.
%%{init: {"sequence": {"width": 130, "actorMargin": 40, "diagramMarginX": 20}}}%%
sequenceDiagram
participant A as Alice
participant S as Server
participant B as Bob
Note over A: Sending chain:<br/>DH(A1, SPK_B)
A->>S: Key A1, message 0
A->>S: Key A1, message 1
S-->>B: Both messages
Note over B: Receiving chain:<br/>DH(SPK_B, A1)<br/>New key pair B1<br/>Sending chain:<br/>DH(B1, A1)
B->>S: Key B1, message 0
S-->>A: Bob's message
Note over A: Receiving chain:<br/>DH(A1, B1)<br/>New key pair A2<br/>Sending chain:<br/>DH(A2, B1)
A->>S: Key A2, message 0
Each reply brings a new Diffie-Hellman secret that an attacker with old keys does not have. So a stolen key stops working after one round trip.
What each message header carries
| Field | Why the receiver needs it |
|---|---|
| Sender's ratchet public key | Shows whether a new Diffie-Hellman step is needed |
| Message number in this chain | Finds the right message key |
| Length of the sender's previous chain | Shows how many messages from that chain are still missing |
Messages can arrive late or out of order. When the receiver sees a gap, it computes the keys for the missing messages and stores them for a while. The number of stored keys has a limit, so a sender cannot make the receiver do unlimited work.
Step 4: Keeping the ratchet quantum-safe
The Diffie-Hellman ratchet is not quantum-safe. The obvious fix is to send a new ML-KEM key with every message. That is expensive: ML-KEM keys and sealed secrets are each over 1,000 bytes, while an X25519 key is 32 bytes.
In 2025 Signal added a third ratchet, the Sparse Post-Quantum Ratchet (SPQR).3 It works like this:
- Keys travel in pieces. Each message carries a small chunk of the next ML-KEM exchange.
- Lost messages do not matter. The chunks use erasure coding: the sender adds extra chunks, so the receiver can rebuild the whole key from any large enough set.
- Each completed exchange starts a new epoch. It produces a fresh quantum-safe secret.
- Both ratchets supply a key. The sender combines the Double Ratchet key and the SPQR key into one message key. An attacker must break both.
flowchart LR
DR[Double Ratchet<br/>message key] --> MIX["KDF: combine"]
PQ[SPQR key for the<br/>current epoch] --> MIX
MIX --> MK[Key that encrypts<br/>the message]
The trade-off: SPQR needs several messages to finish an exchange, so it heals more slowly than the Diffie-Hellman ratchet.
Step 5: Reaching every device
Bob may use a phone and a laptop. The Sesame specification describes how messages reach each one.4
- Each device has its own prekeys and its own sessions.
- To send one message, Alice's phone encrypts a separate copy for each of Bob's devices, and one for each of Alice's other devices.
- The server keeps one mailbox per device. A message waits there until that device fetches it.
If Alice's list of Bob's devices is out of date, the server refuses the message and says which devices changed:
%%{init: {"sequence": {"width": 130, "actorMargin": 40, "diagramMarginX": 20}}}%%
sequenceDiagram
participant A as Alice's phone
participant S as Signal server
A->>S: Copies for Bob's devices 1 and 2,<br/>and for Alice's laptop
S-->>A: Refused: Bob has added device 3
A->>S: Ask for the bundle of Bob's device 3
S-->>A: Bundle for device 3
Note over A: Start a PQXDH session<br/>with device 3
A->>S: Copies for Bob's devices 1, 2 and 3,<br/>and for Alice's laptop
S-->>A: Accepted
Note over S: Puts each copy in<br/>its device's mailbox
The sender limits how often it retries, so a faulty server cannot keep it in a loop.
Checking you are talking to the right person
The server hands out identity keys, so a dishonest server could hand Alice its own key instead of Bob's and read everything in between. Encryption alone cannot stop this.
Signal shows each pair of contacts a safety number made from both identity keys. Alice and Bob can compare it in person, or by scanning a QR code. If the numbers match, no one is in the middle. If a contact's safety number changes, Signal tells the user, because it means a new identity key: usually a new phone, but possibly an attack.
What the server learns
| The server can see | The server cannot see |
|---|---|
| Public keys | Private keys, chain keys or message keys |
| Which device each message is for, and which account sent it5 | The text of any message |
| When each message arrives, and roughly how big it is | Attachments, which are encrypted on the device |
Conclusion
The Signal Protocol lets a server deliver messages that it cannot read. Here is what each part does:
- Prekeys let Alice start a chat while Bob is offline.
- PQXDH gives Alice and Bob a shared secret that the server never sees. A future quantum computer cannot recover it.
- The Double Ratchet makes a new key for every message. A stolen key does not reveal earlier messages, and it stops working after one round trip.
- SPQR protects the ongoing conversation against future quantum computers.
- Sesame describes how each of a person's devices gets its own encrypted copy.
The protocol still has limits:
- The server still sees which device each message is for, when it arrives and roughly how big it is.
- A dishonest server could give Alice its own key instead of Bob's. Alice and Bob can detect this by comparing safety numbers.
- In PQXDH, the proof of identity still relies on Diffie-Hellman, which is not quantum-safe.
- After a key theft, SPQR takes several messages to restore its quantum-safe protection.
If you build an app on the protocol:
- Use libsignal, Signal's open-source library, instead of writing the cryptography yourself.
- Keep every private key on the device. Let the server store public keys only.
- Check prekey signatures before you use a bundle.
- Delete one-time private keys and message keys straight after use.
- Limit how many skipped message keys you store, and for how long.
- Tell users when a contact's identity key changes.
-
The PQXDH key agreement protocol. It replaced the older X3DH, which has no quantum-safe part. ↩
-
ML-KEM Braid specifies the key exchange. Signal's post Signal Protocol and Post-Quantum Ratchets (October 2025) explains how it joins the Double Ratchet. ↩
-
Signal's sealed sender feature hides the sender's account from the server. It is not part of the specifications above; see Signal's 2018 post. ↩
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.