Mermaid To Proverif
Trail of BitsOfficialPublished by Trail of Bits
- Skill
- Tool integration
- Security
- CC-BY-SA-4.0
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill automates the creation of ProVerif formal verification models from Mermaid sequence diagrams. It guides users through parsing participants, inventorying cryptographic operations, defining types and functions, and formulating security queries. By following a structured workflow, it ensures that cryptographic protocols are modeled correctly for ProVerif, allowing for the verification of properties such as secrecy, authentication, and forward secrecy.
What it does
- Parses Mermaid sequence diagrams to identify protocol participants and communication channels.
- Maps cryptographic operations like Sign, Verify, DH, and HKDF to ProVerif functions and destructors.
- Generates ProVerif code for types, constants, and algebraic equations.
- Defines security-relevant events for tracking protocol execution.
- Formulates security queries for secrecy, weak authentication, and injective authentication.
- Structures participant processes to mirror the protocol flow and verify security properties.
When to use it
- When you need to formally verify a cryptographic protocol described in a Mermaid diagram.
- When you want to generate a .pv file for ProVerif from a visual protocol representation.
- When you need to prove security properties like secrecy or forward secrecy.
- When you have already generated a protocol diagram using the crypto-protocol-diagram skill.
Advantages
- Reduces manual effort in writing complex ProVerif models.
- Ensures consistent modeling of cryptographic primitives and security queries.
- Provides a clear path to identifying protocol flaws like replay attacks.
- Integrates seamlessly with existing protocol diagramming workflows.
Usage examples
- “Convert my protocol diagram to a ProVerif model.”
- The agent will parse the provided Mermaid sequence diagram and generate a complete .pv file, including all necessary declarations, processes, and security queries.
- “Verify if my handshake protocol provides forward secrecy.”
- The agent will update the ProVerif model to include a ForwardSecrecyTest process and the corresponding secrecy query to check if past session keys remain secure.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/trailofbits/mermaid-to-proverif/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip mermaid-to-proverif.zip -d .agents/skills
Documentation by Trail of Bits
Mermaid to ProVerif
Reads a Mermaid sequenceDiagram describing a cryptographic protocol and
produces a ProVerif model (.pv file) that can be passed directly to the
ProVerif verifier.
Tools used: Read, Write, Grep, Glob.
The typical input is the output of the crypto-protocol-diagram skill — a
Mermaid sequenceDiagram annotated with cryptographic operations (Sign,
Verify, DH, HKDF, Enc, Dec, etc.) and message arrows.
When to Use
User asks to formally verify a cryptographic protocol described as a Mermaid sequenceDiagram
User wants to generate a ProVerif model (.pv file) from a protocol diagram
User wants to prove secrecy, authentication, or forward secrecy properties
Input is the output of the
crypto-protocol-diagramskill
When NOT to Use
No Mermaid sequenceDiagram exists yet — use
crypto-protocol-diagramfirst to generate oneUser wants to verify properties of non-cryptographic systems (state machines, access control)
User wants to run ProVerif on an existing .pv file — just run
proverif model.pvdirectly
Rationalizations to Reject
| Rationalization | Why It's Wrong | Required Action |
|---|---|---|
| "Reachability queries are just busywork" | If events aren't reachable, all other query results are meaningless | Always add reachability queries first as a sanity check |
| "Public channels are fine for all messages" | Private channels for internal state prevent false attacks | Use private channels for intra-process state threading |
| "I'll skip the forward secrecy test" | Ephemeral keys demand forward secrecy verification | Add the ForwardSecrecyTest process whenever the diagram shows ephemeral keys |
| "Unused declarations are harmless" | ProVerif may report spurious results from orphan declarations | Clean up all unused types, functions, and events |
| "The model compiles, so it's correct" | A compiling model can have dead receives, type mismatches, or impossible guards that make queries vacuously true | Validate reachability before trusting any security query |
| "I don't need to check the example first" | The example defines the expected output quality bar | Study examples/simple-handshake/ before working on unfamiliar protocols |
Workflow
ProVerif Model Progress:
- [ ] Step 1: Parse participants and channels
- [ ] Step 2: Inventory cryptographic operations
- [ ] Step 3: Declare types, functions, and equations
- [ ] Step 4: Identify and declare events
- [ ] Step 5: Formulate security queries
- [ ] Step 6: Write participant processes
- [ ] Step 7: Write main process and finalize
- [ ] Step 8: Verify and deliverStep 1: Parse Participants and Channels
From the Mermaid diagram:
Extract every
participantoractordeclaration. Each becomes a ProVerif process.Count message arrows (
->>,-->>,-x,--x). Each distinctA ->> B: labelcreates a communication step on a channel.Decide channel model:
Public channel for any message sent over the network before a secure channel is established (e.g., ClientHello, ephemeral keys, ciphertext to be decrypted by the peer).
Private channel only for internal state threading within a single party process (not for cross-party messages).
Default: declare one shared public channel
cfor all cross-party messages. Add per-flow channels only when two distinct parallel sessions must be independent.
free c: channel.Step 2: Inventory Cryptographic Operations
Walk through every Note over annotation and message label. Build a list of
all distinct operations used. Map each to a ProVerif declaration category:
| Mermaid annotation | ProVerif category |
|---|---|
keygen() → sk, pk | New name (new sk), public key derived via function |
DH(sk_A, pk_B) | DH function or exp with group |
Sign(sk, msg) → σ | Signature function |
Verify(pk, msg, σ) | Equation or destructor |
Enc(key, msg) → ct | Symmetric or asymmetric encryption function |
Dec(key, ct) → msg | Destructor (equation) |
HKDF(ikm, info) → k | PRF/KDF function |
HMAC(key, msg) → tag | MAC function |
H(msg) → digest | Hash function |
Commit(v, r) → C | Commitment function |
Open(C, v, r) | Commitment equation |
Consult references/crypto-to-proverif-mapping.md for exact ProVerif syntax for each.
Step 3: Declare Types, Functions, and Equations
Build the cryptographic preamble in this order:
Types — declare custom types used to distinguish key material:
type key.
type pkey. (* public key *)
type skey. (* secret key *)
type nonce.Constants — for fixed strings used as domain separators or labels:
const msg1_label: bitstring.
const msg2_label: bitstring.
const info_session_key: bitstring.Functions — constructors and destructors. Destructors use inline
reducso that the process aborts on verification or decryption failure:
(* Asymmetric encryption *)
fun aenc(bitstring, pkey): bitstring.
fun adec(bitstring, skey): bitstring
reduc forall m: bitstring, k: skey;
adec(aenc(m, pk(k)), k) = m.
fun pk(skey): pkey.
(* Symmetric encryption / AEAD *)
fun aead_enc(bitstring, key): bitstring.
fun aead_dec(bitstring, key): bitstring
reduc forall m: bitstring, k: key;
aead_dec(aead_enc(m, k), k) = m.
(* Digital signatures — verify returns the message on success, aborts on failure *)
fun sign(bitstring, skey): bitstring.
fun verify(bitstring, bitstring, pkey): bitstring
reduc forall m: bitstring, k: skey;
verify(sign(m, k), m, pk(k)) = m.
(* KDF — first arg is key (from DH), second is bitstring (info/context) *)
fun hkdf(key, bitstring): key.
(* MAC *)
fun mac(bitstring, key): bitstring.
(* Hash *)
fun hash(bitstring): bitstring.
(* DH *)
fun dh(skey, pkey): key.
fun dhpk(skey): pkey.
(* Serialization — ProVerif is strongly typed: pkey cannot appear
* where bitstring is expected. Use these to build signed payloads. *)
fun pkey2bs(pkey): bitstring.
fun concat(bitstring, bitstring): bitstring.Equations — algebraic identities on constructors only (not on destructors, which already have their rewrite rules inline):
equation forall sk_a: skey, sk_b: skey;
dh(sk_a, dhpk(sk_b)) = dh(sk_b, dhpk(sk_a)).Only declare what the diagram actually uses. Do not add functions for operations not present.
Step 4: Identify and Declare Events
Events mark security-relevant moments in the protocol execution. Extract them by identifying:
Begin events (
event beginRole(params)): triggered immediately before a party sends a message that depends on a long-term identity commitment (e.g., right before sending a signed message or a MAC'd message).End events (
event endRole(params)): triggered immediately after a party successfully verifies the peer's identity (e.g., afterVerify(...)or MAC check passes, session key confirmed).Secrecy markers: any key or nonce that should remain unknown to the attacker after the handshake.
event beginI(pkey, pkey). (* pk_I, pk_R — fired before sending the signed message *)
event endI(pkey, pkey, key). (* pk_I, pk_R, session_key — fired after accepting *)
event beginR(pkey, pkey).
event endR(pkey, pkey, key).Parameters should uniquely identify the session: the parties' public keys, plus the session key or a transcript hash.
Step 5: Formulate Security Queries
Write one query per security property. Choose from:
Reachability (always add first — structural sanity check):
Verify that the success events are actually reachable. ProVerif prints
RESULT not event(endR(...)) is false when the event IS reachable, which is
what you want. If it prints is true, the event never fires: the model has a
structural bug (dead receive, type mismatch, impossible guard) and no other
query result should be trusted. Once the model
is validated, comment them out if they slow down the main property checks:
(* Sanity: both endpoints must be reachable — comment out once validated. *)
(*
query pk_i: pkey, pk_r: pkey, k: key; event(endI(pk_i, pk_r, k)).
query pk_i: pkey, pk_r: pkey, k: key; event(endR(pk_i, pk_r, k)).
*)Secrecy (key not derivable by attacker):
Declare a private free name and encrypt it under the session key. The attacker
knowing private_I is equivalent to breaking the session key:
free private_I: bitstring [private].
(* In process, after deriving sk_session: *)
out(c, aead_enc(private_I, sk_session));
(* Query: *)
query attacker(private_I).Weak authentication (if B accepted, A ran at some point with matching params — does not prevent replay):
query pk_i: pkey, pk_r: pkey, k: key;
event(endR(pk_i, pk_r, k)) ==> event(beginI(pk_i, pk_r)).Injective authentication (prevents replay — each B-accept corresponds to a distinct A-run):
query pk_i: pkey, pk_r: pkey, k: key;
inj-event(endR(pk_i, pk_r, k)) ==>
inj-event(beginI(pk_i, pk_r)).Forward secrecy: add a ForwardSecrecyTest process to the main process
that leaks both long-term secret keys to the attacker after the sessions (in
phase 1), then check that a past session key remains secret. Pair it with a free fs_witness: key [private]
declaration and query attacker(fs_witness). See
references/security-properties.md →
Forward Secrecy.
Choose the strongest applicable query for each property. See references/security-properties.md for the full decision tree.
Step 6: Write Participant Processes
Write one let process per participant. Structure each process to mirror the
Mermaid diagram step-by-step, in order.
Template for a two-party protocol:
let Initiator(sk_I: skey, pk_R: pkey) =
(* Step: generate ephemeral key *)
new ek_I: skey;
let epk_I = dhpk(ek_I) in
(* Step: sign and send msg1 — pkey2bs casts pkey to bitstring *)
let sig_I = sign(concat(msg1_label, pkey2bs(epk_I)), sk_I) in
event beginI(pk(sk_I), pk_R);
out(c, (epk_I, sig_I));
(* Step: receive msg2 *)
in(c, (epk_R: pkey, sig_R: bitstring));
(* Step: verify responder signature — destructor aborts on failure *)
let transcript = concat(pkey2bs(epk_I), pkey2bs(epk_R)) in
let _ = verify(sig_R, concat(msg2_label, transcript), pk_R) in
(* Step: derive session key *)
let dh_val = dh(ek_I, epk_R) in
let sk_session = hkdf(dh_val, concat(info_session_key, transcript)) in
event endI(pk(sk_I), pk_R, sk_session);
(* Secrecy witness: encrypt private_I under the session key.
* Declared as: free private_I: bitstring [private].
* The query attacker(private_I) checks the attacker cannot derive it. *)
out(c, aead_enc(private_I, sk_session)).Rules for writing processes:
Each
A ->> B: msg_contentsin the diagram becomes:out(c, msg_contents)in A's processin(c, x)(with matching destructuring) in B's process
Each
Note over A: op → resultbecomes alet result = op inbindingEach
Note over A: Verify(...)becomes alet _ = verify(...) inbinding (the destructor aborts on failure — no explicit else needed, modeling abort)Use
altblocks in the diagram asif/then/elsein the processLong-term keys are process parameters; ephemeral values use
new
N-party or MPC protocols: write one process per distinct role. For
threshold protocols, write a single role process and replicate it !N times
in the main process.
Step 7: Write Main Process and Finalize
The main process:
Generates long-term keys with
newPublishes public keys to the attacker via
out(c, pk(sk))Runs participant processes in parallel under replication (
!) to allow multiple sessionsOptionally leaks long-term keys for forward-secrecy analysis
process
new sk_I: skey; let pk_I = pk(sk_I) in out(c, pk_I);
new sk_R: skey; let pk_R = pk(sk_R) in out(c, pk_R);
(
!Initiator(sk_I, pk_R)
| !Responder(sk_R, pk_I)
)Place the full file in this order:
(* 1. Channel declarations (free c: channel. / free ch: channel [private].) *)
(* 2. noselect directives (if needed for termination) *)
(* 3. Type declarations *)
(* 4. Constants *)
(* 5. Function declarations *)
(* 6. Equations (algebraic identities on constructors only) *)
(* 7. Table declarations *)
(* 8. Events *)
(* 9. Queries *)
(* 10. Let processes *)
(* 11. Main process *)Step 8: Verify and Deliver
Before writing the file:
[ ] Every participant in the diagram has a matching
letprocess[ ] Every
out(c, ...)has a matchingin(c, ...)on the other side with compatible types[ ] Every function used in a process is declared in the preamble
[ ] Every destructor uses inline
reduc(not a separateequationblock)[ ] Every event in a query is declared and triggered in a process
[ ] Long-term public keys are output to channel
cin the main process (attacker can see them — that is the Dolev-Yao model)[ ] No unused declarations (clean up anything added speculatively)
[ ] If
tabledeclarations are present: everyinsert T(...)has a correspondingget T(...)with compatible column types and matching pattern constraints (=keyvs bare name)[ ] If
noselectis used: its tuple structure matches the actual message shapes sent onc(e.g., pairs →mess(c, (x, y)))[ ] If the Key Exposure Oracle pattern is used:
event key_exposed(sk_type)is declared, the oraclein(c, guess: sk_type); if pk(guess) = pk_new then event key_exposed(guess)appears at the end of the process that holds the secret, and the query isquery x: sk_type; event(key_exposed(x))
Write the model to a .pv file. Choose a filename from the protocol name,
e.g. noise-xx-handshake.pv or x3dh-key-agreement.pv.
After writing, print a brief summary:
Protocol: <Name>
Output: <filename>
Queries: <list each query and what property it tests>
Assumptions: <list modeling decisions and simplifications>Decision Tree
├─ No Mermaid diagram provided?
│ └─ Ask the user: "Please provide the Mermaid sequenceDiagram,
│ or run the crypto-protocol-diagram skill first."
│
├─ Diagram uses DH (not just symmetric crypto)?
│ └─ Use dh/dhpk with commutativity equation
│ See references/crypto-to-proverif-mapping.md → DH section
│
├─ Diagram uses asymmetric signatures (Sign/Verify)?
│ └─ Use sign/verify with inline reduc (not equation)
│ verify returns the message on success; let _ = verify(...) in to abort on failure
│ Distinguish signing key (skey) from verification key (pkey)
│
├─ Diagram has an "alt" block (abort path)?
│ └─ Model as if/then only — the else branch aborts (process terminates)
│ Do NOT add out(c, error_message) unless the diagram shows it
│
├─ Protocol has N > 2 parties?
│ └─ Write one process per role, use ! for replication
│ Pass participant index as a parameter if roles differ by index only
│
├─ Forward secrecy requested?
│ └─ Add a ForwardSecrecy variant in the main process that leaks
│ long-term sk after session; add secrecy query for past session_key
│ See references/security-properties.md → Forward Secrecy
│
├─ Type-checker rejects the model?
│ └─ ProVerif is typed: check every function arg type matches declaration.
│ bitstring is the catch-all; key/pkey/skey/nonce are stricter.
│ Cast with explicit constructors when needed.
│
├─ Protocol has cross-process state coordination (e.g., one process must wait
│ for another to record acceptance before proceeding)?
│ └─ Use ProVerif tables (table/insert/get)
│ See references/proverif-syntax.md → Tables
│
├─ Verification does not terminate after several minutes?
│ └─ Add noselect directive matching the message tuple structure on c
│ See references/proverif-syntax.md → noselect
│
├─ Protocol generates a private-type key (type sk [private]) that is never
│ output directly but whose secrecy should be verified?
│ └─ Use the Key Exposure Oracle pattern instead of query attacker(sk)
│ See references/security-properties.md → Key Exposure Oracle
│
└─ Unsure which security properties to verify?
└─ Default set: secrecy of session key + injective authentication
(both directions). Add forward secrecy if diagram shows ephemeral keys.Example
examples/simple-handshake/ contains a worked example:
diagram.md— Mermaid sequenceDiagram for a two-party authenticated key exchange (X25519 DH + Ed25519 signing + HKDF)sample-output.pv— exact ProVerif model the skill should produce, with secrecy and injective authentication queries. Its Q3 (injective authentication from R's view) reportsfalse: the drawn protocol has no responder challenge, so a replayed msg1 completes another R-run. That is a real finding about the protocol, not a modelling error.
Study this before working on an unfamiliar protocol.
Supporting Documentation
references/crypto-to-proverif-mapping.md — Mapping table from Mermaid cryptographic annotations to ProVerif function declarations, equations, and process patterns
references/proverif-syntax.md — ProVerif language reference: types, functions, equations, processes, events, queries, and common pitfalls
references/security-properties.md — Decision guide for choosing the right queries: secrecy, authentication (weak vs injective), forward secrecy, and how to model them
Files
- ATTRIBUTION.mdAdded by AutoGineer941 B
- LICENSELicence file added by AutoGineer19.7 KiB
- SKILL.md18.2 KiB
- agents/openai.yaml249 B
- assets/trail-of-bits-mark.svg3.0 KiB
- examples/simple-handshake/diagram.md1.8 KiB
- examples/simple-handshake/sample-output.pv9.8 KiB
- references/crypto-to-proverif-mapping.md10.1 KiB
- references/proverif-syntax.md13.9 KiB
- references/security-properties.md12.1 KiB
- tests/test_guidance.pyScript2.0 KiB
references/security-properties.md
Security Properties in ProVerif
A guide to choosing and expressing the right security queries for a given Mermaid sequence diagram.
Decision Tree: Which Queries to Include
Start here for every protocol
│
├─ Does the protocol establish a shared key?
│ └─ YES → Add secrecy query for that key
│
├─ Does a party verify the peer's identity (Verify/MAC check)?
│ └─ YES → Add authentication queries (both directions if mutual auth)
│ ├─ Does the verification cover a fresh session value (nonce, epk)?
│ │ └─ YES → Use injective authentication (prevents replay)
│ └─ NO (only verifies static identity, no freshness) → Use weak auth
│
├─ Does the protocol use ephemeral keys (keygen inside the session)?
│ └─ YES → Add forward secrecy query (compromise long-term key after session)
│
├─ Does the protocol have a "challenge-response" nonce exchange?
│ └─ YES → Add injective authentication to capture that freshness
│
└─ Always add: reachability sanity check query1. Secrecy
Goal
The session key (or any sensitive value) is not learnable by the attacker, even after observing all network traffic.
When to add
Add whenever the Mermaid diagram contains a derived session key label (e.g.,
sk_session, sk_I, key_data) that should remain private.
Query
The canonical pattern: declare a private free name and encrypt it under the
session key. The attacker learning private_I is equivalent to breaking the
session key, because decrypting the ciphertext requires it.
(* Top-level declarations *)
free private_I: bitstring [private].
free private_R: bitstring [private].
(* In the Initiator process, after deriving sk_session: *)
out(c, aead_enc(private_I, sk_session));
(* In the Responder process, after deriving sk_session: *)
out(c, aead_enc(private_R, sk_session));
(* Queries *)
query attacker(private_I).
query attacker(private_R).ProVerif proves attacker(private_I) by verifying that no derivation path
for private_I exists. Since private_I is encrypted and decryption requires
sk_session, this is equivalent to session key secrecy.
Important: attacker(k) cannot appear as the consequent of a
correspondence query (event(...) ==> attacker(k) is not valid ProVerif
syntax). Use the witness pattern above instead.
2. Authentication
Weak Authentication
Goal: If B completed accepting a session with A, then A ran at some point. Does NOT prevent replay (attacker can replay A's messages to create a second session where B accepts).
When to use: Protocol has identity verification but no per-session freshness binding (e.g., only verifies a static certificate, no nonce or ephemeral key in the signature).
query pk_i: pkey, pk_r: pkey, k: key;
event(endR(pk_i, pk_r, k)) ==> event(beginI(pk_i, pk_r)).Note: beginI fires before the session key is known, so it typically
has fewer parameters than endR. Match the parameter list to what the
begin event actually receives when it fires.
Injective Authentication
Goal: Each B-accept corresponds to a distinct, unique A-run. Prevents replay attacks.
When to use: The signature (or MAC) covers a fresh value unique to this session — an ephemeral key, a nonce, or a transcript hash. This is the default for most modern protocols.
query pk_i: pkey, pk_r: pkey, k: key;
inj-event(endR(pk_i, pk_r, k)) ==>
inj-event(beginI(pk_i, pk_r)).Note on inj-event: ProVerif will report this as true if there is a
one-to-one correspondence. If it reports false, the protocol is vulnerable
to replay — investigate whether nonces or ephemeral keys are correctly bound
in the signatures.
Mutual Authentication
For protocols where both parties authenticate each other, add queries in both directions:
(* Responder accepts => Initiator ran *)
query pk_i: pkey, pk_r: pkey, k: key;
inj-event(endR(pk_i, pk_r, k)) ==>
inj-event(beginI(pk_i, pk_r)).
(* Initiator accepts => Responder ran *)
query pk_i: pkey, pk_r: pkey, k: key;
inj-event(endI(pk_i, pk_r, k)) ==>
inj-event(beginR(pk_i, pk_r)).Placing Events in the Process
| Event | Where to trigger |
|---|---|
beginI(pk_I, pk_R) | Just before Initiator sends the first authenticated message (the one signed with sk_I) |
endI(pk_I, pk_R, sk) | Just after Initiator successfully verifies Responder's identity AND derives session key |
beginR(pk_I, pk_R) | Just before Responder sends its signed reply |
endR(pk_I, pk_R, sk) | Just after Responder successfully verifies Initiator's identity AND derives session key |
The session key sk in the event parameters binds authentication to the
specific key material established — preventing cross-session confusion attacks.
3. Forward Secrecy
Goal
Compromise of a long-term key AFTER a session completes does not allow the attacker to decrypt past session traffic.
When to add
Add when:
The Mermaid diagram shows ephemeral key generation inside the session (e.g.,
keygen() → ek_I, epk_I)The session key derivation uses DH over ephemeral keys (not just static ones)
Modeling Pattern
Leak long-term keys to the attacker after the sessions have run (in
phase 1) and check that session key secrecy still holds. If it does,
forward secrecy is achieved because the session key depends only on
ephemeral material. Leaking the keys at the start does not test forward
secrecy: the attacker can then impersonate either party, so secrecy fails
for any authenticated protocol.
(* In main process — leak long-term keys only after the sessions *)
new sk_I: skey; out(c, pk(sk_I));
new sk_R: skey; out(c, pk(sk_R));
(
!Initiator(sk_I, pk(sk_R)) | !Responder(sk_R, pk(sk_I))
| (phase 1; out(c, sk_I); out(c, sk_R)) (* compromise after the sessions *)
)The existing session key secrecy query (query attacker(private_I).) now
tests forward secrecy: if the attacker knows both long-term keys but still
cannot derive the session key, the protocol has forward secrecy. If the
query fails, the session key depended on a long-term key.
4. Reachability (Sanity Check)
Goal
Confirm that the success path of the protocol actually executes. A ProVerif model with a bug (e.g., a type error causing a dead receive) may trivially prove all security properties because the end event is never reached.
Always add
query pk_i: pkey, pk_r: pkey, k: key; event(endI(pk_i, pk_r, k)).
query pk_i: pkey, pk_r: pkey, k: key; event(endR(pk_i, pk_r, k)).Note: ProVerif does not support _ wildcards in query declarations; every
parameter must be bound to a typed variable.
ProVerif reports a reachable event as RESULT not event(...) is false; that
is the expected result. If it reports is true, the event is unreachable and
the model has a structural bug. Fix it before trusting any other query result.
5. Key Exposure Oracle (for [private] type secrets)
Goal
Prove that the attacker cannot learn a secret key sk that is declared with a
private type (type sk [private]) and never directly output on the public
channel.
Why query attacker(sk) doesn't work here
query attacker(sk) checks whether the attacker can derive the term sk from
public channel traffic. A value of a private type is never synthesised by the
attacker (the type prevents it), and if sk is never output, ProVerif cannot
prove the query — it would return cannot be proved, not true. The property
needs a different formulation.
The oracle pattern
Give the attacker an explicit "guess oracle": receive an arbitrary value of
type sk from the public channel, then fire a key_exposed event if that
guess matches the known public key:
(* Declare the sentinel event — use whatever secret key type the protocol uses *)
event key_exposed(skey).
(* Secrecy query: key_exposed must be unreachable *)
query x: skey; event(key_exposed(x)).
(* At the end of the process that generated sk_new — place after all other
* protocol steps so the oracle is only reachable on a complete run: *)
in(c, guess: skey);
if pk(guess) = pk_new then event key_exposed(guess)
else 0.How it works:
The attacker submits its best guess for
sk_newviac.The process checks whether
pk(guess)matches the knownpk_new.If the check succeeds,
key_exposedfires.ProVerif proves
event(key_exposed(x))unreachable → no guess can match the public key →sk_newis secret.
When to use
Use the oracle pattern when all of these hold:
The secret key is of a
[private]type (e.g.,type skey [private]) and never output directly.The corresponding public key IS observable on
c(e.g., sent in a signed message or after key exchange).query attacker(sk_new)would returncannot be proved(not meaningful).
Placement in process
Put the oracle at the very end of the process that generated sk_new,
after all other steps that depend on the secret are complete (typically
after the signature is sent). This ensures key_exposed is only reachable
upon protocol completion, not on every intermediate step.
6. Query Checklist by Protocol Type
Two-party key exchange (e.g., DH-based handshake)
(* Sanity — all parameters must be bound to typed variables *)
query pk_i: pkey, pk_r: pkey, k: key; event(endI(pk_i, pk_r, k)).
query pk_i: pkey, pk_r: pkey, k: key; event(endR(pk_i, pk_r, k)).
(* Session key secrecy — use the witness pattern, not correspondence *)
query attacker(private_I).
query attacker(private_R).
(* Mutual injective authentication *)
query pk_i: pkey, pk_r: pkey, k: key;
inj-event(endR(pk_i, pk_r, k)) ==> inj-event(beginI(pk_i, pk_r)).
query pk_i: pkey, pk_r: pkey, k: key;
inj-event(endI(pk_i, pk_r, k)) ==> inj-event(beginR(pk_i, pk_r)).Unilateral authentication (server authenticates, client does not)
(* Sanity *)
query pk_s: pkey, k: key; event(endC(pk_s, k)).
(* Session key secrecy — use the witness pattern *)
query attacker(private_C).
(* Server authentication only — client to server direction *)
query pk_s: pkey, k: key;
inj-event(endC(pk_s, k)) ==> inj-event(beginS(pk_s, k)).Commit-reveal protocol
(* Binding: verifier accepts only the committed value *)
query v: bitstring, r: bitstring;
event(accepted(v)) ==> event(committed(v, r)).
(* Hiding: attacker cannot learn committed value before reveal *)
query attacker(committed_value).Challenge-response authentication
(* Freshness: each successful auth used a distinct challenge *)
query id: pkey, ch: bitstring;
inj-event(authSuccess(id, ch)) ==> inj-event(challengeSent(id, ch)).7. Interpreting ProVerif Output
| ProVerif result | Meaning |
|---|---|
RESULT ... is true. | Property holds for all executions (proof found) |
RESULT ... is false. | Attack found — ProVerif prints a trace |
RESULT ... cannot be proved. | Proof search timed out or approximation too coarse; does not mean the property is false |
Reachability queries (query event(e)) read the other way round: ProVerif
checks not event(e), so is false means the event is reachable (expected)
and is true means it never fires (a broken model).
When a result is false, read the attack trace carefully:
Identify the event sequence ProVerif found
Map it back to the Mermaid diagram steps
Determine if the attack is a real flaw or a modeling artifact
Common false attacks from modeling artifacts:
Type confusion: Two bitstrings with compatible types where the model should use distinct types
Missing replication: Single-session model allows trivial "man-in-middle" because the attacker is the only other party
Missing freshness binding: Signature does not include the nonce/epk, so replay is possible at the model level even if the spec would bind it
When a result is cannot be proved, consider:
Adding more specific type annotations
Splitting the
!replication into bounded sessionsSwitching to a weaker query first to establish partial results
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Python
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Trail of Bits
- Publisher
- Trail of Bits (Official)
- Source
- trailofbits/skills · plugins/trailmark/skills/mermaid-to-proverif · commit e4dd0bb
- Licence
- Creative Commons Attribution-ShareAlike 4.0 International (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Trail of Bits or Trail of Bits.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download mermaid-to-proverif.zip