mirror of
https://github.com/saymrwulf/fips205-slhdsa-verified.git
synced 2026-09-04 20:03:44 +00:00
The third reviewer demonstrated that the round-2 in-Lean exact-cone audit, though sound for LISTED certs, left three fail-opens OUTSIDE the cone check — and made check.sh print ALL GREEN over a repo proving False. All closed; no theorem, proof, or fold changed (the 11 cones are unchanged). F1 — the audited SET was unbound. Audit.lean now (a) enumerates EVERY theorem defined in the eight certificate modules and requires each cone ⊆ boundary, so an un-manifested `theorem _ : False := cheat _` fails regardless of naming (this is the exact exploit the reviewer used); and (b) prints a MANIFEST fingerprint over the whole committed manifest, which check.sh binds to — so deleting/swapping a cert row fails outside Lean too. F2 — only cones were bound, not statements. Each cert now also carries the structural fingerprint (Expr.hash) of its elaborated type; a statement gutted to a tautology of the same cone changes the fingerprint and fails. F3 — the gen/ model bytes were unbound. New check.sh Phase 0 sha256-pins all four gen/SlhVerify/*.lean (incl. the two hand-maintained *External files, now hashed in PROVENANCE.json) BEFORE compiling; a hand-edited model fails first. F4/F5 — docs. README cone diagram now roots honestly at slh_verify_internal and states the pure/prehash domain-separator byte, the ctx>255 check, M' assembly, and deserialization are ABOVE the root and uncovered (new TRUSTED-BASE item 10). The false "rules out a wrong ADRS field" claim is corrected in README + ChainSpec (a transliteration makes the field visible, not excluded). check-selftest.sh: eight attacks, all rejected (dead file; extra axiom; dropped oracle; vanished cert; un-manifested False theorem; gutted statement; hand-edited model; deleted manifest row). Full transcript + green check.sh in verification/RECORDED-RUN.md. Standing limit unchanged and disclosed: an audit cannot defend against an author who edits the manifest AND check.sh AND the proofs together; the consumer defense is the pinned commit reviewed at the pin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
164 lines
7.6 KiB
Text
164 lines
7.6 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 and wrong state
|
||
threading. It does NOT rule out a wrong ADRS field: `chainFoldN` calls the
|
||
same extracted `set_hash_address` the loop does, so a wrong field would be
|
||
copied into the fold and the theorem would still hold — the field is made
|
||
VISIBLE in the certificate (a transliteration), not excluded by it. 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
|