This F* development is the framework Doppel, and case studies, from the paper Real Ideals: Symbolic Verification of Simulation-Based Security. It formalizes testing equivalence — an indistinguishability notion for security protocols modelled in the symbolic (Dolev–Yao) style — and uses it to verify that concrete protocol implementations (the "real" world) are observationally indistinguishable from idealized functionalities composed with a simulator (the "ideal" world) in the presence of an active adversary (a "tester"). This is the symbolic analogue of simulation-based (real/ideal) security: a protocol realizes an ideal functionality when no tester can distinguish the two.
The core idea: a protocol is modelled as a reactive labelled transition system (RLTS), composed with a tester (adversary) that interacts with it and accumulates knowledge. Two protocols are testing equivalent when, for every interaction the tester can drive against one, it can drive a matching interaction against the other with the same observable effects and ending in statically equivalent knowledges — so the tester can never tell them apart. Rather than reason about traces directly, the development establishes a sufficient condition: exhibiting a barrier bisimulation up to right-movable partitions between two systems implies they are testing equivalent. Each example proves indistinguishability by constructing such a bisimulation.
Naming note. The paper standardizes on barrier bisimulation; the F* module names and identifiers here (like the companion Lean development) retain the earlier name observational (bi)simulation (
ObservationalBisimulation,obs_bisim_up_to_partitions, …). They denote the same notion.
The framework's two central metatheorems are mechanized in the companion Lean development and axiomatized here (see Metatheory mechanized in Lean). Code comments cite the paper by section, definition and theorem number: the formal model lives in paper §3 ("Formal model and definitions") and the two proof techniques (barrier bisimulation and its Lipton-style right-moving partitioned relaxation) in paper §4 ("Proving testing equivalence").
Doppel was developed and tested with exactly the following versions (on Ubuntu 26.04, x86_64). Other F* versions may work, but F*'s SMT encoding changes between versions and SMT-heavy proofs like these can become unstable, so we recommend the pinned versions.
| Tool | Version | Notes |
|---|---|---|
| F* | release tag v2026.05.31 (fstar.exe --version reports 2026.05.31~dev, commit 097bda35) |
installed with opam |
| Z3 | 4.13.3 | the version F* expects; must be on PATH as z3 |
| OCaml | 5.4.1 | only needed to build F* |
| opam | ≥ 2.1 (tested with 2.5.1) | |
| GNU make, Python 3 | any recent | Python ≥ 3.9 for the scripts in scripts/ |
Verifying everything takes about 2 minutes of wall-clock time with make -j8
(about 8 minutes of CPU time in total, so roughly 8 minutes with plain make),
and needs about 2 GB of RAM per parallel job.
# 1. Create an opam switch with the right OCaml (skip if you already have one)
opam init --bare -n # only if opam has never been initialized
opam switch create doppel ocaml-base-compiler.5.4.1
eval $(opam env --switch=doppel)
# 2. Install Z3 4.13.3 and F* at the pinned release tag
# (builds OCaml dependencies and F* from source: ~15 min on a multi-core machine)
opam install z3.4.13.3
opam pin add fstar "git+https://github.com/FStarLang/FStar#v2026.05.31"
# 3. Check the versions
fstar.exe --version # F* 2026.05.31~dev ... commit=097bda35...
z3 --version # Z3 version 4.13.3 - 64 bitAlternatively, if you already have a build of F* at that tag, set
FSTAR_HOME to its root: the Makefile then uses $FSTAR_HOME/bin/fstar.exe.
All commands are run from the root of this repository.
make -j8 # verify everything (~2 min with -j8)
make output/StaticEquivalence.fst.checked # verify one module (and its dependencies)
make audit # list every admit/assume (see below)
make clean # remove all verification resultsVerification results (.checked files) are written to output/. Running
make again only re-verifies modules whose sources changed. Module names in
targets are dotted (output/MA.Ideal.fst.checked), not paths. After adding,
renaming or deleting a module, or changing its opens, run make .depend
first so that the dependency graph is regenerated.
A successful run ends with Verified module: ... / All verification conditions discharged successfully lines for every module and no Error.
The following warnings are expected and harmless:
Warning 274: Implicitly opening namespace 'ma.' shadows module 'adversary'(and the same forsmd.): the example modulesMA.AdversaryandSMD.Adversaryshare their last name component with the library moduleAdversary.Did not expect module FStar.... to be already checked: F*'s standard library is found pre-checked in the opam installation rather than inoutput/.
make audit (equivalently python3 scripts/audit_assumes.py after make)
lists every escape hatch in the development. It combines a source scan for
admit, assume, tadmit, magic, unsafe_coerce, --admit_smt_queries
and --lax (ignoring comments and strings) with F*'s own
--report_assumes warn report. It takes about a minute and fails if it finds
anything beyond the four expected escape hatches. Its output (the code:
line printed under each entry is omitted here) is:
Escape hatches found (file:line, what it is, found by):
ObservationalBisimulation.fst:39 obs_bisim_up_to_partitions_implies_t_equivalence (paper Thm 4.7), proved in Lean [source scan, F* report]
ObservationalBisimulation.fst:68 obs_bisim_up_to_partitions_implies_t_equivalence' (paper Thm 4.13), proved in Lean [source scan, F* report]
helpers/FStar.PartialMap.Ext.fst:15 FStar.PartialMap.Ext.map: workaround for opaque FStar.PartialMap (SMD example only) [source scan, F* report]
helpers/FStar.PartialMap.Ext.fst:19 FStar.PartialMap.Ext.sel_map: workaround for opaque FStar.PartialMap (SMD example only) [source scan, F* report]
AUDIT PASSED: the only escape hatches are the 4 expected ones.
See Metatheory mechanized in Lean and Example-specific helpers for why these are justified.
. the reusable Doppel library (top-level modules, see below)
helpers/ general-purpose F* libraries used by the library and examples
examples/ the two case studies of the paper, as usage examples
secure_message_delivery/ SMD.* (paper §5.1)
nsl_mutual_authentication/ MA.* (paper §5.2)
scripts/ audit_assumes.py (escape-hatch audit), estimate_loc.py (Table 1)
Makefile verification build; output/ holds the .checked results
The top-level modules form the reusable library: they define the model
(protocols, adversaries, the composed tested system, static and testing
equivalence) and the proof technique (barrier bisimulation up to right-movable
partitions), independently of any particular protocol. Read the .fsti
interface of a module first; the .fst holds the proofs.
| Module | Contents |
|---|---|
SymbolicTypes |
Base symbolic types: knowledge (a seq term), channel ids, knowledge membership. |
Adversary |
The adversary typeclass: derivations, derive, the indistinguishable relation (required to be an equivalence relation), and helper fields. |
Protocol |
Protocols as RLTSs: transition labels (Internal / Observable), protocol state (inputs bag × internal state), and the protocol typeclass. |
StaticEquivalence |
statically_equivalent, plus the bijective-correspondence machinery (bijective_correspondence_implies_static_equivalence) used to discharge static equivalence. |
TestedSystem |
The composed tester ∥ protocol system: combined state, combined labels, the transition relation transitions', the knowledge-validity predicates, and system (compose a protocol with an adversary). |
TestEquivalence |
multi_transitions, observable effect projection (label_effects), and the t_preorder / t_equivalence definitions. |
ObservationalBisimulation |
Partitions, right-movability (right_movable, right_movable_mod_inv), observational (= barrier) bisimulation up to partitions, and the two main metatheorems. |
SencTerms, AdversarySenc |
A term algebra and Dolev–Yao adversary for symmetric encryption (derive, derivable, indistinguishability, and lemmas about them). |
AencTerms, AdversaryAenc |
The same for asymmetric (public-key) encryption. |
The library uses three of the helper libraries in helpers/, which are
therefore part of it: Bag (multisets; the protocols' input bags), Partition
(the partition typeclass: an equivalence relation on labels) and
FStar.Set.Properties (lemmas about FStar.Set).
The library's only escape hatches are the two metatheorems in
ObservationalBisimulation, which are proved in the companion Lean development
(see below). Everything else is verified by F*. Main.fst is an empty
placeholder module.
helpers/FStar.Bijection.Ext (extra lemmas about FStar.Bijection) and
helpers/FStar.PartialMap.Ext (extra operations on FStar.PartialMap) are
only used by the examples and are not part of the library.
FStar.PartialMap.Ext admits map (mapping a function over the values of
a PartialMap) and its specification sel_map
(sel (map f m) k == mapTot f (sel m k)), which the SMD example's state
machinery uses. These admits are a temporary workaround: FStar.PartialMap
keeps its definitions opaque behind its interface, which does not expose
enough to define map or prove sel_map from outside the module. Once these
definitions are upstreamed into FStar.PartialMap in F*'s standard library
(where the representation is visible), the admits are no longer needed.
Two end-to-end indistinguishability proofs (the paper's case studies, paper §5), each showing that a protocol implementation realizes an ideal functionality composed with a simulator. They double as worked examples of how to use the library.
secure_message_delivery/(SMD): concurrent two-party secure message delivery. The top-level theorem isSMD.ObservationalBisimulation.Proof.smd_t_equivalence. It uses the unconditioned partitioned theorem (obs_bisim_up_to_partitions_implies_t_equivalence, paper Thm. 4.7) with the partition by sessionsmd_partition.nsl_mutual_authentication/(MA): the Needham–Schroeder–Lowe protocol realizing ideal mutual authentication under statically corrupted participants. The top-level theorem isMA.ObservationalBisimulation.Proof.ma_t_equivalence. It uses the invariant-conditioned variant (obs_bisim_up_to_partitions_implies_t_equivalence', paper Thm. 4.13) with the partition by session, participants and role (MA.PartitioningRole.ma_partition_role), since right-movability only holds at reachable states (MA.IdealInvariants.ma_ideal_system_inv).
To prove that a protocol P is testing equivalent to an ideal functionality
composed with a simulator Q, follow the structure of the examples (module
prefix SMD. or MA.; the SMD example is the smaller one and the better
starting point):
- Terms and adversary. Pick (or write) a term algebra and an
adversaryinstance: reuseSencTerms/AdversarySencorAencTerms/AdversaryAenc, or write a new instance of theAdversary.adversaryclass. Protocol-specific types (session ids, effects, label ids) go in*.Types;*.Adversaryholds protocol-specific lemmas about the adversary. - The two protocols. Define each side as an instance of
Protocol.protocol(SMD.Implementation/MA.NSLfor the real world,SMD.Ideal/MA.Idealfor the ideal functionality plus simulator) and prove that its transitions only consume inputs (*_valid_transitions). Then compose each with the adversary usingTestedSystem.system(*.TestedProtocols:smd_impl_system,smd_ideal_system). - Partition and right-movability. Define a
Partition.partitionon labels (usually by session; seesmd_partitioninSMD.ObservationalBisimulation.Proof.fstiandMA.PartitioningRole) and prove it isright_movablefor both systems. If that only holds on reachable states, proveright_movable_mod_invfor an inductive invariant instead (*.IdealInvariants,*.ImplementationInvariants). - Relation. Define the candidate barrier bisimulation relation
(
*.ObservationalBisimulation.Relation:smd_bisim_relation,ma_bisim_relation) and prove that related states are statically equivalent.StaticEquivalence.bijective_correspondence_implies_static_equivalencereduces this to exhibiting a bijection between the two adversary knowledges that maps each term to an indistinguishable one (SMD.Bijection). - Simulation in both directions. Prove
obs_sim_up_to_partitionsfromPtoQand back. The step lemmas (one per transition kind) are in*.ObservationalBisimulation.Proof.*, with existential witnesses for the matching transitions in*.ObservationalBisimulation.Witnesses*. - Conclude. Apply
obs_bisim_up_to_partitions_implies_t_equivalence(or its primed, invariant-conditioned variant) to obtaint_equivalence, as insmd_t_equivalenceandma_t_equivalence.
Put new examples under examples/<name>/: the Makefile finds every .fst and
.fsti under helpers/ and examples/ automatically. Add the new directory to
include_dirs in .fst.config.json if you use an editor integration.
scripts/estimate_loc.py produces the line counts reported in the paper's
Table 1, split into trusted specification, auxiliary definitions, auxiliary
lemma statements, and proof. Its docstring (python3 scripts/estimate_loc.py --help) explains the classification heuristic; the TRUSTED table in the
script lists the trusted modules and declarations.
python3 scripts/estimate_loc.py # all examples (or: smd, ma)It prints a per-file breakdown followed by a SUMMARY table and the list of
admitted declarations.
The two central metatheorems in ObservationalBisimulation.fst are
axiomatized here with admit (), because they are mechanically proven in
the companion Lean development
Doppel Metatheory.
Each admit () is an instance of the corresponding Lean theorem: the F*
statements match the Lean ones in strength, so that the admit ()s are
genuinely justified by the Lean proofs (e.g. t_preorder is the effects-based
weak preorder, indistinguishable is an equivalence relation, and the axioms
carry the transition-validity hypotheses the Lean proofs rely on).
The F* framework exposes only the two partitioned theorems (the ones the case studies use); the paper's and Lean's unpartitioned barrier-bisimulation theorem (paper Thm. 4.2) is subsumed and not restated here. The two admitted axioms are:
obs_bisim_up_to_partitions_implies_t_equivalence— a (barrier) bisimulation up to right-movable partitions implies testing equivalence (paper Thm. 4.7). Backed by Lean'sobservational_bisimulation_up_to_partitions_is_sufficient_for_weak_test_equivalence.obs_bisim_up_to_partitions_implies_t_equivalence'— the same, but with right-movability only required at states satisfying an inductive invariantinv(over-approximating the reachable states) (paper Thm. 4.13). Backed by Lean'sobservational_bisimulation_up_to_partitions_is_sufficient_for_weak_test_equivalence_mod_inv.
Everything else in the F* framework (the static-equivalence machinery, the
tested-system transition relation, the example bisimulation constructions) is
proven in F* without admit (). The validity of the tested-system relation
derived from a protocol is itself proven in F* as transitions'_valid.
The only other admits are map and sel_map in the example-only helper
helpers/FStar.PartialMap.Ext (see
Example-specific helpers).
The two developments share the same structure but differ in surface
representation (F* uses seq, bag, and decidable-equality eqtypes where
Lean uses List, Multiset, and bare Types with Setoid/DecidableRel
instances). The correspondences below let a reader check that the F* axioms
instantiate the Lean theorems. For the paper's definition and theorem
numbers, see the citations in each module's comments (paper §3 for the formal
model, §4 for the proof techniques); note that both the F* and Lean identifiers
use observational where the paper says barrier bisimulation.
| F* (this repo) | Lean (companion development) |
|---|---|
adversary class |
Tester class |
knowledge term (= seq term) |
List Term |
Protocol.label Internal/Observable |
ObservableLabel .internal/.external |
Protocol.state (inputs, internal) |
ProtoState (inputs, internal) |
id (label identifier type param) |
Label / ProtoLabel |
obs |
Effect |
TestedSystem.state (know, inputs, internal) |
SystemState (knowledge, inputs, protocol_state) |
TestedSystem.label Internal/Observable/Input |
CombinedLabel .internal/.external/.tester_input |
TestedSystem.transitions' |
combined_transitions |
TestedSystem.valid_observable_transition_relation |
validObservableTransitionRelation |
TestedSystem.transitions'_valid |
(no direct counterpart; Lean takes transition validity as a hypothesis) |
multi_transitions |
multi_transition |
tested_effect Observable/Input |
SystemEffect .protocol_effect/.derive |
lbl_to_effect |
effect_from_label |
label_effects |
effects |
statically_equivalent |
statically_equivalent |
is_eventually_observable |
EventuallyObservable |
partition class |
Setoid + DecidableRel |
right_movable |
rightMovablePartition |
right_movable_mod_inv |
rightMovablePartitionModInvariant (+ per-state rightMovableFrom) |
obs_sim_up_to_partitions |
observationalSimulationUpToPartitions |
obs_bisim_up_to_partitions |
observationalBisimulationUpToPartitions |
t_preorder |
weak_test_preorder |
t_equivalence |
weak_test_equivalence |
obs_bisim_up_to_partitions_implies_t_equivalence |
observational_bisimulation_up_to_partitions_is_sufficient_for_weak_test_equivalence |
obs_bisim_up_to_partitions_implies_t_equivalence' |
observational_bisimulation_up_to_partitions_is_sufficient_for_weak_test_equivalence_mod_inv |
-
Adversary / tester. F*'s
adversarytypeclass and Lean'sTesterboth bundle a derivation type, aderive : Derivation -> knowledge -> option termfunction, and a term-indistinguishability relation. Both require indistinguishability to be an equivalence relation — F* via theindistinguishable_equivfield, Lean via modelling it as aSetoid. F*'sderivationis aneqtype(soderive-equality is decidable) while Lean'sDerivationis a bareTypewith propositionalOption Termequality; these are equivalent in content. F* additionally carriesderivableandappend_preserves_derivefields, used only by the example protocols and with no Lean counterpart. -
Static equivalence.
StaticEquivalence.statically_equivalentand Lean'sstatically_equivalentare the same two-part definition: (i) every derivation produces indistinguishable terms in both knowledges, and (ii) two derivations agree on whether they produce equal terms. Equal knowledges are statically equivalent in both (statically_equivalent_if_knowledge_equalin Lean), and the relation is symmetric (statically_equivalent_symmetric). -
Tested system & transition validity.
TestedSystem.transitions'is the three-case combined relation (internal step preserves knowledge; observable step appends the emitted term; tester input applies a derivation into the input bag), matching Lean's inductivecombined_transitions.valid_observable_transition_relationmirrorsvalidObservableTransitionRelation(internal transitions preserve knowledge, others only extend it). The Lean metatheorems take this validity as a hypothesis; the F* axioms instead build their two systems withTestedSystem.system, so validity holds by construction (the F*-proventransitions'_valid) and is not restated as arequires. -
Observable effects & the preorder.
label_effectsprojects a label sequence to its observable subsequence exactly as Lean'seffectsdoes (dropping internal labels, keeping observable effects and tester derivations).t_preorder/t_equivalenceare the weak (effects-based) preorder and equivalence: the matching trace need only agree onlabel_effects, not on the literal label list — corresponding to Lean'sweak_test_preorder/weak_test_equivalenceand the paper's weak multi-step arrow. -
Partitions & right-movability. F*'s
partitiontypeclass (an equivalence relationin_same_partition) corresponds to a LeanSetoidtogether with aDecidableRelinstance.right_movablematchesrightMovablePartition: across distinct parts, an internal label commutes to the right past any other label.right_movable_mod_invmatchesrightMovablePartitionModInvariant, which only requires that commutation from states satisfying an inductive invariantinv(Lean factors the per-state obligation into the helperrightMovableFrom). -
The two metatheorems.
obs_bisim_up_to_partitions(a relation that is an observational simulation up to partitions in both directions) corresponds to Lean'sobservationalBisimulationUpToPartitions. The admitted F* lemmaobs_bisim_up_to_partitions_implies_t_equivalenceis exactly Lean'sobservational_bisimulation_up_to_partitions_is_sufficient_for_weak_test_equivalence, and the invariant-conditioned…'variant is Lean's…_mod_invtheorem. Both F* lemmas — like the Lean and paper theorems — give sufficient conditions for a protocol composed with an adversary: they take two protocolsproto_p/proto_qand the testeradvand compose them withTestedSystem.system(the analogue of Lean'scombined_transitions proto), rather than quantifying over arbitrary tested systems. The remaining hypotheses line up field-for-field: initial relatedness, "relimplies static equivalence", the bisimulation itself, and right-movability (unconditioned, or conditioned on initial-plus-inductive invariants for the primed/_mod_invversions). Transition validity — taken as an explicit hypothesis in Lean — is automatic here because the systems are built byTestedSystem.system(discharged by the F*-proventransitions'_valid), so it is not restated as arequires.
Apache License 2.0; see LICENSE.