libsoliton/tamarin/LO_KEX.spthy
Kamal Tufekcic e6d0a1ef1a
CryptoVerif and Tamarin models, minor doc updates
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
2026-04-13 01:51:32 +03:00

266 lines
No EOL
8.8 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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