examples/messaging/signal.vpactive attacker · 2 sessions · 2204 ms · c0 a1 c1 a0 c0 a1
Protocol
Alice computes galongterm ← PUBKEY(alongterm). Bob generates bo, computes gblongterm ← PUBKEY(blongterm), gbs ← PUBKEY(bs), gbo ← PUBKEY(bo), gbssig ← SIGN(blongterm, gbs). Hop 1: Bob to Alice: [gblongterm], gbssig, gbs, gbo Alice generates ae1, computes SIGNVERIF(gblongterm, gbs, gbssig)?, gae1 ← PUBKEY(ae1), amaster ← HASH(DH_KEX(gbs, alongterm), DH_KEX(gblongterm, ae1), DH_KEX(gbs, ae1), DH_KEX(gbo, ae1)). Alice generates m1, ae2, n_e1, computes gae2 ← PUBKEY(ae2), akshared1 ← DH_KEX(gbs, ae2), arkab1, ackab1 ← HKDF(amaster, akshared1, nil), akenc1 ← HKDF(nil, MAC(ackab1, nil), nil), e1 ← AEAD_ENC(akenc1, n_e1, m1, HASH(galongterm, gblongterm, gae2)). Hop 2: Alice to Bob: galongterm†, gae1†, gae2†, n_e1†, e1† Bob computes bmaster ← HASH(DH_KEX(galongterm, bs), DH_KEX(gae1, blongterm), DH_KEX(gae1, bs), DH_KEX(gae1, bo)). Bob computes bkshared1 ← DH_KEX(gae2, bs), brkab1, bckab1 ← HKDF(bmaster, bkshared1, nil), bkenc1 ← HKDF(nil, MAC(bckab1, nil), nil), m1_d ← AEAD_DEC(bkenc1, n_e1, e1, HASH(galongterm, gblongterm, gae2))?. Bob generates m2, be, n_e2, computes gbe ← PUBKEY(be), bkshared2 ← DH_KEX(gae2, be), brkba2, bckba2 ← HKDF(brkab1, bkshared2, nil), bkenc2 ← HKDF(nil, MAC(bckba2, nil), nil), e2 ← AEAD_ENC(bkenc2, n_e2, m2, HASH(galongterm, gblongterm, gbe)). Hop 3: Bob to Alice: gbe, n_e2, e2 Alice computes akshared2 ← DH_KEX(gbe, ae2), arkba2, ackba2 ← HKDF(arkab1, akshared2, nil), akenc2 ← HKDF(nil, MAC(ackba2, nil), nil), m2_d ← AEAD_DEC(akenc2, n_e2, e2, HASH(galongterm, gblongterm, gbe))?. Alice generates m3, ae3, n_e3, computes gae3 ← PUBKEY(ae3), akshared3 ← DH_KEX(gbe, ae3), arkab3, ackab3 ← HKDF(arkba2, akshared3, nil), akenc3 ← HKDF(nil, MAC(ackab3, nil), nil), e3 ← AEAD_ENC(akenc3, n_e3, m3, HASH(galongterm, gblongterm, gae3)). Hop 4: Alice to Bob: gae3†, n_e3†, e3† Bob computes bkshared3 ← DH_KEX(gae3, be), brkab3, bckab3 ← HKDF(brkba2, bkshared3, nil), bkenc3 ← HKDF(nil, MAC(bckab3, nil), nil), m3_d ← AEAD_DEC(bkenc3, n_e3, e3, HASH(galongterm, gblongterm, gae3))?. Phase 1 begins. Alice leaks alongterm Bob leaks blongterm
Protocol sequence. Guarded values are written in brackets; a dagger marks every value some attack below substitutes or replays.
Verdicts 3 of 6 queries failed.
✓
confidentiality confidentiality? m1 line 186 1 session variant
Holds (search exhausted at 2 sessions)
×
authentication authentication? Alice -> Bob: e1 line 187 1 session variant
Contradiction found
e1 (AEAD_ENC(bkenc1, AEAD_ENC(HKDF(nil, MAC(HKDF(brkba2, bkshared2, nil)|2, nil), nil)|1, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil)))), sent by Attacker and not by Alice, is successfully used in AEAD_DEC(bkenc1, n_e1, e1, HASH(galongterm, gblongterm, gae2))? within Bob's state.
×
confidentiality confidentiality? m2 line 188 1 session variant
Contradiction found
m2 (m2) is obtained by Attacker.
✓
confidentiality confidentiality? m3 line 190 1 session variant
Holds (search exhausted at 2 sessions)
×
authentication authentication? Alice -> Bob: e3 line 191 1 session variant
Contradiction found
e3 (AEAD_ENC(bkenc3, n_e2, nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil)))), sent by Attacker and not by Alice, is successfully used in AEAD_DEC(bkenc3, n_e3, e3, HASH(galongterm, gblongterm, gae3))? within Bob's state.
Attack trace · authentication? Alice -> Bob: e1
1-14 derive 14 derivation steps
1 derive Attacker constructs PUBKEY(nil).
2 derive Attacker replaced galongterm with PUBKEY(nil), gae1 with PUBKEY(nil), gae2 with PUBKEY(nil), after which n_e1 resolved to AEAD_ENC(HKDF(nil, MAC(HKDF(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))).
3 derive Attacker observes gbs on the wire.
4 derive Attacker constructs DH_KEX(gbs, nil).
5 derive Attacker observes gblongterm on the wire.
6 derive Attacker constructs DH_KEX(gblongterm, nil).
7 derive Attacker observes gbo on the wire.
8 derive Attacker constructs DH_KEX(gbo, nil).
9 derive Attacker constructs HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)).
10 derive Attacker constructs HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2.
11 derive Attacker constructs MAC(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2, nil).
12 derive Attacker constructs HKDF(nil, MAC(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2, nil), nil)|1.
13 derive Attacker constructs HASH(PUBKEY(nil), gblongterm, PUBKEY(nil)).
14 derive Attacker constructs AEAD_ENC(HKDF(nil, MAC(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2, nil), nil)|1, AEAD_ENC(HKDF(nil, MAC(HKDF(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))).
15 mutations Attacker intercepts the message from Alice to Bob and delivers:galongterm ⇐ PUBKEY(nil) honest value: PUBKEY(alongterm) gae1 ⇐ PUBKEY(nil) honest value: PUBKEY(ae1) gae2 ⇐ PUBKEY(nil) honest value: PUBKEY(ae2) n_e1 ⇐ AEAD_ENC(HKDF(nil, MAC(HKDF(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))) honest value: n_e1 e1 ⇐ AEAD_ENC(HKDF(nil, MAC(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2, nil), nil)|1, AEAD_ENC(HKDF(nil, MAC(HKDF(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))) honest value: AEAD_ENC(akenc1, n_e1, m1, HASH(galongterm, gblongterm, gae2))
16 gate Bob's AEAD_DEC(bkenc1, AEAD_ENC(HKDF(nil, MAC(HKDF(brkba2, bkshared2, nil)|2, nil), nil)|1, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), AEAD_ENC(bkenc1, AEAD_ENC(HKDF(nil, MAC(HKDF(brkba2, bkshared2, nil)|2, nil), nil)|1, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil)))? passes — the attacker controls one of its inputs.
17-21 derive 5 derivation steps
17 derive Attacker observes n_e1 on the wire.
18 derive Attacker observes e1 on the wire.
19 derive Attacker observes galongterm on the wire.
20 derive Attacker observes gblongterm on the wire.
21 derive Attacker observes gae2 on the wire.
Attack trace · confidentiality? m2
1-14 derive 14 derivation steps
1 derive Attacker constructs PUBKEY(nil).
2 derive Attacker observes gbs on the wire.
3 derive Attacker constructs DH_KEX(gbs, nil).
4 derive Attacker observes gblongterm on the wire.
5 derive Attacker constructs DH_KEX(gblongterm, nil).
6 derive Attacker observes gbo on the wire.
7 derive Attacker constructs DH_KEX(gbo, nil).
8 derive Attacker constructs HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)).
9 derive Attacker constructs HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2.
10 derive Attacker constructs MAC(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2, nil).
11 derive Attacker constructs HKDF(nil, MAC(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2, nil), nil)|1.
12 derive Attacker observes n_e1 on the wire.
13 derive Attacker constructs HASH(PUBKEY(nil), gblongterm, PUBKEY(nil)).
14 derive Attacker constructs AEAD_ENC(HKDF(nil, MAC(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2, nil), nil)|1, n_e1, nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))).
15 mutations Attacker intercepts the message from Alice to Bob and delivers:galongterm ⇐ PUBKEY(nil) honest value: PUBKEY(alongterm) gae1 ⇐ PUBKEY(nil) honest value: PUBKEY(ae1) gae2 ⇐ PUBKEY(nil) honest value: PUBKEY(ae2) e1 ⇐ AEAD_ENC(HKDF(nil, MAC(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2, nil), nil)|1, n_e1, nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))) honest value: AEAD_ENC(akenc1, n_e1, m1, HASH(galongterm, gblongterm, gae2))
16 gate Bob's AEAD_DEC(bkenc1, n_e1, AEAD_ENC(bkenc1, n_e1, nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil)))? passes — the attacker controls one of its inputs.
17-25 derive 9 derivation steps
17 derive Attacker observes AEAD_ENC(HKDF(nil, MAC(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1, n_e2, m2, HASH(PUBKEY(nil), gblongterm, gbe)) on the wire.
18 derive Attacker constructs HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1.
19 derive Attacker observes gbe on the wire.
20 derive Attacker constructs DH_KEX(gbe, nil).
21 derive Attacker constructs HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|2.
22 derive Attacker constructs MAC(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil).
23 derive Attacker constructs HKDF(nil, MAC(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1.
24 derive Attacker observes n_e2 on the wire.
25 derive Attacker opens AEAD_ENC(HKDF(nil, MAC(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1, n_e2, m2, HASH(PUBKEY(nil), gblongterm, gbe)) with HKDF(nil, MAC(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1, n_e2, obtaining m2.
Attack trace · authentication? Alice -> Bob: e3
1-14 derive 14 derivation steps
1 derive Attacker constructs PUBKEY(nil).
2 derive Attacker replaced galongterm with PUBKEY(nil), gae1 with PUBKEY(nil), gae2 with PUBKEY(nil), after which n_e1 resolved to AEAD_ENC(HKDF(nil, MAC(HKDF(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))).
3 derive Attacker observes gbs on the wire.
4 derive Attacker constructs DH_KEX(gbs, nil).
5 derive Attacker observes gblongterm on the wire.
6 derive Attacker constructs DH_KEX(gblongterm, nil).
7 derive Attacker observes gbo on the wire.
8 derive Attacker constructs DH_KEX(gbo, nil).
9 derive Attacker constructs HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)).
10 derive Attacker constructs HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2.
11 derive Attacker constructs MAC(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2, nil).
12 derive Attacker constructs HKDF(nil, MAC(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2, nil), nil)|1.
13 derive Attacker constructs HASH(PUBKEY(nil), gblongterm, PUBKEY(nil)).
14 derive Attacker constructs AEAD_ENC(HKDF(nil, MAC(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2, nil), nil)|1, AEAD_ENC(HKDF(nil, MAC(HKDF(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))).
15 mutations Attacker intercepts the message from Alice to Bob and delivers:galongterm ⇐ PUBKEY(nil) honest value: PUBKEY(alongterm) gae1 ⇐ PUBKEY(nil) honest value: PUBKEY(ae1) gae2 ⇐ PUBKEY(nil) honest value: PUBKEY(ae2) n_e1 ⇐ AEAD_ENC(HKDF(nil, MAC(HKDF(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))) honest value: n_e1 e1 ⇐ AEAD_ENC(HKDF(nil, MAC(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|2, nil), nil)|1, AEAD_ENC(HKDF(nil, MAC(HKDF(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))) honest value: AEAD_ENC(akenc1, n_e1, m1, HASH(galongterm, gblongterm, gae2))
16-23 derive 8 derivation steps
16 derive Attacker observes n_e2 on the wire.
17 derive Attacker constructs HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1.
18 derive Attacker observes gbe on the wire.
19 derive Attacker constructs DH_KEX(gbe, nil).
20 derive Attacker constructs HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1.
21 derive Attacker constructs HKDF(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1, DH_KEX(gbe, nil), nil)|2.
22 derive Attacker constructs MAC(HKDF(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil).
23 derive Attacker constructs HKDF(nil, MAC(HKDF(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1.
24 mutations Attacker intercepts the message from Alice to Bob and delivers:gae3 ⇐ PUBKEY(nil) honest value: PUBKEY(ae3) n_e3 ⇐ n_e2 honest value: n_e3 e3 ⇐ AEAD_ENC(HKDF(nil, MAC(HKDF(HKDF(HKDF(HASH(DH_KEX(gbs, nil), DH_KEX(gblongterm, nil), DH_KEX(gbs, nil), DH_KEX(gbo, nil)), DH_KEX(gbs, nil), nil)|1, DH_KEX(gbe, nil), nil)|1, DH_KEX(gbe, nil), nil)|2, nil), nil)|1, n_e2, nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))) honest value: AEAD_ENC(akenc3, n_e3, m3, HASH(galongterm, gblongterm, gae3))
25 gate Bob's AEAD_DEC(bkenc1, AEAD_ENC(bkenc3, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), AEAD_ENC(bkenc1, AEAD_ENC(bkenc3, n_e3, PUBKEY(nil), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil)))? passes — the attacker controls one of its inputs.
26 gate Bob's AEAD_DEC(bkenc3, n_e2, AEAD_ENC(bkenc3, n_e2, nil, HASH(PUBKEY(nil), gblongterm, PUBKEY(nil))), HASH(PUBKEY(nil), gblongterm, PUBKEY(nil)))? passes — the attacker controls one of its inputs.
27-31 derive 5 derivation steps
27 derive Attacker observes n_e3 on the wire.
28 derive Attacker observes e3 on the wire.
29 derive Attacker observes galongterm on the wire.
30 derive Attacker observes gblongterm on the wire.
31 derive Attacker observes gae3 on the wire.
Model source
// SPDX-FileCopyrightText: © 2019-2026 Nadim Kobeissi <[email protected] >
// SPDX-License-Identifier: GPL-3.0-only
//
// The Signal protocol: an X3DH handshake followed by three messages of the
// Double Ratchet, following the two Signal specifications (X3DH revision 1,
// The Double Ratchet Algorithm revision 4). Phase 1 then leaks BOTH parties'
// long-term identity keys, and every query still holds (c0a0c0a0c0a0) — the
// model demonstrates the two properties Signal is famous for:
//
// Forward secrecy: message keys descend from ephemeral Diffie-Hellman
// shares (via the X3DH secret and each ratchet step), so a later
// compromise of identity keys opens no recorded ciphertext.
//
// Authentication under compromise: the leak happens after phase 0, and
// phases model "later" — within the conversation, every ciphertext is
// bound to the sender's ratchet state, which the attacker cannot enter
// without a private ephemeral it never gets.
//
// X3DH in one paragraph: Bob publishes an identity key IK_B (gblongterm), a
// signed prekey SPK_B (gbs, signature gbssig) and a one-time prekey OPK_B
// (gbo). Alice verifies the prekey signature, generates an ephemeral EK_A
// (ae1), and derives the session secret SK (amaster) from four DH values:
//
// DH1 = DH(IK_A, SPK_B) — authenticates Alice to Bob
// DH2 = DH(EK_A, IK_B) — authenticates Bob to Alice
// DH3 = DH(EK_A, SPK_B) — forward secrecy from Alice's side
// DH4 = DH(EK_A, OPK_B) — forward secrecy from Bob's side
// SK = KDF(DH1 || DH2 || DH3 || DH4)
//
// No single compromised value breaks all four at once. SK then seeds the
// Double Ratchet: it is the initial root key, and Bob's signed prekey
// doubles as his initial ratchet key pair (Double Ratchet §7.1). Every
// ratchet step below is the spec's key schedule:
//
// RK', CK = KDF_RK(RK, DH(own ratchet key, peer ratchet key))
// = HKDF(salt: RK, ikm: the fresh DH share)
// mk = KDF_CK(CK) — an HMAC keyed by the chain key: MAC(ck, nil)
//
// and the AEAD key is expanded from mk the way the spec's recommended
// ENCRYPT does internally: a zero-salt HKDF of mk (§7.2). This conversation
// alternates strictly, so every send answers a freshly received ratchet
// public key with a full DH ratchet step; each chain key therefore yields
// exactly one message key, and KDF_CK's next-chain-key output is elided.
// New ratchet keys are generated at send time — the deferred variant of
// §8.5. The associated data models CONCAT(AD, header): the X3DH identity
// binding AD = Encode(IK_A) || Encode(IK_B), in that fixed order for both
// directions, plus the header's ratchet public key (its message counters
// carry no secrets and are elided).
//
// Modeling notes: the identity public keys travel guarded ([…]) — Signal
// users verify safety numbers out-of-band; try removing a guard and watch
// the queries fail. The prekey signature is verified with a checked
// SIGNVERIF before Alice touches gbs, exactly as X3DH §3.3 orders the
// steps. bs is `knows private` while bo is `generates`: under Verifpal's
// parallel sessions, the medium-term signed prekey is shared across
// sessions while each session gets a fresh one-time prekey, matching the
// two lifetimes in the spec. See pqxdh.vp for Signal's post-quantum
// successor to this handshake, and signal_twelve.vp for this same model
// extended to twelve ratchet messages.
attacker [active ]
principal Alice [
// Alice's long-term identity key pair, IK_A.
knows private alongterm
galongterm = PUBKEY (alongterm)
]
principal Bob [
// blongterm: the identity key IK_B. bs: the signed prekey SPK_B,
// uploaded to the server in advance so Alice can start a session while
// Bob is offline — it doubles as Bob's initial ratchet key pair.
knows private blongterm, bs
// bo: a one-time prekey OPK_B, fresh per session.
generates bo
gblongterm = PUBKEY (blongterm)
gbs = PUBKEY (bs)
gbo = PUBKEY (bo)
// Bob signs his prekey with his identity key: this is what stops an
// attacker from handing Alice a prekey of its own.
gbssig = SIGN (blongterm, gbs)
]
// Bob's "prekey bundle", as fetched from the Signal server. The identity
// key is guarded — its authenticity is anchored out-of-band (safety
// numbers) — while the rest is protected by the signature, not the wire.
Bob -> Alice : [gblongterm], gbssig, gbs, gbo
principal Alice [
// Checked signature verification, before the prekey is used anywhere:
// Alice aborts rather than run X3DH against an unsigned prekey.
_ = SIGNVERIF (gblongterm, gbs, gbssig)?
generates ae1
gae1 = PUBKEY (ae1)
// The four X3DH shared secrets, combined into the session secret SK.
amaster = HASH (DH_KEX (gbs, alongterm), DH_KEX (gblongterm, ae1), DH_KEX (gbs, ae1), DH_KEX (gbo, ae1))
]
principal Alice [
generates m1, ae2
// ae2 is Alice's initial ratchet key; Bob's half is his signed prekey.
gae2 = PUBKEY (ae2)
akshared1 = DH_KEX (gbs, ae2)
// Ratchet initialization, one root-KDF step: SK is the initial root
// key, mixed with the first ratchet DH share — KDF_RK(SK, DH(ae2, SPK_B)).
arkab1, ackab1 = HKDF (amaster, akshared1, nil)
// Message key derivation: KDF_CK is an HMAC keyed by the chain key,
// and ENCRYPT expands it into the AEAD key with a zero-salt HKDF.
akenc1 = HKDF (nil, MAC (ackab1, nil), nil)
// The associated data binds both identities — AD = IK_A || IK_B, the
// same order in both directions — and the header's ratchet key, so
// replaying the ciphertext in another context fails AEAD_DEC.
generates n_e1
e1 = AEAD_ENC (akenc1, n_e1, m1, HASH (galongterm, gblongterm, gae2))
]
Alice -> Bob : galongterm, gae1, gae2, n_e1, e1
principal Bob [
// Bob mirrors X3DH from his side: same four DH values, opposite
// private/public halves — DH_KEX(PUBKEY(a), b) = DH_KEX(PUBKEY(b), a).
bmaster = HASH (DH_KEX (galongterm, bs), DH_KEX (gae1, blongterm), DH_KEX (gae1, bs), DH_KEX (gae1, bo))
]
principal Bob [
// The receiving half of the same ratchet step.
bkshared1 = DH_KEX (gae2, bs)
brkab1, bckab1 = HKDF (bmaster, bkshared1, nil)
bkenc1 = HKDF (nil, MAC (bckab1, nil), nil)
// Checked decryption: a forged or replayed e1 halts Bob here.
m1_d = AEAD_DEC (bkenc1, n_e1, e1, HASH (galongterm, gblongterm, gae2))?
]
// Message 2: Bob replies, advancing the DH ratchet with his own fresh
// ephemeral be, generated at send time (the deferred variant of §8.5).
// Note the root-key chaining: each KDF_RK takes the previous root key as
// its salt, so history is folded into every future key.
principal Bob [
generates m2, be
gbe = PUBKEY (be)
bkshared2 = DH_KEX (gae2, be)
brkba2, bckba2 = HKDF (brkab1, bkshared2, nil)
bkenc2 = HKDF (nil, MAC (bckba2, nil), nil)
generates n_e2
e2 = AEAD_ENC (bkenc2, n_e2, m2, HASH (galongterm, gblongterm, gbe))
]
Bob -> Alice : gbe, n_e2, e2
principal Alice [
akshared2 = DH_KEX (gbe, ae2)
arkba2, ackba2 = HKDF (arkab1, akshared2, nil)
akenc2 = HKDF (nil, MAC (ackba2, nil), nil)
m2_d = AEAD_DEC (akenc2, n_e2, e2, HASH (galongterm, gblongterm, gbe))?
]
// Message 3: back to Alice — another full ratchet step.
principal Alice [
generates m3, ae3
gae3 = PUBKEY (ae3)
akshared3 = DH_KEX (gbe, ae3)
arkab3, ackab3 = HKDF (arkba2, akshared3, nil)
akenc3 = HKDF (nil, MAC (ackab3, nil), nil)
generates n_e3
e3 = AEAD_ENC (akenc3, n_e3, m3, HASH (galongterm, gblongterm, gae3))
]
Alice -> Bob : gae3, n_e3, e3
principal Bob [
bkshared3 = DH_KEX (gae3, be)
brkab3, bckab3 = HKDF (brkba2, bkshared3, nil)
bkenc3 = HKDF (nil, MAC (bckab3, nil), nil)
m3_d = AEAD_DEC (bkenc3, n_e3, e3, HASH (galongterm, gblongterm, gae3))?
]
phase [1]
// Both identity keys fall — a full long-term compromise of both parties,
// after the conversation. The ephemerals (ae1, ae2, ae3, be, bs, bo) do
// not leak; they are what forward secrecy stands on.
principal Alice [leaks alongterm]
principal Bob [leaks blongterm]
queries [
confidentiality? m1
authentication? Alice -> Bob: e1
confidentiality? m2
authentication? Bob -> Alice: e2
confidentiality? m3
authentication? Alice -> Bob: e3
]
Scope of this result
Every verdict above was reached against an active attacker, with each principal running 2 concurrent sessions, over exactly the model as written. An attack is a witness and stands on its own. A query reported as holding says only that this search found no attack at those parameters: the search space this engine defines was explored, which is never the space of all attacks.