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
350
tamarin/LO_NegativeTests.spthy
Normal file
350
tamarin/LO_NegativeTests.spthy
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
theory LO_NegativeTests
|
||||
begin
|
||||
|
||||
/*
|
||||
* Negative Tests (Expected Falsifications)
|
||||
* =========================================
|
||||
*
|
||||
* Each lemma SHOULD be falsified. If any verifies, the model is
|
||||
* over-constraining the adversary or making incorrect assumptions.
|
||||
* These confirm the model correctly represents attack paths.
|
||||
*
|
||||
* ALL EXPECTED: FALSIFIED
|
||||
*
|
||||
* neg_auth_ik_corrupt: IK corruption → auth forged
|
||||
* neg_auth_rng_corrupt: RNG corruption → auth forged
|
||||
* neg_ratchet_no_fs_0step: Corrupt immediately → current ek revealed
|
||||
* neg_ratchet_recv_1step: 1-step recv FS fails (prev_ek_r retains)
|
||||
* neg_call_rk_plus_rng: rk + RNG corruption → call key derivable
|
||||
* neg_stream_key_corrupt: Key corruption → forgery possible
|
||||
* neg_reflect_self_session: Self-session → reflection succeeds
|
||||
*/
|
||||
|
||||
builtins: hashing, signing, asymmetric-encryption
|
||||
|
||||
functions: kem_pk/1, kem_c/2, kem_ss/2, kem_decaps/2, mac/2
|
||||
equations: kem_decaps(sk, kem_c(kem_pk(sk), r)) = r
|
||||
|
||||
functions: aead_enc/4, aead_verify/4, accept/0
|
||||
equations: aead_verify(k, n, aad, aead_enc(k, n, m, aad)) = accept()
|
||||
|
||||
functions: kdf_call_a/3
|
||||
|
||||
restriction Eq:
|
||||
"All x y #i. Eq(x,y) @i ==> x = y"
|
||||
|
||||
|
||||
/* =========== NEG 1-2: LO-Auth corruption =========== */
|
||||
|
||||
rule NAuth_GenIK:
|
||||
[ Fr(~sk) ] --[ NAuthGen($P) ]->
|
||||
[ !NAuthLtk($P, ~sk), !NAuthPk($P, kem_pk(~sk)), Out(kem_pk(~sk)) ]
|
||||
|
||||
rule NAuth_CorruptIK:
|
||||
[ !NAuthLtk($P, sk) ] --[ NAuthCorruptIK($P) ]-> [ Out(sk) ]
|
||||
|
||||
rule NAuth_CorruptRNG:
|
||||
[ !NAuthRNG($S, r) ] --[ NAuthCorruptRNG($S, r) ]-> [ Out(r) ]
|
||||
|
||||
rule NAuth_Challenge:
|
||||
let c = kem_c(pk_c, ~r)
|
||||
ss = kem_ss(pk_c, ~r)
|
||||
token = mac(ss, 'lo-auth-v1')
|
||||
in
|
||||
[ !NAuthPk($C, pk_c), Fr(~r) ]
|
||||
--[ NAuthChallenge($S, $C, ~r, token) ]->
|
||||
[ NAuthPending($S, $C, token), Out(c), !NAuthRNG($S, ~r) ]
|
||||
|
||||
rule NAuth_Verify:
|
||||
[ NAuthPending($S, $C, token), In(proof) ]
|
||||
--[ Eq(token, proof), NAuthAccepted($S, $C) ]->
|
||||
[ ]
|
||||
|
||||
// NEG 1: IK corruption defeats auth
|
||||
lemma neg_auth_ik_corrupt:
|
||||
"All S C #v.
|
||||
NAuthAccepted(S, C) @v
|
||||
==> not (Ex #j. NAuthCorruptIK(C) @j)"
|
||||
|
||||
// NEG 2: RNG corruption defeats auth
|
||||
lemma neg_auth_rng_corrupt:
|
||||
"All S C r token #ch #v.
|
||||
NAuthChallenge(S, C, r, token) @ch &
|
||||
NAuthAccepted(S, C) @v
|
||||
==> not (Ex #j. NAuthCorruptRNG(S, r) @j)"
|
||||
|
||||
|
||||
/* =========== NEG 3-4: LO-Ratchet FS boundaries =========== */
|
||||
|
||||
restriction NRatchetOnce:
|
||||
"All #i #j. NRInit() @i & NRInit() @j ==> #i = #j"
|
||||
|
||||
rule NR_Init:
|
||||
[ Fr(~ek) ] --[ NRInit() ]-> [ NR1(~ek, 'nil') ]
|
||||
|
||||
rule NR_Recv1:
|
||||
[ NR1(ek_r, prev), Fr(~ek) ] --[ NRRecv(~ek) ]-> [ NR2(~ek, ek_r) ]
|
||||
|
||||
rule NR_Adv1:
|
||||
[ NR2(ek_r, prev) ] --> [ NR3(ek_r, prev) ]
|
||||
|
||||
rule NR_Recv2:
|
||||
[ NR3(ek_r, prev), Fr(~ek) ] --[ NRRecv(~ek) ]-> [ NR4(~ek, ek_r) ]
|
||||
|
||||
rule NR_Corrupt:
|
||||
[ NR1(ek, p) ] --[ NRCorrupt(), NRRevealed(ek) ]-> [ ]
|
||||
|
||||
rule NR_Corrupt2:
|
||||
[ NR2(ek, p) ] --[ NRCorrupt(), NRRevealed(ek), NRRevealed(p) ]-> [ ]
|
||||
|
||||
rule NR_Corrupt3:
|
||||
[ NR3(ek, p) ] --[ NRCorrupt(), NRRevealed(ek), NRRevealed(p) ]-> [ ]
|
||||
|
||||
rule NR_Corrupt4:
|
||||
[ NR4(ek, p) ] --[ NRCorrupt(), NRRevealed(ek), NRRevealed(p) ]-> [ ]
|
||||
|
||||
// NEG 3: Corrupt with 0 subsequent recvs → current ek revealed
|
||||
lemma neg_ratchet_no_fs_0step:
|
||||
"All ek #r #c.
|
||||
NRRecv(ek) @r & NRCorrupt() @c & r < c
|
||||
==> not (Ex #rev. NRRevealed(ek) @rev)"
|
||||
|
||||
// NEG 4: Same as recv_fs_1step — 1 subsequent recv, ek still in prev
|
||||
lemma neg_ratchet_recv_1step:
|
||||
"All ek ek2 #i #j #c.
|
||||
NRRecv(ek) @i & NRRecv(ek2) @j & NRCorrupt() @c &
|
||||
i < j & j < c
|
||||
==> not (Ex #r. NRRevealed(ek) @r)"
|
||||
|
||||
|
||||
/* =========== NEG 5: LO-Call rk+RNG corruption =========== */
|
||||
|
||||
restriction NCallNeq:
|
||||
"All I R rk #i. NCallSetup(I, R, rk) @i ==> not (I = R)"
|
||||
|
||||
rule NCall_Setup:
|
||||
[ Fr(~rk) ] --[ NCallSetup($I, $R, ~rk) ]-> [ !NCallRK($I, $R, ~rk) ]
|
||||
|
||||
rule NCall_CorruptRK:
|
||||
[ !NCallRK($I, $R, rk) ] --[ NCallCorruptRK($I, $R) ]-> [ Out(rk) ]
|
||||
|
||||
rule NCall_CorruptRNG:
|
||||
[ !NCallRNG($P, r) ] --[ NCallCorruptRNG($P, r) ]-> [ Out(r) ]
|
||||
|
||||
// Step 1: initiator generates pk_eph (public), sk_eph
|
||||
// Step 2: peer encapsulates to pk_eph with randomness r_eph
|
||||
// Corrupt(RNG, initiator, t1) reveals sk_eph
|
||||
// Corrupt(RNG, peer, t2) reveals r_eph
|
||||
rule NCall_Derive:
|
||||
let pk_eph = kem_pk(~sk_eph)
|
||||
ss = kem_ss(pk_eph, ~r_eph)
|
||||
ka = kdf_call_a(rk, ss, ~cid)
|
||||
in
|
||||
[ !NCallRK($I, $R, rk), Fr(~sk_eph), Fr(~r_eph), Fr(~cid) ]
|
||||
--[ NCallDerived($R, ka) ]->
|
||||
[ Out(pk_eph),
|
||||
Out(~cid),
|
||||
Out(kem_c(pk_eph, ~r_eph)),
|
||||
!NCallRNG($I, ~sk_eph),
|
||||
!NCallRNG($R, ~r_eph) ]
|
||||
|
||||
// NEG 5: Both rk and ephemeral RNG corrupted → call key derivable
|
||||
lemma neg_call_rk_plus_rng:
|
||||
"All R ka #d.
|
||||
NCallDerived(R, ka) @d
|
||||
==> not (Ex #k. K(ka) @k)"
|
||||
|
||||
|
||||
/* =========== NEG 6: LO-Stream key corruption =========== */
|
||||
|
||||
functions: nonce_n/2, aad_n/2
|
||||
|
||||
rule NS_Init:
|
||||
[ Fr(~key), Fr(~bn) ]
|
||||
--[ NSInit(~key, ~bn) ]->
|
||||
[ !NSSP(~key, ~bn), NSEnc(~key, ~bn, '0'), Out(~bn) ]
|
||||
|
||||
rule NS_CorruptKey:
|
||||
[ !NSSP(key, bn) ] --[ NSCorrupt(key) ]-> [ Out(key) ]
|
||||
|
||||
rule NS_Enc:
|
||||
let n = nonce_n(bn, idx)
|
||||
aad = aad_n(bn, idx)
|
||||
c = aead_enc(key, n, ~m, aad)
|
||||
in
|
||||
[ NSEnc(key, bn, idx), Fr(~m) ]
|
||||
--[ NSEncrypted(bn, idx, c) ]->
|
||||
[ Out(<c, idx>) ]
|
||||
|
||||
// Adversary decrypts with corrupted key
|
||||
rule NS_Dec_Adversary:
|
||||
let n = nonce_n(bn, idx)
|
||||
aad = aad_n(bn, idx)
|
||||
in
|
||||
[ !NSSP(key, bn), In(<c, idx>) ]
|
||||
--[ Eq(aead_verify(key, n, aad, c), accept()),
|
||||
NSDecrypted(bn, idx) ]->
|
||||
[ ]
|
||||
|
||||
// NEG 6: Key corruption → adversary can get ciphertext accepted
|
||||
// (trivially true since adversary holds key and can replay honest ciphertexts)
|
||||
lemma neg_stream_key_corrupt:
|
||||
"All bn idx #d.
|
||||
NSDecrypted(bn, idx) @d
|
||||
==> not (Ex #j key. NSCorrupt(key) @j)"
|
||||
|
||||
|
||||
/* =========== NEG 7: Anti-reflection without invariant (h) =========== */
|
||||
|
||||
rule NRefl_Setup:
|
||||
[ Fr(~mk), Fr(~n) ]
|
||||
--[ NReflSetup($A, $B, ~mk) ]->
|
||||
[ !NReflKey($A, $B, ~mk), NReflSend($A, $B, ~mk, ~n) ]
|
||||
|
||||
// NOTE: No Neq restriction — self-sessions allowed!
|
||||
|
||||
rule NRefl_Send:
|
||||
let aad = <'lo-dm-v1', $A, $B, ~hdr>
|
||||
c = aead_enc(mk, n, ~m, aad)
|
||||
in
|
||||
[ NReflSend($A, $B, mk, n), Fr(~m), Fr(~hdr) ]
|
||||
--[ NReflSent($A, $B, c) ]->
|
||||
[ Out(<c, ~hdr, n>) ]
|
||||
|
||||
rule NRefl_RecvReflected:
|
||||
let aad = <'lo-dm-v1', $B, $A, header>
|
||||
in
|
||||
[ !NReflKey($A, $B, mk), In(<c, header, n>) ]
|
||||
--[ Eq(aead_verify(mk, n, aad, c), accept()),
|
||||
NReflAccepted($A, $B, c) ]->
|
||||
[ ]
|
||||
|
||||
// NEG 7: Without invariant (h), self-session allows reflection
|
||||
// A sends to A, reflected back — AAD matches because fp order is same
|
||||
lemma neg_reflect_self_session:
|
||||
"All A c #i #j.
|
||||
NReflSent(A, A, c) @i &
|
||||
NReflAccepted(A, A, c) @j
|
||||
==> F"
|
||||
|
||||
|
||||
/* =========================================================
|
||||
* 8. LO-KEX: OPK-absent weakens secrecy threshold (2-key)
|
||||
* ========================================================= */
|
||||
|
||||
// KEX without OPK: IKM = ss_IK ‖ ss_SPK only.
|
||||
// Corruption of IK + SPK alone suffices to derive rk.
|
||||
|
||||
// Reuse KEX IK generation (produces !KEXLtk and !KEXPk for NKEX2 rules)
|
||||
rule NKEX2_GenIK:
|
||||
[ Fr(~sk) ] --[ NKEX2_GenIK($P, ~sk) ]->
|
||||
[ !KEXLtk($P, ~sk), !KEXPk($P, pk(~sk)), Out(pk(~sk)) ]
|
||||
|
||||
rule NKEX2_Publish:
|
||||
let pk_SPK = pk(~sk_SPK)
|
||||
pk_IK = pk(~sk_IK)
|
||||
sig_SPK = sign(<'lo-spk-sig-v1', pk_SPK>, ~sk_IK)
|
||||
in
|
||||
[ !KEXLtk($B, ~sk_IK), Fr(~sk_SPK), Fr(~id_SPK) ]
|
||||
--[ NKEX2_Publish($B, ~id_SPK) ]->
|
||||
[ !NKEX2_SPK_Secret($B, ~id_SPK, ~sk_SPK),
|
||||
!NKEX2_SPK_Pk($B, ~id_SPK, pk_SPK),
|
||||
Out(<pk_IK, pk_SPK, ~id_SPK, sig_SPK>) ]
|
||||
|
||||
rule NKEX2_CorruptSPK:
|
||||
[ !NKEX2_SPK_Secret($P, id, sk) ] --[ NKEX2_CorruptSPK($P, id) ]-> [ Out(sk) ]
|
||||
|
||||
rule NKEX2_Alice_NoOPK:
|
||||
let pk_IK_A = pk(~sk_IK_A)
|
||||
c_IK = aenc(~r_IK, pk_IK_B)
|
||||
c_SPK = aenc(~r_SPK, pk_SPK_B)
|
||||
ikm = <~r_IK, ~r_SPK>
|
||||
rk = h(<ikm, 'rk'>)
|
||||
in
|
||||
[ !KEXLtk($A, ~sk_IK_A), !KEXPk($B, pk_IK_B),
|
||||
!NKEX2_SPK_Pk($B, id_SPK, pk_SPK_B),
|
||||
In(sig_SPK), Fr(~r_IK), Fr(~r_SPK) ]
|
||||
--[ Eq(verify(sig_SPK, <'lo-spk-sig-v1', pk_SPK_B>, pk_IK_B), true),
|
||||
NKEX2_Init($A, $B, rk, id_SPK) ]->
|
||||
[ Out(<c_IK, c_SPK>) ]
|
||||
|
||||
rule NKEX2_CorruptIK:
|
||||
[ !KEXLtk($P, sk) ] --[ NKEX2_CorruptIK($P) ]-> [ Out(sk) ]
|
||||
|
||||
// NEG 8: OPK-absent — IK + SPK corruption alone → adversary derives rk
|
||||
lemma neg_kex_no_opk:
|
||||
"All A B rk id_S #i.
|
||||
NKEX2_Init(A, B, rk, id_S) @i
|
||||
==> not (Ex #k. K(rk) @k)"
|
||||
|
||||
|
||||
/* =========================================================
|
||||
* 9. Ratchet: Duplicate message without recv_seen
|
||||
* ========================================================= */
|
||||
|
||||
// Minimal ratchet with message counter but NO duplicate detection.
|
||||
// An adversary replays a valid ciphertext → accepted twice.
|
||||
|
||||
rule NRDup_Init:
|
||||
[ Fr(~ek) ] --[ NRDupInit() ]-> [ !NRDupKey(~ek) ]
|
||||
|
||||
restriction NRDupOnce:
|
||||
"All #i #j. NRDupInit() @i & NRDupInit() @j ==> #i = #j"
|
||||
|
||||
functions: kdf_msg/2, nonce_dup/1
|
||||
|
||||
rule NRDup_Enc:
|
||||
let mk = kdf_msg(ek, ~ctr)
|
||||
n = nonce_dup(~ctr)
|
||||
c = aead_enc(mk, n, ~m, 'aad')
|
||||
in
|
||||
[ !NRDupKey(ek), Fr(~ctr), Fr(~m) ]
|
||||
--[ NRDupSent(~ctr, c) ]->
|
||||
[ Out(<c, ~ctr>) ]
|
||||
|
||||
// Decrypt WITHOUT recv_seen — always accepts if AEAD passes
|
||||
rule NRDup_Dec:
|
||||
let mk = kdf_msg(ek, ctr)
|
||||
n = nonce_dup(ctr)
|
||||
in
|
||||
[ !NRDupKey(ek), In(<c, ctr>) ]
|
||||
--[ Eq(aead_verify(mk, n, 'aad', c), accept()),
|
||||
NRDupAccepted(ctr, c) ]->
|
||||
[ ]
|
||||
|
||||
// NEG 9: Without recv_seen, same ciphertext accepted twice
|
||||
lemma neg_ratchet_duplicate:
|
||||
"All ctr c #i #j.
|
||||
NRDupAccepted(ctr, c) @i &
|
||||
NRDupAccepted(ctr, c) @j
|
||||
==> #i = #j"
|
||||
|
||||
|
||||
/* =========================================================
|
||||
* 10. Call: Self-session collapses role assignment
|
||||
* ========================================================= */
|
||||
|
||||
// Without the local_fp != remote_fp guard, both parties get the
|
||||
// same role — send/recv key separation collapses.
|
||||
|
||||
rule NCallSelf_Setup:
|
||||
[ Fr(~rk) ] --[ NCallSelfSetup($A, $A, ~rk) ]->
|
||||
[ !NCallSelfRK($A, $A, ~rk) ]
|
||||
|
||||
rule NCallSelf_Derive:
|
||||
let ss = kem_ss(kem_pk(~sk_eph), ~r_eph)
|
||||
// With fp_lo = fp_hi (same party), canonical ordering produces
|
||||
// identical role assignment for both sides
|
||||
ka = kdf_call_a(rk, ss, ~cid)
|
||||
in
|
||||
[ !NCallSelfRK($A, $A, rk), Fr(~sk_eph), Fr(~r_eph), Fr(~cid) ]
|
||||
--[ NCallSelfDerived($A, ka) ]->
|
||||
[ Out(kem_pk(~sk_eph)), Out(~cid), Out(kem_c(kem_pk(~sk_eph), ~r_eph)) ]
|
||||
|
||||
// NEG 10: Self-session is reachable (no guard prevents it)
|
||||
// This confirms the guard is necessary — without it, A initiates a call with itself
|
||||
lemma neg_call_self_session:
|
||||
exists-trace
|
||||
"Ex A ka #i. NCallSelfDerived(A, ka) @i"
|
||||
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue