DEV Community

Daniel Deng
Daniel Deng

Posted on Originally published at marble.md

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 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:

  1. Publish prekeys so that others can start a chat.
  2. Start a session with someone who is offline.
  3. Change keys with every message, and keep doing so in a quantum-safe way.
  4. 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 IKBIK_B 1 Long term — Identifies Bob's device and signs the other keys
Signed prekey SPKBSPK_B 1 Replaced regularly, for example weekly Yes The main key for new sessions
One-time prekeys OPKBOPK_B 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 IKBIK_B , 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)DH(X, Y) is the shared secret of key pairs XX and YY . Each side computes it from its own private key and the other side's public key.

DH1=DH(IKA, SPKB)DH2=DH(EKA, IKB)DH3=DH(EKA, SPKB)DH4=DH(EKA, OPKB)only if the bundle had a one-time prekey(CT, SS)=Encapsulate⁡(PQPKB)using Bob’s quantum-safe prekeySK=KDF⁡(DH1∥DH2∥DH3∥DH4∥SS)\begin{aligned} DH_1 &= DH(IK_A,\ SPK_B) \cr DH_2 &= DH(EK_A,\ IK_B) \cr DH_3 &= DH(EK_A,\ SPK_B) \cr DH_4 &= DH(EK_A,\ OPK_B) && \text{only if the bundle had a one-time prekey} \cr (CT,\ SS) &= \operatorname{Encapsulate}(PQPK_B) && \text{using Bob's quantum-safe prekey} \cr SK &= \operatorname{KDF}(DH_1 \Vert DH_2 \Vert DH_3 \Vert DH_4 \Vert SS) \end{aligned}

∥\Vert means "joined end to end". CTCT is the sealed copy of SSSS that Alice sends to Bob. PQPKBPQPK_B is whichever quantum-safe prekey the bundle held. Each part protects against a different attack:

Part What it adds
DH1DH_1 Needs Alice's identity private key, so Bob knows the message came from Alice
DH2DH_2 Needs Bob's identity private key, so only Bob can read the message
DH3DH_3 Uses Alice's temporary key, so old sessions stay safe once Bob replaces SPKBSPK_B and deletes its private key
DH4DH_4 Uses a key that works once, so this session stays safe as soon as Bob deletes OPKBOPK_B
SSSS Quantum-safe, so an attacker who records traffic now and gets a quantum computer later still cannot find SKSK

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.

Why does the first message list prekey IDs?
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 SKSK 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:

(CKn+1, MKn)=KDF⁡(CKn)(CK_{n+1},\ MK_n) = \operatorname{KDF}(CK_n)

MKnMK_n is the message key. It encrypts exactly one message and is then deleted. The KDF is one-way, so a stolen CKn+1CK_{n+1} does not reveal MKnMK_n 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
Enter fullscreen mode Exit fullscreen mode

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:

(RK′, CK)=KDF⁡(RK, DH(my ratchet key, their ratchet key))(RK',\ CK) = \operatorname{KDF}\big(RK,\ DH(\text{my ratchet key},\ \text{their ratchet key})\big)

The first root key comes from SKSK . Bob's first ratchet key is the signed prekey SPKBSPK_B .

%%{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.

  1. The PQXDH key agreement protocol. It replaced the older X3DH, which has no quantum-safe part. ↩

  2. The Double Ratchet algorithm. ↩

  3. 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. ↩

  4. The Sesame algorithm. ↩

  5. 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. ↩

Top comments (0)