Skip to main content

✓ Verifying Bob's Keys

Preventing Impersonation in X3DH

In 15 minutes: Understand why signatures protect against man-in-the-middle attacks
Prerequisite: Initial Secret Setup


🎯 The Simple Story

Alice downloads Bob's signed pre-key from the server.

Problem: How does Alice know it's really Bob's key, not Eve's?

Solution: Cryptographic signature!

  1. Bob signs his signed pre-key with his identity key
  2. Alice verifies the signature with Bob's identity key
  3. If signature is valid → It's Bob's key
  4. If signature fails → Eve replaced it (reject!)

Without verification, Eve could replace Bob's public keys with her own!


🧠 Mental Model

Hold this picture in your head:

Key Verification Flow:

Bob (offline):
┌────────────────────────────────┐
│ Step 1: Create Keys │
├────────────────────────────────┤
│ IK_B (Identity Key) │
│ SPK_B (Signed Pre-Key) │
│ OPK_B[] (One-Time Keys) │
└────────────────────────────────┘

─────────────────────────────────────────
┌────────────────────────────────┐
│ Step 2: Sign SPK_B with IK_B │
├────────────────────────────────┤
│ SIG_B = Sign(pk(SPK_B), sk(IK_B))│
│ "Bob's SPK, signed by Bob" │
└────────────────────────────────┘

Upload to server

(Eve watches)

Eve tries to replace!

┌────────────────────────────────┐
│ Step 3: Eve creates fake SPK │
├────────────────────────────────┤
│ pk(SPK_EVE) = Eve's SPK │
│ pk(OPK_EVE) = Eve's OPK │
└────────────────────────────────┘

─────────────────────────────────────────
┌────────────────────────────────┐
│ Step 4: Eve tries to sign │
├────────────────────────────────┤
│ SIG_EVE = Sign(pk(SPK_EVE), │
│ sk(IK_EVE)) │
│ "Eve's SPK, signed by Eve" │
└────────────────────────────────┘

─────────────────────────────────────────
┌────────────────────────────────┐
│ Step 5: Alice downloads keys │
├────────────────────────────────┤
│ pk(IK_B), pk(SPK_EVE) ❌ │
│ pk(OPK_EVE) ❌, SIG_EVE ❌ │
└────────────────────────────────┘

─────────────────────────────────────────
┌────────────────────────────────┐
│ Step 6: Alice verifies │
├────────────────────────────────┤
│ Verify(pk(SPK_EVE), SIG_EVE, │
│ pk(IK_B)) │
│ ↓ │
│ ❌ INVALID! │
│ │
│ Alice: "This signed with a │
│ private key I don't │
│ recognize Bob's IK!" │
└────────────────────────────────┘

❌ Reject Bob's keys
❌ Don't do X3DH

📊 See It Happen

Let's watch Eve try to impersonate Bob:


🎭 The Story: The Impersonation Attack

Eve wants to break into Alice and Bob's conversation.

Eve's plan:

  • Replace Bob's public keys with Eve's public keys
  • Alice thinks she's messaging Bob
  • Alice is actually messaging Eve!

Without verification (what Eve wants):

  1. Bob uploads: pk(IK_B), pk(SPK_B), pk(OPK_B)
  2. Eve replaces on server with: pk(IK_EVE), pk(SPK_EVE), pk(OPK_EVE)
  3. Alice downloads what she thinks are Bob's keys
  4. Alice does X3DH with Eve's keys!
  5. Alice is now talking to Eve (impersonating Bob)!
  6. Eve reads Alice's messages, responds as "Bob"

With verification (what actually happens):

  1. Bob uploads: pk(IK_B), pk(SPK_B), pk(OPK_B), SIG_B (SIG_B is signed with sk(IK_B))
  2. Eve tries to replace with: pk(IK_EVE), pk(SPK_EVE), pk(OPK_EVE), SIG_EVE (SIG_EVE is signed with sk(IK_EVE))
  3. Alice downloads: pk(IK_B), pk(SPK_EVE), pk(OPK_EVE), SIG_EVE
  4. Alice verifies: Does SIG_EVE verify with pk(IK_B)?
  5. Alice checks: Is SIG_EVE = Sign(pk(SPK_EVE), sk(IK_EVE))?
  6. Alice sees: ❌ NO! SIG_EVE doesn't match!
  7. Alice: "Eve signed this, not Bob!" or "This signature is from a different key than I have!"
  8. Alice: ❌ Reject these keys, don't do X3DH

Result: Alice won't talk to Eve's fake Bob!


🔐 Why Verification Matters

Man-in-the-Middle (MITM) Attack

Scenario:

Alice → [Network] → Bob (intended)

Eve (intercepts)

Eve pretends to be Bob to Alice
Eve pretends to be Alice to Bob

Without verification:

  1. Alice downloads "Bob's keys" from server
  2. Eve replaced with her own keys
  3. Alice does X3DH to Eve (thinking it's Bob!)
  4. Alice sends sensitive data to Eve (as "Bob")
  5. Eve learns Alice's secrets

With verification:

  1. Alice downloads "Bob's keys"
  2. Alice verifies signature
  3. Alice sees: "This signature isn't from Bob!"
  4. Alice: ❌ Reject won't trust server-provided keys

The verification step:

# Alice checks Bob's keys

pk(IK_B) = Bob's identity key (public)
pk(SPK_B) = Bob's signed pre-key (public)
SIG_B = Signature

# Alice verifies
Verify(pk(SPK_B), SIG_B, pk(IK_B))

# This means:
Check: Is SIG_B = Sign(pk(SPK_B), sk(IK_B))?

# If yes → Bob's keys, proceed
# If no → Eve's keys, reject!

Eve's Problem

Eve can:

  • Replace Bob's public keys with Eve's public keys
  • Sign Eve's signed pre-key with Eve's identity key

Eve can't:

  • Sign with Bob's identity key (Eve doesn't have sk(IK_B))
  • Make Bob's identity key verify Eve's signature

So verification catches Eve's impersonation!


🔢 The Math

Verification Process

# Alice downloads
pk(IK_B), pk(SPKEVE_), pk(OPK_EVE_), SIG_EVE ← Download()

# Alice verifies
Valid = Verify(pk(SPKEVE_), SIG_EVE, pk(IK_B))

if Valid:
# Signature is valid for pk(SPKEVE_) using pk(IK_B)
# This means SIG_EVE = Sign(pk(SPKEVE_), sk(IK_B))
# But Eve only has sk(IK_EVE), not sk(IK_B)
# So Valid = FALSE ❌
else:
# Signature is invalid!
# Eve signed SPK_EVE_ with sk(IK_EVE), not sk(IK_B)
# Alice rejects!

Bob's Signature Creation

# Bob signs his SPK

pk(SPK_B) = Bob's signed pre-key (public)
sk(IK_B) = Bob's identity key (private)

SIG_B = Sign(pk(SPK_B), sk(IK_B))

# Upload
Send(pk(IK_B), pk(SPK_B), SIG_B)

Properties

Binding:

  • SIG_B binds pk(SPK_B) to sk(IK_B)
  • Only SK(IK_B) can create valid SIG_B for pk(SPK_B)

Unforgeability:

  • Eve can't forge SIG_B without sk(IK_B)
  • Even with pk(SPK_B), Eve can't sign it

Verification:

  • Alice has pk(IK_B), can verify SIG_B
  • If SIG_B verifies, Eve hasn't replaced keys
  • If SIG_B fails, keys are fake (Eve's)

🎮 Try It Yourself

Question 1: Eve has pk(SPKEVE_) and SIG_EVE (signed with sk(IK_EVE)). Alice has pk(IK_B). Will Verify(pk(SPKEVE_), SIG_EVE, pk(IK_B)) return valid?

Show Answer

No!

Verify(pk(SPKEVE_), SIG_EVE, pk(IK_B)) checks if SIG_EVE was created using sk(IK_B).

But SIG_EVE = Sign(pk(SPKEVE_), sk(IK_EVE)) (Eve's identity key).

For Verify to return valid:

  • SIG_EVE must equal Sign(SPKEVE_, sk(IK_B))
  • But SIG_EVE = Sign(SPKEVE_, sk(IK_EVE))
  • sk(IVEVE) ≠ sk(IK_B)
  • So SIG_EVE ≠ Sign(SPKEVE_, sk(IK_B))
  • Verify returns ❌ Invalid

Answer: No (SIG_EVE uses sk(IK_EVE), so verification fails)


Question 2: Why can't Eve create fake SIG_B that verifies with pk(IK_B)?

Show Answer

Because Eve doesn't have sk(IK_B) (Bob's identity private key)!

To create valid SIG_B: Eve needs: SIG_B = Sign(pk(SPK_B), sk(IK_B))

Eve has:

  • pk(SPK_B) ✅
  • sk(IK_B) ❌ (Eve doesn't have this!)

Without sk(IK_B), Eve can't fake Bob's signature!

Answer: Eve doesn't have Bob's private key (sk(IK_B))


Question 3: What if Eve signs Alice's message to Bob with her own identity key?

Show Answer

Bob will reject it!

Bob expects Alice's signature to verify with Alice's identity key (pk(IK_A)).

If Eve signs Alice's message with Eve's identity key:

  • SIG_EVE = Sign(message, sk(IK_EVE))
  • Bob verifies with pk(IK_A)
  • Verify(message, SIG_EVE, pk(IK_A))
  • Checks: Is SIG_EVE = Sign(message, sk(IK_A))?
  • No! SIG_EVE = Sign(message, sk(IK_EVE))
  • Bob: ❌ Invalid signature, reject!

Answer: Bob uses Alice's identity key (pk(IK_A)), not Eve's!


💡 Why We Care

The Security Model

X3DH provides mutual authentication:

  • Alice verifies Bob's signed pre-key (Bob's identity)
  • Bob receives and decrypts Alice's message (proves Alice has private key for the DH operations)

Without verification:

  • MITM attack: Eve can replace Bob's keys with hers
  • Alice thinks she's talking to Bob, actually talking to Eve
  • Confidentiality broken, authenticity lost

With verification:

  • Alice ensures Bob's keys are genuine
  • Eve can't impersonate (can't sign with Bob's private key)
  • Trust established before Double Ratchet

The Chain of Trust

Alice's X3DH:
1. Download Bob's public keys
2. Verify signature
├─ If valid → Bob's keys, proceed ✅
└─ If invalid → Eve's keys, reject ❌
3. Generate ephemeral key
4. Compute 4 DH operations
5. Derive shared secret
6. Send message

Bob's X3DH:
1. Receive Alice's message
2. Extract Alice's ephemeral key
3. Compute 4 DH operations (prove Alice has sk(EK_A))
4. Derive shared secret
5. Decrypt message

Result:
- Alice verified Bob's identity (signature)
- Bob can trust Alice (can't decrypt without Alice's ephemeral key)
- Mutual authentication achieved! ✅

✅ Quick Check

Why verify signatures?

To prevent impersonation:

Bob signs his SPK with his IK. Alice verifies.

If signature doesn't match Bob's IK, Eve replaced keys!

Alice rejects "Eve's Bob" and won't do X3DH.

What if Eve captures Alice's ephemeral key when Alice sends it to Bob?

Eve still can't complete X3DH:

Eve needs Bob's private keys (sk(SPKEVE_), sk(IVEVE_)) to complete DH operations.

Eve doesn't have sk(SPKEVE_) or sk(IVEVE_) (only Bob has those!)

Even with Alice's ephemeral key, Eve can't compute DH1, DH2, DH3, DH4 (missing Bob's private keys).

Answer: Eve still can't derive shared secret (needs Bob's private keys)


📋 Key Takeaways

Signature verification: Ensures Bob's keys are genuine
Eve can't forge: Needs Bob's private key (sk(IK_B))
Bob signs: SPK_B with IK_B (signature = SIG_B)
Alice verifies: Does SIG_B match pk(IK_B)?
If invalid: Eve's keys, reject X3DH
If valid: Bob's keys, proceed
Prevents: Man-in-the-middle attacks
Enables: Authentication before Double Ratchet


🎉 X3DH Complete!

Congratulations! You've completed the X3DH section. You now know:

  1. ✅ What X3DH is (four Diffie-Hellman operations)
  2. ✅ Four types of keys (identity, signed pre-key, one-time, ephemeral)
  3. ✅ Complete handshake flow (Bob uploads, Alice initiates, both derive S)
  4. ✅ Why verification matters (prevents impersonation)

Next: The Double Ratchet - forward secrecy per message!

🔧 Continue: What is Ratcheting

We'll learn how the Signal Protocol creates and deletes keys every single message to protect against compromise!


X3DH section complete! Now let's learn the Double Ratchet - how to get forward secrecy per message!