Tamarin is a security protocol verification tool used to identify attacks from adversaries that can leak information during the execution of said protocol. Tamarin uses symbolic reasoning and multiset term rewriting to automatically construct proofs that show a protocol is safe against adversaries for an arbitrary amount of instances of said protocol.
Tamarin has a full guide and manual available on their website , which is good introduction to the tool and extensive behavior. This page serves as additional information that would have helped me when I first began utilizing the tool as well, and is meant only to be supplemental to the offical manual.
This tutorial will use the same example that the official manual uses. However, this example will go a lot slower, and, in general, include more details for the less cryptographically minded. If you are already familiar with inference rules and multiset term rewriting, using the official manual or skipping this section is suggested.
A full copy of the protocol written in the section can be found here, ready to run.
First, let's set up the file outline.
We begin by naming the theory we are going to describe in a file named
TamarinExample.spthy
.
Tamarin comments use //
or /* */
, so I use those to show where the protocol itself is written.
theory FirstExample begin
/* */
end
This is the theory we will write:
First, sends an encrypted value, (encrypted with key ). then returns the hash of the value to confirm was correctly received. To begin writing this theory, we first need to write a system to create encryption keys, use them, and reveal them.
Let's begin with the registration of a new key.
theory FirstExample begin
+ builtins: hashing, asymmetric-encryption
+ rule Register_pk:
+ [ Fr(~ltk) ]
+ -->
+ [ !Ltk($A, ~ltk), !Pk($A, pk(~ltk)) ]
end
What do these changes do?
builtins
section.rule
that lets us write a new inference rule about the system.[ LHS ] --> [ RHS ]
is the inference rule itself. The LHS is the assumption we are making about the system, and if satisify the assumption, we can draw the conclusions in RHS.Fr(~ltk)
assumes the generation of a new variable, ~ltk
, which is unused in previous rules. Generating fresh variables can be done anytime, and are abstracted away from concrete values. This means they can represent keys or secrets in a protocol.!Ltk($A, ~ltk)
is a fact in Tamarin. Fact names are determined by the model writer, and represent accessible peices of knowledge that protocol parties or adversaries can obtain. In this case, we name the fact !Ltk
to represent a long-term private key for the protocol's execution and we associate it with the owner, $A
.!
represents persistance - !Ltk
can be assumed again and again, without any changes. If the !
were not present, a new key would be generated everytime the model referenced this fact.$
in $A
indicates publicity. A
is a public value, which can be accessed and seen by any party, adversary or not.!Pk($A, pk(~ltk))
associates $A
with the public value of ~ltk
. The pk( KEY )
is a function defined in the asymmetric encryption library, and represents the public key for other parties to use.Let's look at the next section of the key creation, publishing the public version for others to use and with which others encrypt messages.
theory FirstExample begin
builtins: hashing, asymmetric-encryption
rule Register_pk:
[ Fr(~ltk) ]
-->
[ !Ltk($A, ~ltk), !Pk($A, pk(~ltk)) ]
+ rule Get_pk:
+ [ !Pk(Owner, pubkey) ]
+ -->
+ [ Out(pubkey) ]
end
What about these changes? What do they do?
!Pk(Owner, pubkey)
- we are given the public key fact that associates some Owner
with some pubkey
.pubkey
. The Out
fact is a Tamarin-reserved fact that publishes the given value on an unsecured network, where adversaries and parties can access the given value.Out(value)
's corresponding fact is In(value)
, which any fact can assume, and is listening on the unsecured network for published messages.~ltk
and $A
, and Get_pk does not use ~
or $
. In Tamarin, special characters are considered to be part of the variable, so in Register_pk, ltk
is different from ~ltk
.Let's add the last part of the key - revealing it to everybody after it's not being used anymore.
theory FirstExample begin
builtins: hashing, asymmetric-encryption
rule Register_pk:
[ Fr(~ltk) ]
-->
[ !Ltk($A, ~ltk), !Pk($A, pk(~ltk)) ]
rule Get_pk:
[ !Pk(Owner, pubkey) ]
-->
[ Out(pubkey) ]
+ rule Reveal_Ltk:
+ [ !Ltk(A, ltk) ]
+ --[ LtkReveal(A) ]->
+ [ Out(ltk) ]
end
How do these changes work?
-->
, we use --[ LtkReveal(A) ]->
. This is an Action Fact. Action facts annotate the traces construted by the inference rules, and allow for reasoning over traces in the protocol. This action fact has no impact on the rule, but lets us see in a trace that the key has been revealed.Lemmas in Tamarin describe desired behavior of the model, which can then be proven or counter-examples identified. This is where the action facts start to be utilized. For details on how the proofs in Tamarin actually work, see the official tamarin manual. Right now, we are going to write a toy, sanity lemma for us to visualize how Tamarin works.
theory FirstExample begin
builtins: hashing, asymmetric-encryption
rule Register_pk:
[ Fr(~ltk) ]
-->
[ !Ltk($A, ~ltk), !Pk($A, pk(~ltk)) ]
rule Get_pk:
[ !Pk(Owner, pubkey) ]
-->
[ Out(pubkey) ]
rule Reveal_Ltk:
[ !Ltk(A, ltk) ]
--[ LtkReveal(A) ]->
[ Out(ltk) ]
+ lemma Sanity_Reveal_Occurs:
+ exists-trace
+ "Ex Cli #t. LtkReveal(Cli)@t"
end
How does this lemma work?
exists-trace
indicates that the lemma should search for at least one instance that satisfies the following property. Otherwise, all-traces
can be used, which checks that every possible trace satisfies the property. If neither are specified, all-traces
is defaulted to.#
specifies that t
is a timestamp, and when used, must be proceeded by a #
or @
.tamarin-prover interactive <directory/file>
" and navigate on a web browser to "localhost:3001
". We then select the theory, and can see the different options to begin the proof.
LtkReveal
, and works backwards to find sources that satisfy the trace.
Here, we can see that Tamarin starts with the inference rule with LtkReveal(A)
and searches for a source for !Ltk($A, ~ltk)
.
Since it finds Register_pk
as the source, it attempts to find sources for Register_pk
.
However, since Register_pk
only requires a fresh nonce, and those do not need sources, it has completed it's search, and returns the successful trace.
Let's move on to modeling the client interaction portion of the protocol, starting with the first message:
theory FirstExample begin
builtins: hashing, asymmetric-encryption
rule Register_pk:
[ Fr(~ltk) ]
-->
[ !Ltk($A, ~ltk), !Pk($A, pk(~ltk)) ]
rule Get_pk:
[ !Pk(Owner, pubkey) ]
-->
[ Out(pubkey) ]
rule Reveal_Ltk:
[ !Ltk(A, ltk) ]
--[ LtkReveal(A) ]->
[ Out(ltk) ]
+ rule C_Step_1:
+ [ Fr(~k)
+ , !Pk($S, pks)
+ ]
+ -->
+ [ Client_1($S, ~k)
+ , Out(aenc(~k, pks) )
+ ]
lemma Sanity_Reveal_Occurs:
exists-trace
"Ex Cli #t. LtkReveal(Cli)@t"
end
Out(aenc(~k, pks))
to encrypt the message (using a fucntion from asymmetric-encryption), and output on the network.Client_1($S, ~k)
. This fact let's us "save" the information for another inference rule. Since there is no !
, it can only be consumed once.Let's add the second message of the protocol:
theory FirstExample begin
builtins: hashing, asymmetric-encryption
rule Register_pk:
[ Fr(~ltk) ]
-->
[ !Ltk($A, ~ltk), !Pk($A, pk(~ltk)) ]
rule Get_pk:
[ !Pk(Owner, pubkey) ]
-->
[ Out(pubkey) ]
rule Reveal_Ltk:
[ !Ltk(A, ltk) ]
--[ LtkReveal(A) ]->
[ Out(ltk) ]
rule C_Step_1:
[ Fr(~k)
, !Pk($S, pks)
]
-->
[ Client_1($S, ~k)
, Out(aenc(~k, pks) )
]
+ rule C_Step_2:
+ [ Client_1(S, k)
+ , In( h(k) )
+ ]
+ --[ SessKeyC(S, k) ]->
+ []
lemma Sanity_Reveal_Occurs:
exists-trace
"Ex Cli #t. LtkReveal(Cli)@t"
end
Client_1($S, ~k)
. When this inference rule is used during a proof, only one occurance of the Client_1
fact can happen in a trace, because it is not persistant.In(h(k))
.SessKeyC(S,k)
so we can reason over the client sending the message in future lemmas.To finish this model off, we just need to model the server that handles incoming requests and outputs the hash.
theory FirstExample begin
builtins: hashing, asymmetric-encryption
rule Register_pk:
[ Fr(~ltk) ]
-->
[ !Ltk($A, ~ltk), !Pk($A, pk(~ltk)) ]
rule Get_pk:
[ !Pk(Owner, pubkey) ]
-->
[ Out(pubkey) ]
rule Reveal_Ltk:
[ !Ltk(A, ltk) ]
--[ LtkReveal(A) ]->
[ Out(ltk) ]
rule C_Step_1:
[ Fr(~k)
, !Pk($S, pks)
]
-->
[ Client_1($S, ~k)
, Out(aenc(~k, pks) )
]
rule C_Step_2:
[ Client_1(S, k)
, In( h(k) )
]
--[ SessKeyC(S, k) ]->
[]
+ rule S_Step_1:
+ [ !Ltk($S, ~ltkS)
+ , In( request )
+ ]
+ --[ AnswerRequest($S, adec(request, ~ltkS)) ]->
+ [ Out( h(adec(request, ~ltkS)) ) ]
lemma Sanity_Reveal_Occurs:
exists-trace
"Ex Cli #t. LtkReveal(Cli)@t"
end
!Ltk($S, ~ltkS)
, and recieves an encrypted request from the network with In(request)
.Out( h(adec(request, ~ltkS)) )
. Because the input was encrypted, this works only if the key that encrypted the message corresponds to the private decryption key. Tamarin's library for asymmetric-encryption uses function and equations to make sure these values on comparable. (See section on functions).AnswerRequest($S, adec(request, ~ltkS))
annotates that the server is responding to request
.Let's take a look at some of the security properties for the protocol. In brief, we want to write properties for:
Let's go over each of these step-by-step. Note: when the formal FOL is written, timestamps are ignored to avoid use of temporal logic, which would likely be confusing to include here.
theory FirstExample begin
builtins: hashing, asymmetric-encryption
rule Register_pk:
[ Fr(~ltk) ] --> [ !Ltk($A, ~ltk), !Pk($A, pk(~ltk)) ]
rule Get_pk:
[ !Pk(Owner, pubkey) ] --> [ Out(pubkey) ]
rule Reveal_Ltk:
[ !Ltk(A, ltk) ] --[ LtkReveal(A) ]-> [ Out(ltk) ]
rule C_Step_1:
[ Fr(~k), !Pk($S, pks)] --> [ Client_1($S, ~k), Out(aenc(~k, pks) )]
rule C_Step_2:
[ Client_1(S, k), In( h(k) )] --[ SessKeyC(S, k) ]-> []
rule S_Step_1:
[ !Ltk($S, ~ltkS), In( request )] --[ AnswerRequest($S, adec(request, ~ltkS)) ]-> [ Out( h(adec(request, ~ltkS)) ) ]
lemma Sanity_Reveal_Occurs: exists-trace "Ex Cli #t. LtkReveal(Cli)@t"
+ lemma Client_session_key_secrecy:
+ all-traces
+ "not ( Ex Serv key #i #j.
+ SessKeyC(Serv, key)@i
+ & K(key)@j
+ & not( Ex #r. LtkReveal(Serv)@r )
+ )"
end
Ex
. If timestamps are ignored, it is easy to write the lemmas as FOL, and then translate them into Tamarin, before adding timestamps and time restrictions back in.
theory FirstExample begin
builtins: hashing, asymmetric-encryption
rule Register_pk:
[ Fr(~ltk) ] --> [ !Ltk($A, ~ltk), !Pk($A, pk(~ltk)) ]
rule Get_pk:
[ !Pk(Owner, pubkey) ] --> [ Out(pubkey) ]
rule Reveal_Ltk:
[ !Ltk(A, ltk) ] --[ LtkReveal(A) ]-> [ Out(ltk) ]
rule C_Step_1:
[ Fr(~k), !Pk($S, pks)] --> [ Client_1($S, ~k), Out(aenc(~k, pks) )]
rule C_Step_2:
[ Client_1(S, k), In( h(k) )] --[ SessKeyC(S, k) ]-> []
rule S_Step_1:
[ !Ltk($S, ~ltkS), In( request )] --[ AnswerRequest($S, adec(request, ~ltkS)) ]-> [ Out( h(adec(request, ~ltkS)) ) ]
lemma Sanity_Reveal_Occurs: exists-trace "Ex Cli #t. LtkReveal(Cli)@t"
lemma Client_session_key_secrecy:
all-traces
"not ( Ex Serv key #i #j.
SessKeyC(Serv, key)@i
& K(key)@j
& not( Ex #r. LtkReveal(Serv)@r )
)"
+ lemma Client_Auth:
+ all-traces
+ "All Serv sec #i. SessKeyC(Serv, sec)@i
+ ==> (
+ (Ex #a. AnswerRequest(Serv, sec)@a)
+ |
+ (Ex #r. LtkReveal(Serv)@r & #r<#i)
+ )"
end
LtkReveal
occurs before the session is initiated with #r<#i
.
theory FirstExample begin
builtins: hashing, asymmetric-encryption
rule Register_pk:
[ Fr(~ltk) ] --> [ !Ltk($A, ~ltk), !Pk($A, pk(~ltk)) ]
rule Get_pk:
[ !Pk(Owner, pubkey) ] --> [ Out(pubkey) ]
rule Reveal_Ltk:
[ !Ltk(A, ltk) ] --[ LtkReveal(A) ]-> [ Out(ltk) ]
rule C_Step_1:
[ Fr(~k), !Pk($S, pks)] --> [ Client_1($S, ~k), Out(aenc(~k, pks) )]
rule C_Step_2:
[ Client_1(S, k), In( h(k) )] --[ SessKeyC(S, k) ]-> []
rule S_Step_1:
[ !Ltk($S, ~ltkS), In( request )] --[ AnswerRequest($S, adec(request, ~ltkS)) ]-> [ Out( h(adec(request, ~ltkS)) ) ]
lemma Sanity_Reveal_Occurs: exists-trace "Ex Cli #t. LtkReveal(Cli)@t"
lemma Client_session_key_secrecy:
all-traces
"not ( Ex Serv key #i #j.
SessKeyC(Serv, key)@i
& K(key)@j
& not( Ex #r. LtkReveal(Serv)@r )
)"
lemma Client_Auth:
all-traces
"All Serv sec #i. SessKeyC(Serv, sec)@i
==> (
(Ex #a. AnswerRequest(Serv, sec)@a)
|
(Ex #r. LtkReveal(Serv)@r & #r<#i)
)"
+ lemma Sanity_Session_Occurs:
+ exists-trace
+ "Ex Serv sec #i. (
+ SessKeyC(Serv, sec)@i
+ & not( Ex #r. LtkReveal(Serv)@r )
+ )"
end
After completing these lemmas, let's take a closer look at the results after we automatically prove all of the lemmas in our file.
This is the result for the last lemma, Sanity_Session_Occurs
:
We can see that for each inference rule, every fact on the left hand side has been sourced back to fresh values.
Additionally, Tamarin uses circular nodes to show adversary intervention, and is automatically enabled during verification.
For more information on the adversary, see section Adversaries.
If you got to this point, that means you have successfully run the Tamarin first example. Congratulations! You are now ready to begin proving the security of your own protocols.
Language guide to come.
Lemma guide to come.
Function guide to come.
Oracle guide to come.
Macro guide to come.