Reading Tamarin Prover Output: A Visual Tutorial

Formal MethodsTutorialSecurity

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

executableverified
secrecyverified
  1. Generate_Keypair
  2. Alice_Send
  3. 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:

The Protocol

We use a simple encrypted message exchange:

Message flow

AliceNetworkBob
1. pk(sk)
2. aenc(secret, pk(sk))
3. Bob decrypts locally with sk

Properties

VERIFIEDexecutable

There is at least one complete run where Alice sends and Bob receives.

VERIFIEDsecrecy

Across all traces, the adversary never learns the secret.

  1. Bob generates a keypair (sk, pk(sk)) and publishes the public key

  2. Alice picks a fresh secret, encrypts it with Bob's public key, sends the ciphertext

  3. 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:

KindSyntaxBehavior
LinearFact(x, y)Consumed when a rule reads it
Persistent!Fact(x, y)Never consumed; any number of rules can read it
FreshFr(~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(...))).

Generate_KeypairFr(~sk) -> !Sk, !Pk, Out(pk(~sk))
!Pk
Alice_SendFr(~secret), !Pk -> Out(aenc(...))
Out -> In
Bob_Receive!Sk, In(aenc(...)) -> Received
Step 1. Bob creates the long-term key material. !Sk and !Pk are persistent facts, so later rules can read them without consuming them.
Step 2. Alice reads Bob's public key, logs Secret and Sent, and puts a ciphertext on the network.
Step 3. Bob reads his private key, receives the ciphertext, and logs 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:

  1. Assume the negation: "Suppose the adversary knows the secret."

  2. Case split: "What rule could have produced K(~secret)?"

  3. For each case, ask what preconditions are needed.

  4. 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 ~sk to decrypt

  • ~sk is in !Sk (persistent) but never in any Out() 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
Closed: only ciphertext appears on Out; private key never appears on the network.
Case: decrypt ciphertext
Closed: decryption requires sk, but sk is stored only in !Sk and is never output.
Case: synthesize secret
Closed: ~secret is fresh, so the adversary cannot guess or construct it from public terms.

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:

VERIFIED
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.

FALSIFIED
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:

  1. Generate_Keypair -> Alice_Send -> Bob_Receive_And_Leak

  2. Out(secret) puts the plaintext on the network

  3. K(~secret) -- adversary learns it

The vulnerability is visible immediately: no MFA gate, no encryption, just Out(secret) in the clear.

Generate_Keypairpublish pk(sk)
->
Alice_SendOut(aenc(secret, pk(sk)))
->
Bob_Receive_And_LeakOut(secret)
->
AdversaryK(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 (Out facts)

  • Can inject any message it can construct (In facts)

  • 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

PrefixMeaningExample
~xFresh (unique, secret)~sk, ~secret
$XPublic name (known to everyone)$A, $B
xArbitrary termsecret, pkB

From Here to IAMReach

The IAMReach project uses exactly the same Tamarin structure to model AWS IAM role chains:

Tutorial conceptIAMReach equivalent
Generate_KeypairLegitLambdaInit (entity gets role credentials)
Alice_SendAssume_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 traceThe IAM role chain violating a security property
Proof treeExhaustive 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.