✓ 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!
- Bob signs his signed pre-key with his identity key
- Alice verifies the signature with Bob's identity key
- If signature is valid → It's Bob's key
- 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):
- Bob uploads: pk(IK_B), pk(SPK_B), pk(OPK_B)
- Eve replaces on server with: pk(IK_EVE), pk(SPK_EVE), pk(OPK_EVE)
- Alice downloads what she thinks are Bob's keys
- Alice does X3DH with Eve's keys!
- Alice is now talking to Eve (impersonating Bob)!
- Eve reads Alice's messages, responds as "Bob"
With verification (what actually happens):
- Bob uploads: pk(IK_B), pk(SPK_B), pk(OPK_B), SIG_B (SIG_B is signed with sk(IK_B))
- Eve tries to replace with: pk(IK_EVE), pk(SPK_EVE), pk(OPK_EVE), SIG_EVE (SIG_EVE is signed with sk(IK_EVE))
- Alice downloads: pk(IK_B), pk(SPK_EVE), pk(OPK_EVE), SIG_EVE
- Alice verifies: Does SIG_EVE verify with pk(IK_B)?
- Alice checks: Is SIG_EVE = Sign(pk(SPK_EVE), sk(IK_EVE))?
- Alice sees: ❌ NO! SIG_EVE doesn't match!
- Alice: "Eve signed this, not Bob!" or "This signature is from a different key than I have!"
- 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:
- Alice downloads "Bob's keys" from server
- Eve replaced with her own keys
- Alice does X3DH to Eve (thinking it's Bob!)
- Alice sends sensitive data to Eve (as "Bob")
- Eve learns Alice's secrets
With verification:
- Alice downloads "Bob's keys"
- Alice verifies signature
- Alice sees: "This signature isn't from Bob!"
- 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:
- ✅ What X3DH is (four Diffie-Hellman operations)
- ✅ Four types of keys (identity, signed pre-key, one-time, ephemeral)
- ✅ Complete handshake flow (Bob uploads, Alice initiates, both derive S)
- ✅ 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!