215 lines
7.3 KiB
Text
215 lines
7.3 KiB
Text
theory LO_Ratchet
|
|
begin
|
|
|
|
/*
|
|
* LO-Ratchet: Forward Secrecy (Theorem 4) and Post-Compromise Security (Theorem 5)
|
|
* =================================================================================
|
|
*
|
|
* Abstract model: epoch keys are Fr() (fresh names), not KDF-derived.
|
|
* This proves FS and PCS are STRUCTURAL properties of the state machine,
|
|
* independent of KDF/KEM security. Send and recv are modeled as separate
|
|
* sections to avoid Tamarin non-termination on combined state machines.
|
|
*
|
|
* Corruption comes in two flavors:
|
|
* - Consuming (Corrupt_S/R): reveals state, terminates session → used for FS
|
|
* - Non-consuming (Observe_S/R): reveals state, session continues → used for PCS
|
|
*
|
|
* LIMITATIONS: This abstraction does NOT capture:
|
|
* - KEM-level PCS (compromised sk_s means compromised ss for one step)
|
|
* - Message key derivation (mk = KDF_MsgKey(ek, n)) or Theorem 3
|
|
* - Message counters, skip handling, or replay detection
|
|
*
|
|
* EXPECTED RESULTS:
|
|
* send_sanity: VERIFIED
|
|
* send_fs: VERIFIED (Theorem 4a)
|
|
* send_corrupt_terminates: VERIFIED
|
|
* send_pcs: VERIFIED (Theorem 5, send)
|
|
* recv_sanity: VERIFIED
|
|
* recv_fs_1step: FALSIFIED (Theorem 4b, 1-step counterexample)
|
|
* recv_fs_2step: VERIFIED (Theorem 4b, 2-step FS)
|
|
* recv_corrupt_terminates: VERIFIED
|
|
* recv_pcs: VERIFIED (Theorem 5, recv)
|
|
*/
|
|
|
|
|
|
/* ================= SEND SIDE (unchanged — already works) ================= */
|
|
|
|
restriction Once_S:
|
|
"All #i #j. InitS() @i & InitS() @j ==> #i = #j"
|
|
|
|
rule Bounds_S:
|
|
[ ] --> [ !BS('1','2'), !BS('2','3'), !BS('3','4') ]
|
|
|
|
rule Init_S:
|
|
[ Fr(~ek) ]
|
|
--[ InitS() ]->
|
|
[ S('1', ~ek, 'pending') ]
|
|
|
|
rule KEM_Send:
|
|
[ S(step, ek_s, 'pending'), !BS(step, next), Fr(~ek2) ]
|
|
--[ Send(~ek2) ]->
|
|
[ S(next, ~ek2, 'idle') ]
|
|
|
|
rule Advance_S:
|
|
[ S(step, ek_s, 'idle'), !BS(step, next) ]
|
|
-->
|
|
[ S(next, ek_s, 'pending') ]
|
|
|
|
// Consuming corruption: reveals epoch key, terminates session (for FS proofs)
|
|
rule Corrupt_S:
|
|
[ S(step, ek_s, status) ]
|
|
--[ CorruptS(), RevealedS(ek_s) ]->
|
|
[ ]
|
|
|
|
// Non-consuming observation: reveals epoch key, session continues (for PCS proofs)
|
|
rule Observe_S:
|
|
[ S(step, ek_s, status) ]
|
|
--[ ObserveS(), ObservedS(ek_s) ]->
|
|
[ S(step, ek_s, status) ]
|
|
|
|
restriction Once_ObserveS:
|
|
"All #i #j. ObserveS() @i & ObserveS() @j ==> #i = #j"
|
|
|
|
lemma send_sanity:
|
|
exists-trace
|
|
"Ex ek1 ek2 #i #j. Send(ek1) @i & Send(ek2) @j & i < j"
|
|
|
|
lemma send_fs:
|
|
"All ek ek2 #i #j #c.
|
|
Send(ek) @i & Send(ek2) @j & CorruptS() @c &
|
|
i < j & j < c
|
|
==> not (Ex #r. RevealedS(ek) @r)"
|
|
|
|
// Consuming corruption terminates the session: no Send can follow CorruptS.
|
|
lemma send_corrupt_terminates:
|
|
"All ek #s #c. Send(ek) @s & CorruptS() @c ==> s < c"
|
|
|
|
// Theorem 5 (PCS, send side): after non-consuming observation, one fresh
|
|
// KEM ratchet step produces an epoch key the adversary never observed.
|
|
// (Structurally trivial in this abstraction — Fr() is always fresh — but
|
|
// documents that the state machine permits recovery from compromise.)
|
|
lemma send_pcs:
|
|
"All ek_new #obs #send.
|
|
ObserveS() @obs &
|
|
Send(ek_new) @send & obs < send
|
|
==> not (Ex #r. ObservedS(ek_new) @r)"
|
|
|
|
|
|
/* ================= RECV SIDE (unrolled) ================= */
|
|
|
|
/*
|
|
* State machine (each node is a distinct fact):
|
|
*
|
|
* Init → R1(ek_r, prev) "idle"
|
|
* ↓ Recv1
|
|
* R2(ek_r, prev) "pending"
|
|
* ↓ Adv1
|
|
* R3(ek_r, prev) "idle"
|
|
* ↓ Recv2
|
|
* R4(ek_r, prev) "pending"
|
|
* ↓ Adv2
|
|
* R5(ek_r, prev) "idle"
|
|
* ↓ Recv3
|
|
* R6(ek_r, prev) "pending"
|
|
*
|
|
* Corrupt can consume any R1..R6.
|
|
*
|
|
* Each R_i fact has EXACTLY ONE source rule.
|
|
* Source analysis: zero branching. Proof search: linear.
|
|
*/
|
|
|
|
restriction Once_R:
|
|
"All #i #j. InitR() @i & InitR() @j ==> #i = #j"
|
|
|
|
/* Init: ek_r from KEX, no previous epoch key */
|
|
rule Init_R:
|
|
[ Fr(~ek) ]
|
|
--[ InitR() ]->
|
|
[ R1(~ek, 'nil') ]
|
|
|
|
/* Recv steps: new ek_r, old ek_r → prev */
|
|
rule Recv1:
|
|
[ R1(ek_r, prev), Fr(~ek) ]
|
|
--[ Recv(~ek) ]->
|
|
[ R2(~ek, ek_r) ]
|
|
|
|
rule Adv1:
|
|
[ R2(ek_r, prev) ]
|
|
--[ Adv() ]->
|
|
[ R3(ek_r, prev) ]
|
|
|
|
rule Recv2:
|
|
[ R3(ek_r, prev), Fr(~ek) ]
|
|
--[ Recv(~ek) ]->
|
|
[ R4(~ek, ek_r) ]
|
|
|
|
rule Adv2:
|
|
[ R4(ek_r, prev) ]
|
|
--[ Adv() ]->
|
|
[ R5(ek_r, prev) ]
|
|
|
|
rule Recv3:
|
|
[ R5(ek_r, prev), Fr(~ek) ]
|
|
--[ Recv(~ek) ]->
|
|
[ R6(~ek, ek_r) ]
|
|
|
|
/* Consuming corruption: reveals ek_r and prev, terminates session */
|
|
rule Corrupt_R1: [ R1(ek_r, prev) ] --[ CorruptR(), RevealedR(ek_r), RevealedR(prev) ]-> [ ]
|
|
rule Corrupt_R2: [ R2(ek_r, prev) ] --[ CorruptR(), RevealedR(ek_r), RevealedR(prev) ]-> [ ]
|
|
rule Corrupt_R3: [ R3(ek_r, prev) ] --[ CorruptR(), RevealedR(ek_r), RevealedR(prev) ]-> [ ]
|
|
rule Corrupt_R4: [ R4(ek_r, prev) ] --[ CorruptR(), RevealedR(ek_r), RevealedR(prev) ]-> [ ]
|
|
rule Corrupt_R5: [ R5(ek_r, prev) ] --[ CorruptR(), RevealedR(ek_r), RevealedR(prev) ]-> [ ]
|
|
rule Corrupt_R6: [ R6(ek_r, prev) ] --[ CorruptR(), RevealedR(ek_r), RevealedR(prev) ]-> [ ]
|
|
|
|
/* Non-consuming observation: reveals ek_r and prev, session continues (PCS) */
|
|
rule Observe_R1: [ R1(ek,p) ] --[ ObserveR(), ObservedR(ek), ObservedR(p) ]-> [ R1(ek,p) ]
|
|
rule Observe_R2: [ R2(ek,p) ] --[ ObserveR(), ObservedR(ek), ObservedR(p) ]-> [ R2(ek,p) ]
|
|
rule Observe_R3: [ R3(ek,p) ] --[ ObserveR(), ObservedR(ek), ObservedR(p) ]-> [ R3(ek,p) ]
|
|
rule Observe_R4: [ R4(ek,p) ] --[ ObserveR(), ObservedR(ek), ObservedR(p) ]-> [ R4(ek,p) ]
|
|
rule Observe_R5: [ R5(ek,p) ] --[ ObserveR(), ObservedR(ek), ObservedR(p) ]-> [ R5(ek,p) ]
|
|
rule Observe_R6: [ R6(ek,p) ] --[ ObserveR(), ObservedR(ek), ObservedR(p) ]-> [ R6(ek,p) ]
|
|
|
|
restriction Once_ObserveR:
|
|
"All #i #j. ObserveR() @i & ObserveR() @j ==> #i = #j"
|
|
|
|
|
|
/* Recv lemmas */
|
|
|
|
lemma recv_sanity:
|
|
exists-trace
|
|
"Ex ek1 ek2 #i #j. Recv(ek1) @i & Recv(ek2) @j & i < j"
|
|
|
|
/* Theorem 4b part 1 (1 step): FALSIFIED
|
|
* After 1 subsequent Recv, old ek_r is in prev — revealed on corrupt. */
|
|
lemma recv_fs_1step:
|
|
"All ek ek2 #i #j #c.
|
|
Recv(ek) @i & Recv(ek2) @j & CorruptR() @c &
|
|
i < j & j < c
|
|
==> not (Ex #r. RevealedR(ek) @r)"
|
|
|
|
/* Theorem 4b part 2 (2 steps): VERIFIED
|
|
* After 2 subsequent Recvs, prev has been overwritten — old ek_r gone. */
|
|
lemma recv_fs_2step:
|
|
"All ek ek2 ek3 #i #j #k #c.
|
|
Recv(ek) @i & Recv(ek2) @j & Recv(ek3) @k & CorruptR() @c &
|
|
i < j & j < k & k < c
|
|
==> not (Ex #r. RevealedR(ek) @r)"
|
|
|
|
// Consuming corruption terminates the session: no Recv can follow CorruptR.
|
|
lemma recv_corrupt_terminates:
|
|
"All ek #r #c. Recv(ek) @r & CorruptR() @c ==> r < c"
|
|
|
|
// PCS (recv side, ABSTRACT MODEL ONLY): after non-consuming observation,
|
|
// one fresh recv step produces an epoch key the adversary never observed.
|
|
// NOTE: This is an artifact of the Fr()-based abstraction, NOT a Theorem 5
|
|
// result. In the real protocol (see LO_Ratchet_PCS.spthy), recv-only PCS
|
|
// is NOT achieved — the adversary holds sk_s from the compromise and can
|
|
// derive ss from the peer's ciphertext. Real PCS requires a direction
|
|
// change (send after recv). See §9.2 worked trace.
|
|
lemma recv_pcs:
|
|
"All ek_new #obs #recv.
|
|
ObserveR() @obs &
|
|
Recv(ek_new) @recv & obs < recv
|
|
==> not (Ex #r. ObservedR(ek_new) @r)"
|
|
|
|
end
|