Reading Tamarin Prover Output: A Visual Tutorial
Introduction
Visual protocol verification tutorial
Read Tamarin output like an attack narrative
This post turns Tamarin's proof trees and dependency graphs into a guided walkthrough: what each box means, why arrows matter, and how a counterexample becomes a concrete security story.
Tamarin summary
executableverifiedsecrecyverified-
Generate_Keypair -
Alice_Send -
Bob_Receive
Tamarin Prover is a tool for formally verifying security protocols. You give it a model of a protocol and properties you want to verify, and it either proves the property holds for all possible executions or finds a counterexample -- a concrete attack.
Tamarin's interactive UI produces dependency graphs and proof trees that can be hard to interpret at first. This tutorial walks through them using a simple key-exchange protocol, building up from the basics.
The full interactive version of this tutorial (with step-through trace explorers, expandable proof trees, and live protocol comparison) is available as a standalone interactive page. The companion Tamarin theory files are available for download:
-
tutorial.spthy-- working theory -
tutorial_broken.spthy-- broken version (for counterexample demo)
The Protocol
We use a simple encrypted message exchange:
Message flow
Properties
executableThere is at least one complete run where Alice sends and Bob receives.
secrecyAcross all traces, the adversary never learns the secret.
-
Bob generates a keypair
(sk, pk(sk))and publishes the public key -
Alice picks a fresh secret, encrypts it with Bob's public key, sends the ciphertext
-
Bob decrypts with his private key
Properties we verify:
-
executable: Can this protocol actually run? (sanity check) -
secrecy: Can the adversary learn the secret? (security property)
Core Concepts
Facts
Facts are the state of the world. They sit on a multiset (a bag that can hold duplicates). Rules consume and produce facts. There are three kinds:
| Kind | Syntax | Behavior |
|---|---|---|
| Linear | Fact(x, y) | Consumed when a rule reads it |
| Persistent | !Fact(x, y) | Never consumed; any number of rules can read it |
| Fresh | Fr(~x) | Built-in; generates a globally unique value |
Rules
A rule is a state transition: "if these facts exist, consume them, log some actions, and produce new facts."
Click through a rule
[ Fr(~secret), !Pk($B, pkB) ]Premises
These facts must be available before the rule can fire. Fresh facts create new names; persistent facts can be read repeatedly.
--[ Secret(~secret), Sent($A, $B, ~secret) ]->Actions
Action facts are timeline events. Lemmas talk about these events using timepoints such as #i and #j.
[ Out(aenc(~secret, pkB)) ]Conclusions
Conclusions are produced by the rule. Here Alice emits a ciphertext onto the network, where the adversary can see it.
rule Alice_Send: [ Fr(~secret), !Pk($B, pkB) ] // PREMISES --[ Secret(~secret), Sent($A, $B, ~secret) ]-> // ACTIONS [ Out(aenc(~secret, pkB)) ] // CONCLUSIONS
Premises -- facts that must exist for the rule to fire.
Actions -- events logged on the global timeline (what lemmas reason about).
Conclusions -- new facts produced.
The Timeline
Tamarin maintains a global timeline of action facts. Lemmas are statements about this timeline: "for all traces, if X happens then Y must also happen."
The Theory
Here is the complete Tamarin theory:
theory Tutorial begin builtins: asymmetric-encryption rule Generate_Keypair: [ Fr(~sk) ] --[ KeyGen($B, ~sk) ]-> [ !Sk($B, ~sk), !Pk($B, pk(~sk)), Out(pk(~sk)) ] rule Alice_Send: [ Fr(~secret), !Pk($B, pkB) ] --[ Secret(~secret), Sent($A, $B, ~secret) ]-> [ Out(aenc(~secret, pkB)) ] rule Bob_Receive: [ !Sk($B, skB), In(aenc(secret, pk(skB))) ] --[ Received($B, secret) ]-> [ ] lemma executable: exists-trace "Ex A B s #i #j. Sent(A, B, s) @ i & Received(B, s) @ j & i < j" lemma secrecy: all-traces "All s #i. Secret(s) @ i ==> not(Ex #j. K(s) @ j)" end
Reading Attack Traces
When you click an exists-trace lemma (like executable), Tamarin finds a
concrete execution that satisfies the formula and displays it as a
dependency graph.
How to read it:
-
Time flows top to bottom. Earlier events are higher.
-
Boxes are rule instances. Each box is one specific firing of a rule with concrete values bound to variables.
-
Arrows are fact dependencies. An arrow from Box A to Box B means "A produced a fact that B consumed."
-
Solid arrows = linear fact flow (consumed on use).
-
Dashed arrows = persistent fact flow (read but not consumed).
-
Red arrows = network flow (
Out->In, through the adversary).
The executable trace shows three boxes in sequence:
Generate_Keypair -> Alice_Send -> Bob_Receive, connected by
persistent fact arrows (!Pk, !Sk) and a network arrow
(Out(aenc(...)) -> In(aenc(...))).
Fr(~sk) -> !Sk, !Pk, Out(pk(~sk))Fr(~secret), !Pk -> Out(aenc(...))!Sk, In(aenc(...)) -> Received!Sk and !Pk are persistent facts, so later rules can read them without consuming them.Secret and Sent, and puts a ciphertext on the network.Received. The executable trace is complete.Reading Proof Trees
When you click an all-traces lemma that is VERIFIED (like secrecy),
Tamarin shows a proof tree -- backward reasoning from the goal.
Tamarin works backwards:
-
Assume the negation: "Suppose the adversary knows the secret."
-
Case split: "What rule could have produced
K(~secret)?" -
For each case, ask what preconditions are needed.
-
Each branch either hits a contradiction (green leaf, closed) or leads to a complete attack (red leaf, property violated).
For secrecy, every branch closes:
-
The adversary would need Bob's private key
~skto decrypt -
~skis in!Sk(persistent) but never in anyOut()fact -
The adversary only learns values on the network
-
Contradiction on every branch. VERIFIED.
Expandable proof tree
Goal: adversary learns secret?
Case: learn from network
Case: decrypt ciphertext
Case: synthesize secret
When every branch closes, Tamarin marks the all-traces lemma as verified.
Introducing a Bug
What if Bob accidentally leaks the secret? Add this rule:
rule Bob_Receive:
[ !Sk($B, skB), In(aenc(secret, pk(skB))) ]
--[ Received($B, secret) ]->
[ ]Bob records receipt but does not emit the plaintext. The adversary still only sees ciphertext.
rule Bob_Receive_And_Leak:
[ !Sk($B, skB), In(aenc(secret, pk(skB))) ]
--[ Received($B, secret) ]->
[ Out(secret) ]
The single extra conclusion changes the proof: Out(secret) makes the plaintext available to adversary knowledge K(secret).
rule Bob_Receive_And_Leak: [ !Sk($B, skB), In(aenc(secret, pk(skB))) ] --[ Received($B, secret) ]-> [ Out(secret) ] // <-- secret in cleartext!
Now secrecy is FALSIFIED. The counterexample trace shows the same
structure as an attack trace, but with an adversary derivation at the bottom:
-
Generate_Keypair->Alice_Send->Bob_Receive_And_Leak -
Out(secret)puts the plaintext on the network -
K(~secret)-- adversary learns it
The vulnerability is visible immediately: no MFA gate, no encryption, just
Out(secret) in the clear.
publish pk(sk)Out(aenc(secret, pk(sk)))Out(secret)K(secret)The counterexample is not just a failed proof. It is a readable attack path: the exact rule firing that turns protected data into adversary knowledge.
The Adversary
Tamarin uses the Dolev-Yao adversary model:
-
Sees all network messages (
Outfacts) -
Can inject any message it can construct (
Infacts) -
Can decompose, encrypt, decrypt (only with known keys)
-
Cannot break cryptographic primitives
In graphs, adversary derivation nodes appear as small boxes labeled coerce,
adec, pair, etc. K(x) is the terminal node: "adversary knows x."
Variable Conventions
| Prefix | Meaning | Example |
|---|---|---|
~x | Fresh (unique, secret) | ~sk, ~secret |
$X | Public name (known to everyone) | $A, $B |
x | Arbitrary term | secret, pkB |
From Here to IAMReach
The IAMReach project uses exactly the same Tamarin structure to model AWS IAM role chains:
| Tutorial concept | IAMReach equivalent |
|---|---|
Generate_Keypair | LegitLambdaInit (entity gets role credentials) |
Alice_Send | Assume_LambdaExec_to_SharedProcessor (sts:AssumeRole) |
!Pk($B, pk(~sk)) | !ExternalId('VendorA', 'ProdIngestion', extId) |
MFAAuth(entity, token) | MFA gate on a trust relationship |
K(~secret) | Adversary reaches a target role |
| Counterexample trace | The IAM role chain violating a security property |
| Proof tree | Exhaustive proof that isolation holds |
Try It
# Install Tamarin brew install tamarin-prover # Automated proofs tamarin-prover tutorial.spthy --prove # Interactive UI (recommended) tamarin-prover interactive tutorial.spthy # Opens at http://127.0.0.1:3001
Or explore the interactive tutorial with step-through traces, expandable proof trees, and a live protocol comparison toggle.
