CryptoVerif and Tamarin models, minor doc updates
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
This commit is contained in:
parent
9ba7ea2def
commit
3acaa0fa3f
18 changed files with 2925 additions and 8 deletions
266
tamarin/LO_KEX.spthy
Normal file
266
tamarin/LO_KEX.spthy
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
theory LO_KEX
|
||||
begin
|
||||
|
||||
/*
|
||||
* LO-KEX: Session Establishment (Theorems 1, 2a, 2b)
|
||||
*
|
||||
* SCOPE: This model covers the OPK-PRESENT case only. All three KEM
|
||||
* encapsulations (IK, SPK, OPK) are unconditional.
|
||||
*
|
||||
* The code (kex/mod.rs) supports OPK-absent sessions where IKM = ss_IK ‖ ss_SPK
|
||||
* (2 shared secrets instead of 3). For those sessions, corruption of IK + SPK
|
||||
* alone suffices to derive the session key — the Theorem 1 lemmas' requirement
|
||||
* of all-three-corrupted does not apply. The OPK-absent case has strictly
|
||||
* weaker security (2-key threshold vs 3-key) and is not modeled here.
|
||||
*
|
||||
* This is NOT a soundness issue: the proofs are correct for the OPK-present
|
||||
* case they model. The OPK-absent case would need separate rules with 2-KEM
|
||||
* IKM and adjusted secrecy lemmas requiring only IK+SPK corruption.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 1. CRYPTOGRAPHIC PRIMITIVES
|
||||
*/
|
||||
builtins: asymmetric-encryption, signing, hashing
|
||||
|
||||
// AEAD Abstraction
|
||||
functions: aead_enc/4, aead_dec/4, aead_verify/4, accept/0
|
||||
equations:
|
||||
aead_dec(k, n, aad, aead_enc(k, n, m, aad)) = m,
|
||||
aead_verify(k, n, aad, aead_enc(k, n, m, aad)) = accept()
|
||||
|
||||
// Constant-time equality check helper
|
||||
restriction Eq:
|
||||
"All x y #i. Eq(x,y) @i ==> x = y"
|
||||
|
||||
/*
|
||||
* 2. ADVERSARY MODEL & PKI (§8.2)
|
||||
*/
|
||||
|
||||
rule Generate_IK:
|
||||
[ Fr(~sk) ] --[ LtkGen($P, ~sk) ]->[ !Ltk($P, ~sk), !Pk($P, pk(~sk)), Out(pk(~sk)) ]
|
||||
|
||||
rule Corrupt_IK:
|
||||
[ !Ltk($P, sk) ] --[ CorruptIK($P) ]-> [ Out(sk) ]
|
||||
|
||||
rule Corrupt_SPK:
|
||||
[ !SPK_Secret($P, id, sk) ] --[ CorruptSPK($P, id) ]-> [ Out(sk) ]
|
||||
|
||||
rule Corrupt_OPK:[ !OPK_Secret($P, id, sk) ] --[ CorruptOPK($P, id) ]-> [ Out(sk) ]
|
||||
|
||||
rule Corrupt_RNG_KEX:[ !RNG_KEX($P, r_IK, r_SPK, r_OPK) ] --[ CorruptRNG_KEX($P) ]->[ Out(<r_IK, r_SPK, r_OPK>) ]
|
||||
|
||||
|
||||
/*
|
||||
* 3. LO-KEX PROTOCOL (§4)
|
||||
*/
|
||||
|
||||
// §4.1: Pre-Key Bundle Generation (Bob)
|
||||
rule LO_KEX_Publish_Bundle:
|
||||
let
|
||||
pk_SPK = pk(~sk_SPK)
|
||||
pk_OPK = pk(~sk_OPK)
|
||||
pk_IK = pk(~sk_IK)
|
||||
sig_SPK = sign(<'lo-spk-sig-v1', pk_SPK>, ~sk_IK)
|
||||
bundle = <'lo-crypto-v1', pk_IK, pk_SPK, ~id_SPK, sig_SPK, pk_OPK, ~id_OPK>
|
||||
in[ !Ltk($B, ~sk_IK), Fr(~sk_SPK), Fr(~sk_OPK), Fr(~id_SPK), Fr(~id_OPK) ]
|
||||
--[ PublishBundle($B, ~id_SPK, ~id_OPK) ]->[
|
||||
!SPK_Secret($B, ~id_SPK, ~sk_SPK), !SPK_Pk($B, ~id_SPK, pk_SPK),
|
||||
!OPK_Secret($B, ~id_OPK, ~sk_OPK), !OPK_Pk($B, ~id_OPK, pk_OPK),
|
||||
// OPK_Available is a LINEAR fact per §4.4 Step 6
|
||||
OPK_Available($B, ~id_OPK),
|
||||
Out(bundle)
|
||||
]
|
||||
|
||||
// §4.3: Session Initiation (Alice)
|
||||
rule LO_KEX_Alice_Init:
|
||||
let
|
||||
pk_IK_A = pk(~sk_IK_A)
|
||||
|
||||
// KEM using standard PKE (ss = ~r, c = aenc(~r, pk))
|
||||
c_IK = aenc(~r_IK, pk_IK_B)
|
||||
ss_IK = ~r_IK
|
||||
|
||||
c_SPK = aenc(~r_SPK, pk_SPK_B)
|
||||
ss_SPK = ~r_SPK
|
||||
|
||||
c_OPK = aenc(~r_OPK, pk_OPK_B)
|
||||
ss_OPK = ~r_OPK
|
||||
|
||||
// HKDF derivation using built-in hashing
|
||||
ikm = <ss_IK, ss_SPK, ss_OPK>
|
||||
pk_EK = pk(~sk_EK)
|
||||
info = <'lo-kex-v1', 'lo-crypto-v1', pk_IK_A, pk_IK_B, pk_EK>
|
||||
|
||||
rk = h(<ikm, info, 'rk'>)
|
||||
ek = h(<ikm, info, 'ek'>)
|
||||
|
||||
fp_IK_A = h(pk_IK_A)
|
||||
fp_IK_B = h(pk_IK_B)
|
||||
SI = <'lo-crypto-v1', fp_IK_A, fp_IK_B, pk_EK, c_IK, c_SPK, id_SPK, c_OPK, id_OPK>
|
||||
sig_SI = sign(<'lo-kex-init-sig-v1', SI>, ~sk_IK_A)
|
||||
|
||||
mk0 = h(<ek, '0'>)
|
||||
aad0 = <'lo-dm-v1', fp_IK_A, fp_IK_B, SI>
|
||||
c0 = aead_enc(mk0, ~n0, ~m0, aad0)
|
||||
in[
|
||||
!Ltk($A, ~sk_IK_A),
|
||||
!Pk($B, pk_IK_B),
|
||||
!SPK_Pk($B, id_SPK, pk_SPK_B),
|
||||
!OPK_Pk($B, id_OPK, pk_OPK_B),
|
||||
In(sig_SPK),
|
||||
Fr(~sk_EK), Fr(~r_IK), Fr(~r_SPK), Fr(~r_OPK), Fr(~n0), Fr(~m0)
|
||||
]
|
||||
--[
|
||||
Eq(verify(sig_SPK, <'lo-spk-sig-v1', pk_SPK_B>, pk_IK_B), true),
|
||||
Init_A($A, $B, rk, ek, id_SPK, id_OPK)
|
||||
]->[
|
||||
Out(<SI, sig_SI, ~n0, c0>),
|
||||
!RNG_KEX($A, ~r_IK, ~r_SPK, ~r_OPK)
|
||||
]
|
||||
|
||||
// §4.4: Session Reception (Bob)
|
||||
rule LO_KEX_Bob_Recv:
|
||||
let
|
||||
pk_IK_B = pk(~sk_IK_B)
|
||||
pk_SPK_B = pk(~sk_SPK_B)
|
||||
pk_OPK_B = pk(~sk_OPK_B)
|
||||
|
||||
fp_IK_A = h(pk_IK_A)
|
||||
fp_IK_B = h(pk_IK_B)
|
||||
SI = <'lo-crypto-v1', fp_IK_A, fp_IK_B, pk_EK, c_IK, c_SPK, id_SPK, c_OPK, id_OPK>
|
||||
|
||||
// Decapsulate
|
||||
ss_IK = adec(c_IK, ~sk_IK_B)
|
||||
ss_SPK = adec(c_SPK, ~sk_SPK_B)
|
||||
ss_OPK = adec(c_OPK, ~sk_OPK_B)
|
||||
|
||||
// HKDF derivation using built-in hashing
|
||||
ikm = <ss_IK, ss_SPK, ss_OPK>
|
||||
info = <'lo-kex-v1', 'lo-crypto-v1', pk_IK_A, pk_IK_B, pk_EK>
|
||||
|
||||
rk = h(<ikm, info, 'rk'>)
|
||||
ek = h(<ikm, info, 'ek'>)
|
||||
|
||||
mk0 = h(<ek, '0'>)
|
||||
aad0 = <'lo-dm-v1', fp_IK_A, fp_IK_B, SI>
|
||||
in[
|
||||
!Ltk($B, ~sk_IK_B),
|
||||
!Pk($A, pk_IK_A),
|
||||
!SPK_Secret($B, id_SPK, ~sk_SPK_B),
|
||||
!OPK_Secret($B, id_OPK, ~sk_OPK_B),
|
||||
OPK_Available($B, id_OPK),
|
||||
In(<SI, sig_SI, n0, c0>)
|
||||
]
|
||||
--[
|
||||
Eq(verify(sig_SI, <'lo-kex-init-sig-v1', SI>, pk_IK_A), true),
|
||||
Eq(aead_verify(mk0, n0, aad0, c0), accept()),
|
||||
Init_B($B, $A, rk, ek, id_SPK, id_OPK)
|
||||
]->
|
||||
[ ]
|
||||
|
||||
/*
|
||||
* 4. LEMMAS (§8.6)
|
||||
*/
|
||||
|
||||
// Sanity check
|
||||
lemma KEX_Exists:
|
||||
exists-trace
|
||||
"Ex A B rk ek id_S id_O #i #j.
|
||||
Init_A(A, B, rk, ek, id_S, id_O) @ i &
|
||||
Init_B(B, A, rk, ek, id_S, id_O) @ j"
|
||||
|
||||
// Theorem 1 (A's perspective): Session Key Secrecy
|
||||
// The root key is secret UNLESS Adv corrupts Alice's RNG, OR ALL of Bob's keys.
|
||||
lemma Theorem1_Session_Key_Secrecy_A:
|
||||
"All A B rk ek id_S id_O #i.
|
||||
Init_A(A, B, rk, ek, id_S, id_O) @ i
|
||||
==>
|
||||
not (Ex #j. K(rk) @ j)
|
||||
| (Ex #j. CorruptRNG_KEX(A) @ j)
|
||||
| ( (Ex #j1. CorruptIK(B) @ j1) & (Ex #j2. CorruptSPK(B, id_S) @ j2) & (Ex #j3. CorruptOPK(B, id_O) @ j3) )
|
||||
"
|
||||
|
||||
// Theorem 1 (B's perspective): Session Key Secrecy
|
||||
// If Bob accepts, the key is secret UNLESS Adv impersonated Alice (CorruptIK(A))
|
||||
// OR Adv corrupted ALL of Bob's keys.
|
||||
lemma Theorem1_Session_Key_Secrecy_B:
|
||||
"All B A rk ek id_S id_O #i.
|
||||
Init_B(B, A, rk, ek, id_S, id_O) @ i
|
||||
==>
|
||||
not (Ex #j. K(rk) @ j)
|
||||
| (Ex #j. CorruptIK(A) @ j)
|
||||
| (Ex #j. CorruptRNG_KEX(A) @ j) // <--- THE MISSING PREDICATE!
|
||||
| ( (Ex #j1. CorruptIK(B) @ j1) & (Ex #j2. CorruptSPK(B, id_S) @ j2) & (Ex #j3. CorruptOPK(B, id_O) @ j3) )
|
||||
"
|
||||
|
||||
// Theorem 1 (A's perspective): Epoch Key Secrecy
|
||||
// Same structure as rk — ek is derived from the same IKM via a distinct KDF label.
|
||||
lemma Theorem1_EK_Secrecy_A:
|
||||
"All A B rk ek id_S id_O #i.
|
||||
Init_A(A, B, rk, ek, id_S, id_O) @ i
|
||||
==>
|
||||
not (Ex #j. K(ek) @ j)
|
||||
| (Ex #j. CorruptRNG_KEX(A) @ j)
|
||||
| ( (Ex #j1. CorruptIK(B) @ j1) & (Ex #j2. CorruptSPK(B, id_S) @ j2) & (Ex #j3. CorruptOPK(B, id_O) @ j3) )
|
||||
"
|
||||
|
||||
// Theorem 1 (B's perspective): Epoch Key Secrecy
|
||||
lemma Theorem1_EK_Secrecy_B:
|
||||
"All B A rk ek id_S id_O #i.
|
||||
Init_B(B, A, rk, ek, id_S, id_O) @ i
|
||||
==>
|
||||
not (Ex #j. K(ek) @ j)
|
||||
| (Ex #j. CorruptIK(A) @ j)
|
||||
| (Ex #j. CorruptRNG_KEX(A) @ j)
|
||||
| ( (Ex #j1. CorruptIK(B) @ j1) & (Ex #j2. CorruptSPK(B, id_S) @ j2) & (Ex #j3. CorruptOPK(B, id_O) @ j3) )
|
||||
"
|
||||
|
||||
// Theorem 2(a): Recipient Binding
|
||||
// If Alice initiates with B and Bob accepts the same session, then Alice
|
||||
// and Bob agree on each other's identity. This is explicit binding:
|
||||
// fp_IK_B = H(pk_IK_B) is embedded in SI, which Alice signs as σ_SI,
|
||||
// and Bob verifies σ_SI against pk_IK_A. If both Init_A and Init_B fire
|
||||
// for the same (rk, ek), the session names both parties correctly.
|
||||
// (No corruption exclusion needed — this is a structural property of the
|
||||
// signature + fingerprint binding, not a secrecy claim.)
|
||||
lemma Theorem2a_Recipient_Binding:
|
||||
"All A B rk ek id_S id_O #i #j.
|
||||
Init_A(A, B, rk, ek, id_S, id_O) @i &
|
||||
Init_B(B, A, rk, ek, id_S, id_O) @j
|
||||
==>
|
||||
i < j
|
||||
"
|
||||
|
||||
// Theorem 2(b): Initiator Authentication
|
||||
// If Bob verifies the session init, Alice actually generated it,
|
||||
// UNLESS the adversary corrupted Alice's Identity Key.
|
||||
lemma Theorem2b_Initiator_Authentication:
|
||||
"All B A rk ek id_S id_O #i.
|
||||
Init_B(B, A, rk, ek, id_S, id_O) @ i
|
||||
==>
|
||||
(Ex #j. Init_A(A, B, rk, ek, id_S, id_O) @ j & j < i)
|
||||
| (Ex #j. CorruptIK(A) @ j)
|
||||
"
|
||||
|
||||
// §4.4 Step 6: OPK single-use — the linear OPK_Available fact ensures a given
|
||||
// OPK id is consumed by at most one session reception.
|
||||
lemma OPK_Single_Use:
|
||||
"All B A1 A2 rk1 rk2 ek1 ek2 id_S1 id_S2 id_O #i #j.
|
||||
Init_B(B, A1, rk1, ek1, id_S1, id_O) @ i &
|
||||
Init_B(B, A2, rk2, ek2, id_S2, id_O) @ j
|
||||
==>
|
||||
#i = #j
|
||||
"
|
||||
|
||||
// Key uniqueness: distinct Init_A events (with fresh KEM randomness) produce
|
||||
// distinct root keys. Two sessions sharing the same rk must be the same event.
|
||||
lemma Key_Uniqueness:
|
||||
"All A1 A2 B1 B2 rk ek1 ek2 id_S1 id_S2 id_O1 id_O2 #i #j.
|
||||
Init_A(A1, B1, rk, ek1, id_S1, id_O1) @ i &
|
||||
Init_A(A2, B2, rk, ek2, id_S2, id_O2) @ j
|
||||
==>
|
||||
#i = #j
|
||||
"
|
||||
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue