Skip to content

ProVerif analysis of the v1 ML-KEM Braid (hax Rust→ProVerif) + proof-status docs - #4

Draft
karthikbhargavan wants to merge 7 commits into
mainfrom
spqr-proverif-backend
Draft

ProVerif analysis of the v1 ML-KEM Braid (hax Rust→ProVerif) + proof-status docs#4
karthikbhargavan wants to merge 7 commits into
mainfrom
spqr-proverif-backend

Conversation

@karthikbhargavan

@karthikbhargavan karthikbhargavan commented Jul 13, 2026

Copy link
Copy Markdown

What this adds

Machine-checked ProVerif analysis of the SPQR public ratchet (the v1 ML-KEM Braid
continuous key agreement), with the protocol model compiled from the Rust source by
hax's ProVerif backend rather than hand-transcribed,
plus a unified F*/ProVerif proof-status doc set.

Draft: the ProVerif extraction depends on the hax ProVerif backend (cryspen/hax
proverif-rust-backend, upstream draft PR #2068);
CI builds it via proofs/proverif/setup-hax.sh at the pinned commit.

Commits (reviewable chunks)

  1. spqr: source annotations …cfg(hax_backend_proverif)-gated pv_extern /
    replace_body on the crypto leaves + the behaviour-preserving refactors they need.
    Inert in normal builds (52 tests pass).
  2. proverif: models … — hand-written reference (spqr-cka / spqr-dr) + the
    hax-extracted extraction/lib.pvl (rooted at the state-machine entry points) + the
    composition (symbolic crypto, compromise model, per-property queries with sound
    nounif + EXPECTPV blocks).
  3. proverif: hax.py driver + CIhax.py {setup,extract-proverif,verify-proverif, check-proverif} as the sole driver + setup-hax.sh + a two-tier proverif.yml
    (verify per-push, re-extract + drift-check nightly).
  4. docsproofs/PROOF_STATUS.md (single source for what's proven + the trust
    boundary) + per-tool REPRODUCING.mds; README defers to them.

What is proven (see proofs/PROOF_STATUS.md)

  • ProVerif (symbolic, bounded): reachability, confidentiality with forward
    secrecy
    , and mutual authentication for the CKA. Post-compromise healing holds against
    a KEM-key compromise but not an authenticator compromise (a MAC-chain property,
    shared with the hand-written model) — documented honestly rather than claimed flatly.
  • F* (via hax): panic-freedom for most of the crate + GF(2¹⁶) finite-field
    correctness, modulo a documented assume/admit trust boundary (ML-KEM is trusted as an
    interface).

Note on wording

The docs correct two earlier over-broad claims: flat "post-compromise security" → the
FS-yes / PCS-partial nuance above; and "proved panic-free and correct" →
panic-freedom modulo the documented trust boundary. No code behaviour changes.

Not included here

An experimental Aeneas-backend ProVerif + Lean driver (hax-aeneas.py) reproduces the
same ProVerif verdicts via an independent toolchain (charon → aeneas Pure-IR →
proverif-from-pure); it lives on its own branch (needs external aeneas tooling, not
CI-runnable).

AI assisted.

…ng refactors

cfg(hax_backend_proverif)-gated pv_extern/replace_body annotations mapping each crypto leaf onto the symbolic model, plus the behaviour-preserving refactors they require (Authenticator::update made consuming; struct-literal field order; rng threading). Inert in normal builds (52 tests pass).
handwritten/ reference (spqr-cka, spqr-dr, cryptolib); extraction/lib.pvl compiled from src/v1/unchunked by hax's ProVerif backend, rooted at the state-machine entry points; extraction-model/ composition (symbolic crypto, ping-pong + fixed-compromise process model, per-property queries with sound nounif + EXPECTPV blocks).
hax.py {setup,extract-proverif,verify-proverif,check-proverif} is the sole driver; setup-hax.sh builds the pinned backend (Rust-only); proverif.yml verifies per-push and re-extracts + drift-checks nightly.
proofs/PROOF_STATUS.md is the single source for what is proven + the trust boundary (F* panic-freedom + GF(2^16) correctness modulo a documented assume/admit boundary; ProVerif CKA security with the honest forward-secrecy / post-compromise nuance). Per-tool REPRODUCING guides; README defers to them. Corrects the earlier flat FS/PCS and 'panic-free and correct' claims.
…nt extraction)

The rust ProVerif engine delegates its import phase to the OCaml hax-engine, located via $HAX_ENGINE_BINARY (else a bare 'hax-engine' PATH lookup). The shell's active opam switch usually ships a mismatched hax-engine build -> the import panics ('ocaml engine crashed'). _proverif_env now resolves the matching engine from the hax-proverif switch (override via $HAX_OPAM_SWITCH / $HAX_ENGINE_BINARY), so extract-proverif works regardless of the active switch. Verified byte-reproducible under the default switch.
…ment the switch requirement

Extraction runs its import phase through the OCaml hax-engine, so the rust-only build was insufficient. setup-hax.sh now creates the hax-proverif switch and builds+installs the matching OCaml engine (opam pin engine/) — inlined, because hax's setup-local.sh is an untracked local convention NOT in the committed hax tree, so a fresh clone wouldn't have it. Same latent bug fixed in the proverif.yml nightly extract job (it also called ./hax-engine/setup-local.sh). REPRODUCING.md: corrected the 'no OCaml/opam' claim, added the OCaml-engine toolchain row (needs opam/node/jq), documented hax.py auto-resolving the engine (override via $HAX_OPAM_SWITCH / $HAX_ENGINE_BINARY).
The doc quoted an expected SHA-256 of
51b54eed55ee23d19386f7a62fb68dbf3a25ef80d5567c77f86e1c8d20f6395b, but the real
digest of the committed extraction/lib.pvl -- and the value in
extraction/lib.pvl.sha256 -- is
4914798cd53878dec1b3f43a00dad9d322013cd0cc09f14e7634e6366c70572c. The prose
copy had gone stale.

Nothing was actually wrong with the extraction: the L2 recipe checks the
.sha256 file, not the doc, so it kept passing. But anyone eyeballing
REPRODUCING.md would conclude the extraction had drifted when it had not --
which is a bad failure mode for a document whose whole job is to certify
byte-identity.

Point at extraction/lib.pvl.sha256 instead of restating the value. That file is
regenerated alongside lib.pvl, so it cannot drift, and it is what `shasum -c`
already verifies. Repeating the digest next to the file that holds it is what
caused this, so the fix removes the duplication rather than refreshing it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant