Hi, I'm a bachelor's student doing a thesis on challenge response protocols with Tamarin Prover.
One of the protocols I'm trying to implement is a clone detection mechanism where the verifier can detect if the prover's secret key has been cloned.
I followed the manual, which dictates that I should use injective facts for loops that consume and reproduce the same fact. I also used heuristics for stateful protocols, as the verifier has two states (one in which it awaits the challenge and the other in which it must perform the verify).
theory 3_2_CD_Secure_Challenge
begin
builtins: signing, natural-numbers
heuristic: I
restriction Equality:
"All x y #i. Eq(x, y) @i ==> x = y"
restriction LessThan:
"All x y #i. LessThan(x, y) @i ==> x << y"
restriction GreaterEqualThan:
"All x y #i. GreaterEqualThan(x, y) @i ==> not(x << y)"
/*
* Assumptions:
* - The protocol is based on 2_3_csa_per_verifier_keying.spthy
* - For each (prover, verifier) pair, the prover and the verifier each maintain
* a counter initialized to the same value
* - Challenges are secure, meaning the sender and recipient's identities cannot be
* spoofed, and the challenge is confidential.
*
* Init:
* - Each prover generates its own key pair (sk, pk)
* - The public key is shared with the verifier
* - Prover and verifier initialize their counters counterP and counterV
*
* Protocol:
* - The verifier generates a challenge ch and sends it to the prover
* - The prover increments its counter, and computes the response
* sig = sign(<counterP, ch>, sk)
* - The prover sends sig and its incremented counter to the verifier
* - The verifier verifies the signature verify(sig, <counterP, ch>, pk):
* - If true, it continues
* - Otherwise, it rejects it
* - It checks that counterP <= counterV:
* - If true, it authenticates the prover and updates its counter
* counterV = counterP
* - Otherwise, it rejects it
*
* Security objective:
* - Assuming the adversary used the cloned key to authenticate, if a cloned
* prover tries to authenticate, the verifier rejects the authentication
*
*
*/
// The secure channel is used only for the challenge.
rule secure_channel_send:
[ SecSend(A,B,m) ]
--[ SecChan_Out(A,B,m) ]->
[ Sec(A,B,m) ]
rule secure_channel_receive:
[ Sec(A,B,m) ]
--[ SecChan_In(A,B,m) ]->
[ SecRecv(A,B,m) ]
// Prover registration with the verifier:
// - Generation of a key pair
// - Sharing of the public key
// - Initialization of counters
rule Init:
[ Fr(~sk), Fr(~pid), Fr(~vid) ]
--[ Registers($P, $V),
InitP(~pid, $P, $V),
InitV(~vid, $V, $P) ]->
[ !ProverSK($P, $V, ~sk),
!VerifierPK($V, $P, pk(~sk)),
StateP(~pid, $P, $V, %1),
StateV(~vid, $V, $P, %1, 'Challenge', 'null'),
Out(pk(~sk)) ]
// Each prover can register with a verifier only once.
restriction RegistersOnce:
"All p v #i #j. Registers(p, v) @i & Registers(p, v) @j ==> #i = #j"
// Reveals the secret key of a prover (equivalent to cloning).
rule RevealSK:
[ !ProverSK($P, $V, ~sk) ]
--[ Reveal($P, $V) ]->
[ Out(~sk) ]
// A prover can be cloned at most once.
restriction RevealOnce:
"All p v #i #j. Reveal(p, v) @i & Reveal(p, v) @j ==> #i = #j"
// The verifier sends a challenge to the prover.
rule Challenge:
[ StateV(~vid, $V, $P, %n_v, 'Challenge', 'null'),
!VerifierPK($V, $P, pk),
Fr(~ch) ]
--[ Challenge($V, $P, ~ch),
LoopV(~vid, $V, $P) ]->
[ StateV(~vid, $V, $P, %n_v, 'Verify', ~ch),
SecSend($V, $P, ~ch) ]
// The prover receives the challenge, signs it, increments its counter, and sends
// (signature, counter) to the verifier.
rule Response:
let %next_n = %n %+ %1
n_ch = <%next_n, ch>
sig = sign(n_ch, ~sk)
in
[ SecRecv($V, $P, ch),
StateP(~pid, $P, $V, %n),
!ProverSK($P, $V, ~sk) ]
--[ Responds($P, $V, n_ch),
LoopP(~pid, $P, $V) ]->
[ StateP(~pid, $P, $V, %next_n),
Out(<$P, $V, n_ch, sig>) ]
// The verifier receives the signature, verifies it, and compares the counters
// successfully.
rule Verify:
let n_ch = <n_p, ch>
in
[ In(<$P, $V, n_ch, sig>),
StateV(~vid, $V, $P, %n_v, 'Verify', ch),
!VerifierPK($V, $P, pk) ]
--[ Verifies($V, $P, n_ch),
Eq(verify(sig, n_ch, pk), true),
LessThan(%n_v, n_p),
Accepts($V, $P, n_ch),
LoopV(~vid, $V, $P) ]->
[ StateV(~vid, $V, $P, n_p, 'Challenge', 'null') ]
// The verifier receives the signature and verifies it successfully, but
// receives a counter greater than or equal to its local counter.
rule Detection:
let n_ch = <n_p, ch>
in
[ In(<$P, $V, n_ch, sig>),
StateV(~vid, $V, $P, %n_v, 'Verify', ch),
!VerifierPK($V, $P, pk) ]
--[ Verifies($V, $P, n_ch),
Eq(verify(sig, n_ch, pk), true),
GreaterEqualThan(%n_v, n_p),
Detects($V, $P, n_ch),
LoopV(~vid, $V, $P) ]->
[ /* The verifier performs some mitigation */ ]
// ------------------------------------LEMMAS------------------------------------
// Each prover loop must have been initialized.
lemma InitBeforeLoopProver [reuse, use_induction]:
"All p v pid #i2.
LoopP(pid, p, v) @i2
==>
(Ex #i1. InitP(pid, p, v) @i1 & #i1 < #i2)"
// Each verifier loop must have been initialized.
lemma InitBeforeLoopVerifier [reuse, use_induction]:
"All p v vid #i2.
LoopV(vid, v, p) @i2
==>
(Ex #i1. InitV(vid, v, p) @i1 & #i1 < #i2)"
// If the verifier accepts a signature from the prover, then:
// - The same prover must have replied to the same challenge and counter
// associated with the signature
// - There must be no further acceptances of that same challenge and counter
lemma InjectiveAgreement:
"All p v n_ch #i2.
Accepts(v, p, n_ch) @i2 &
not(Ex #i. Reveal(p, v) @i)
==>
(Ex #i1. Responds(p, v, n_ch) @i1 & #i1 < #i2) &
not(Ex p_j v_j #j. Accepts(v_j, p_j, n_ch) @j & not(#j = #i2))"
// If the prover has responded to a challenge, then the verifier must have sent
// it.
// True: The challenge can't be forged nor replayed from other sessions
lemma RespondsOnlyIfChallenged:
"All p v n_p ch #i2.
Responds(p, v, <n_p, ch>) @i2
==>
(Ex #i1. Challenge(v, p, ch) @i1 & #i1 < #i2)"
// If there was a detection, then there was a cloning.
lemma CloneDetectionSound:
"All p v n_ch #i2.
Detects(v, p, n_ch) @i2
==>
(Ex #i1. Reveal(p, v) @i1 & #i1 < #i2)"
// After a cloning, it is always possible to detect it.
// False: the adversary chooses not to use the prover's cloned key.
lemma CloneDetectionCompleteness:
"All p v #i1.
Reveal(p, v) @i1
==>
(Ex n_ch #i2. Detects(v, p, n_ch) @i2 & #i1 < #i2)"
lemma count_unique_prover[reuse]:
"All p v pid n_p #i #j.
LoopP(pid, p, v, n_p) @i & LoopP(pid, p, v, n_p) @j
==> #i = #j"
lemma count_unique_verifier[reuse]: // <---- Doesn't terminate
"All p v vid n_v #i #j.
LoopV(vid, v, p, n_v) @i & LoopV(vid, v, p, n_v) @j
==> #i = #j"
end