mirror of
https://github.com/saymrwulf/fips205-slhdsa-verified.git
synced 2026-09-03 19:53:49 +00:00
Addresses the round-2 reviewer punch-list. No theorem statement, proof term,
or fold definition changed; the eleven cones are unchanged (independent
collectAxioms dump in verification/RECORDED-RUN.md).
AUDIT GATE (both reviewers, the critical one)
- Retire the bash #print-axioms text parser (fail-open on empty/truncated
reports, and only a SUBSET check). Replace with verification/Proofs/Audit.lean:
reads each certificate's cone from the kernel via collectAxioms and asserts
EXACT set equality against its expected boundary. Extra axiom, dropped
oracle, renamed/deleted cert, or an axiom/opaque sham each throw -> non-zero
Lean exit. No text to misparse; nothing fails open. check.sh Phase 3 now just
compiles it (and still requires the explicit PASSED line).
- check-selftest.sh rewritten to attack the new gate: dead-file, smuggled extra
axiom (named), dropped-oracle (subset would pass, exact must not), and a
vanished certificate (the collectAxioms-returns-[] trap). All four rejected.
REPRODUCIBILITY (GPT B1.4 / B1.5)
- extract.sh refuses a wrong-commit or dirty source tree (fail-closed), takes
an optional source-path arg, and pins the source commit.
- verification/PROVENANCE.json: single machine-readable pin set (source +
charon + aeneas commits/channel + lean + ocaml) with generated-file sha256.
- Re-running extract.sh reproduces gen/SlhVerify/{Types,Funs}.lean
byte-identically (companion fips205-source commit adds Cargo.lock +
rust-toolchain.toml; verified not to perturb the model).
DOC HONESTY (both reviewers)
- README: fix the self-contradiction (apex "not yet proven" trailer vs the
proven apex), the false "oracles kept OUTSIDE every cone" (they are INSIDE,
by design), "deployed monomorphic path" and "semantics-identical for every
parameter set" overclaims, "only two lines changed", stale snapshot head;
retitle the stale future-tense "what will be claimed" section.
- TRUSTED-BASE: drop "nothing proven yet"; add base_2b-inner and deployment-
bridge non-claims explicitly; current pin.
- ChainSpec header: "deployed monomorphic path" -> private verify_mono facade
(comment only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
161 lines
7.3 KiB
Text
161 lines
7.3 KiB
Text
/- Proofs/ChainSpec.lean — Algorithm 5 (chain / WOTS+ chaining) fidelity.
|
||
|
||
THEOREM chain_free_loop_eq: the extracted `chain_free` loop equals the
|
||
explicit s-fold application of the hash F, with the hash-address set to
|
||
i, i+1, …, i+s−1 in turn. This rules out — machine-checked, for the
|
||
monomorphic SHA2-128s `verify_mono` path (a private facade, not the
|
||
deployed generic verifier) — an off-by-one loop bound, a wrong address
|
||
field, and wrong threading. F stays opaque
|
||
(verify_mono.oracle.f), so the certificate cone is the three kernel axioms
|
||
+ oracle.f, and nothing else (audited by check.sh Phase 3).
|
||
|
||
The proof: an induction on the step count. `chain_step` is one loop step =
|
||
one fold step, proven by unfolding the Aeneas `loop` fixpoint one turn
|
||
(loop_unfold_bind), reducing the range iterator to a clean equation
|
||
(fips205_hnext) and the loop body to a clean do-block (fips205_hbody), and
|
||
aligning the monadic u32 index increment (u32_succ / fwd_succ) with the
|
||
fold's. The succ case threads the IH under the opaque binds with
|
||
bind_congr.
|
||
-/
|
||
import SlhVerify.Funs
|
||
open Aeneas Aeneas.Std Result ControlFlow
|
||
open fips205
|
||
|
||
set_option maxHeartbeats 4000000
|
||
|
||
namespace fips205
|
||
|
||
/-- The successful u32 increment as a clean equation (no overflow). -/
|
||
theorem u32_succ {start : Std.U32} (hb : start.val + 1 < 2 ^ 32) :
|
||
∃ w : Std.U32, start + 1#u32 = ok w ∧ w.val = start.val + 1 := by
|
||
have he := Std.UScalar.add_equiv start (1#u32)
|
||
cases hc : start + 1#u32 with
|
||
| ok w =>
|
||
refine ⟨w, rfl, ?_⟩
|
||
rw [hc] at he
|
||
have : (1#u32 : Std.U32).val = 1 := by rfl
|
||
omega
|
||
| fail e =>
|
||
exfalso; rw [hc] at he; simp [Std.UScalar.inBounds] at he
|
||
have : (1#u32 : Std.U32).val = 1 := by rfl
|
||
omega
|
||
| div => rw [hc] at he; simp at he
|
||
|
||
/-- The range iterator's forward step, when start+1 succeeds. -/
|
||
theorem fwd_succ {start w : Std.U32} (hw : start + 1#u32 = ok w) :
|
||
U32.Insts.CoreIterRangeStep.forward_checked start 1#usize = ok (some w) := by
|
||
unfold U32.Insts.CoreIterRangeStep.forward_checked
|
||
have h1 : (1#usize : Std.Usize).val < 2 ^ 32 := by decide
|
||
simp only [h1, dif_pos]
|
||
have hone : Std.U32.ofNatCore (1#usize : Std.Usize).val h1 = (1#u32 : Std.U32) := by
|
||
apply Std.UScalar.eq_of_val_eq; rfl
|
||
rw [hone]
|
||
unfold Std.U32.checked_add core.num.checked_add_UScalar Option.ofResult
|
||
rw [hw]
|
||
|
||
/-- The Aeneas `loop` fixpoint, unfolded one turn into a bind. The `casesOn`
|
||
continuation matches loop's own reduction, so it closes by cases+rfl
|
||
(a hand-written `match` would compile to a different, non-defeq matcher). -/
|
||
theorem loop_unfold_bind {α β : Type} (body : α → Result (ControlFlow α β)) (x : α) :
|
||
loop body x = body x >>= (fun r => ControlFlow.casesOn r (fun c => loop body c) (fun d => ok d)) := by
|
||
conv_lhs => rw [loop.eq_1]
|
||
cases body x with
|
||
| ok cf => cases cf <;> rfl
|
||
| fail e => rfl
|
||
| div => rfl
|
||
|
||
/-- The range iterator step on a non-empty range, as a clean equation. -/
|
||
theorem hnext {start stop w : Std.U32}
|
||
(hd : decide (start.val < stop.val) = true) (hwok : start + 1#u32 = ok w) :
|
||
core.iter.range.IteratorRange.next U32.Insts.CoreIterRangeStep
|
||
{ start := start, «end» := stop }
|
||
= ok (some start, { start := w, «end» := stop }) := by
|
||
unfold core.iter.range.IteratorRange.next
|
||
simp only [core.cmp.impls.PartialOrdU32.lt, hd, decide_true, if_true,
|
||
bind_tc_ok, bind_ok, core.clone.impls.CloneU32.clone, fwd_succ hwok]
|
||
|
||
/-- The loop body on a non-empty range reduces to a clean do-block. -/
|
||
theorem hbody {N : Std.Usize} (pk_seed : Slice Std.U8) (start stop w : Std.U32)
|
||
(adrs : types.Adrs) (tmp : Array Std.U8 N)
|
||
(hd : decide (start.val < stop.val) = true) (hwok : start + 1#u32 = ok w) :
|
||
verify_mono.chain_free_loop.body pk_seed { start := start, «end» := stop } adrs tmp
|
||
= (do
|
||
let adrs1 ← helpers.Adrs.set_hash_address adrs start
|
||
let s ← lift (Array.to_slice tmp)
|
||
let tmp1 ← verify_mono.oracle.f N pk_seed adrs1 s
|
||
ok (cont (({ start := w, «end» := stop } : core.ops.range.Range Std.U32), adrs1, tmp1))) := by
|
||
unfold verify_mono.chain_free_loop.body
|
||
rw [hnext hd hwok]
|
||
simp
|
||
|
||
/-- The mathematical chaining fold: at each step set the hash address to the
|
||
current index, hash, advance the index (monadically, matching the u32
|
||
range iterator). Recursion on the step count. Agreement with the extracted
|
||
loop holds under `start.val + s < 2^32`, which makes every increment
|
||
succeed. -/
|
||
noncomputable def chainFoldN {N : Std.Usize} (pk_seed : Slice Std.U8) :
|
||
types.Adrs → Array Std.U8 N → Std.U32 → Nat → Result (Array Std.U8 N)
|
||
| _, tmp, _, 0 => ok tmp
|
||
| adrs, tmp, start, (k+1) => do
|
||
let adrs1 ← helpers.Adrs.set_hash_address adrs start
|
||
let s ← lift (Array.to_slice tmp)
|
||
let tmp1 ← verify_mono.oracle.f N pk_seed adrs1 s
|
||
let start1 ← start + 1#u32
|
||
chainFoldN pk_seed adrs1 tmp1 start1 k
|
||
|
||
/-- One full loop step on a non-empty range = one fold step, tail as the
|
||
continuation loop. -/
|
||
theorem chain_step {N : Std.Usize} (pk_seed : Slice Std.U8) (start stop : Std.U32)
|
||
(adrs : types.Adrs) (tmp : Array Std.U8 N)
|
||
(hlt : start.val < stop.val) (hb : start.val + 1 < 2 ^ 32) :
|
||
verify_mono.chain_free_loop { start := start, «end» := stop } pk_seed adrs tmp
|
||
= (do
|
||
let adrs1 ← helpers.Adrs.set_hash_address adrs start
|
||
let s ← lift (Array.to_slice tmp)
|
||
let tmp1 ← verify_mono.oracle.f N pk_seed adrs1 s
|
||
let start1 ← start + 1#u32
|
||
verify_mono.chain_free_loop { start := start1, «end» := stop } pk_seed adrs1 tmp1) := by
|
||
obtain ⟨w, hwok, _⟩ := u32_succ hb
|
||
have hd : decide (start.val < stop.val) = true := by simp [hlt]
|
||
conv_lhs => rw [verify_mono.chain_free_loop, loop_unfold_bind]
|
||
dsimp only
|
||
rw [hbody pk_seed start stop w adrs tmp hd hwok]
|
||
simp only [bind_assoc, bind_ok]
|
||
conv_rhs => rw [show (start + 1#u32) = ok w from hwok]
|
||
simp only [bind_tc_ok, bind_ok]
|
||
rfl
|
||
|
||
/-- **Algorithm 5 fidelity.** The extracted chain loop over [start, start+s)
|
||
equals the explicit s-fold hash-chain. -/
|
||
theorem chain_free_loop_eq {N : Std.Usize} (pk_seed : Slice Std.U8) (s : Nat) :
|
||
∀ (start : Std.U32) (adrs : types.Adrs) (tmp : Array Std.U8 N),
|
||
start.val + s < 2 ^ 32 →
|
||
∀ (stop : Std.U32), stop.val = start.val + s →
|
||
verify_mono.chain_free_loop { start := start, «end» := stop } pk_seed adrs tmp
|
||
= chainFoldN pk_seed adrs tmp start s := by
|
||
induction s with
|
||
| zero =>
|
||
intro start adrs tmp _ stop hstop
|
||
have hse : start = stop := by apply Std.UScalar.eq_of_val_eq; omega
|
||
subst hse
|
||
unfold verify_mono.chain_free_loop chainFoldN
|
||
rw [loop.eq_1]
|
||
unfold verify_mono.chain_free_loop.body core.iter.range.IteratorRange.next
|
||
simp [core.cmp.impls.PartialOrdU32.lt]
|
||
| succ k ih =>
|
||
intro start adrs tmp hb stop hstop
|
||
have hlt : start.val < stop.val := by omega
|
||
have hb1 : start.val + 1 < 2 ^ 32 := by omega
|
||
obtain ⟨w, hwok, hwv⟩ := u32_succ hb1
|
||
rw [chain_step pk_seed start stop adrs tmp hlt hb1]
|
||
unfold chainFoldN
|
||
rw [hwok]
|
||
simp only [bind_tc_ok, bind_ok]
|
||
have hbound : w.val + k < 2 ^ 32 := by omega
|
||
have hstop' : stop.val = w.val + k := by omega
|
||
apply bind_congr; intro adrs1
|
||
apply bind_congr; intro s
|
||
apply bind_congr; intro tmp1
|
||
exact ih w adrs1 tmp1 hbound stop hstop'
|
||
|
||
end fips205
|