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-4 gate was closed at the EXPLOITS, not the MECHANISMS: it bound the manifest rows while the things he moved sat outside the binding. Three fail-opens, each executed end-to-end with the button printing ALL GREEN. No theorem, proof, fold, or model body changed. NEW-1 — the policy constant was unbound. `manifestFingerprint` covered `manifest` but never `allowedBoundary`, the sole predicate the enumeration tests against; adding one name re-opened the False-proof with the committed fingerprint BYTE-IDENTICAL. NEW-2 — the specification bodies were unbound. The statement fingerprint pinned each certificate's TYPE, which names its reference fold; redefining that fold to *be* the extracted loop left cone and type-hash identical while the certificate degenerated to `loop = loop`. 100% of the fidelity content lives in those defs. NEW-3 — the harness was unbound. Stubbing verification/lean-guard alone, without touching check.sh or the proofs, gave ALL GREEN in 3.6s over destroyed proofs. FIX — one mechanism rather than three patches. Audit.lean now emits a canonical AUDIT-MANIFEST block and check.sh binds to its SHA-256. The block covers the POLICY constants, every certificate's fully-elaborated statement, and every specification constant transitively reachable from those statements with its fully-elaborated BODY (41 constants; the closure is computed, so a new fold cannot appear without moving the digest; Prop-valued constants contribute their statement, by proof irrelevance). This also retires the 32-bit Expr.hash as the binding (NEW-5) — it survives only as a per-certificate diagnostic. Enumeration now covers EVERY declaration kind (a `def : False` passed before) in the eight certificate modules AND in Audit.lean itself — the auditor is no longer exempt (round-5 R1). A bare `axiom` in audited scope is now an error. Phase 0 purges stale .olean (the verdict must depend on committed bytes, not .gitignored build state — NEW-4), forbids any .lean outside gen/ and Proofs/, and sha256-pins the four model files AND lean-guard. lean-guard is KEPT rather than removed (the reviewer's portability advice is declined by operator decision): it is the memory cap and machine-wide lock that protect the build machine after a 12.2GB OOM took the host down. That trade-off is documented. check.sh's "Certificates proven:" line now comes from the audited manifest; the hand-kept CERTS array — the one authoritative claim string nothing bound — is deleted. check-selftest.sh: 14 attacks, all rejected, plus a check that the hashed block literally carries the twelve fold bodies. Attacks 9-14 are the reviewers' and an independent drill's own exploits, turned into regression tests. DOCS. TRUSTED-BASE gains item 11 (the REAL trusted computing base — lean-guard pinned; check.sh, the toolchain env, $AENEAS_HOME, python3 and Lean still trusted) and item 12 (the apex does not compose the ten). README: the audit description rewritten; the XMSS sibling-order claim downgraded from "pins" to "makes visible", with a new blanket non-claim covering all ten loop certificates; the de-plumbing file claim corrected (round 1 touched only verify_mono.rs, round 2 only helpers.rs — which is ON the deployed verify AND sign paths, now disclosed; wots.rs was never patched). RECORDED-RUN: three lines that stood inside a fence were a hand-written summary, not console output — fabricated evidence in the file whose purpose is machine evidence. They are removed and the fabrication is named in place, together with the correction that the "INDEPENDENT RUN" block predates this gate. New rule: nothing goes in a fence unless captured with tee/cat, and every block states its date, pin, and who ran it. The transcripts added here follow it. Also disclosed rather than buried: three bugs in my own test harness this round (an olean-purge build-order break, an attack rejected by the wrong rule, and a coverage assertion looking on the wrong line) — each would have let an attack pass or fail for an unrelated reason. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
241 lines
13 KiB
Text
241 lines
13 KiB
Text
/- ──────────────────────────────────────────────────────────────────────────────
|
||
Proofs/Audit.lean — the axiom / statement / specification / coverage audit.
|
||
|
||
ROUND 5 (third reviewer, 2026-07-27) demonstrated that the round-4 gate was
|
||
closed at the EXPLOITS but not at the MECHANISMS. Three fail-opens, each
|
||
executed end-to-end with the button printing ALL GREEN:
|
||
|
||
NEW-1 `manifestFingerprint` covered `manifest` only — never
|
||
`allowedBoundary`, the sole predicate the enumeration tests against.
|
||
Adding one name to that list re-opened the `False`-proof completely,
|
||
with the committed fingerprint BYTE-IDENTICAL.
|
||
NEW-2 the statement fingerprint pinned each certificate's TYPE, which
|
||
references its hand-written reference fold BY NAME. Redefining that
|
||
fold to *be* the extracted loop left cone and type-hash identical
|
||
while the certificate degenerated to "the loop equals the loop" —
|
||
100% of the fidelity content lives in those definitions.
|
||
(drill) layer-2 enumeration matched `.thmInfo` only, so a `def : False`
|
||
passed; and this file's own declarations were enumerated by nothing.
|
||
|
||
The fix binds the WHOLE audited object, not the parts an attacker happened to
|
||
touch. This file emits a canonical AUDIT-MANIFEST block covering:
|
||
|
||
· the POLICY constants (`allowedBoundary`, `certModules`) — NEW-1;
|
||
· every certificate's fully-elaborated STATEMENT (`pp.all`) — F2 properly;
|
||
· every hand-written SPECIFICATION constant transitively reachable from
|
||
those statements, with its fully-elaborated DEFINITION BODY — NEW-2.
|
||
(Proof-valued constants contribute their statement, not their proof term:
|
||
proof irrelevance makes the term semantically immaterial.)
|
||
|
||
`check.sh` binds to the **SHA-256** of that block. This also retires the
|
||
32-bit `Expr.hash` as the load-bearing digest (NEW-5): the per-certificate
|
||
type hashes below remain, but only as fast, human-legible DIAGNOSTICS that
|
||
name which certificate moved — the cryptographic binding is the SHA-256.
|
||
|
||
Enumeration now covers EVERY declaration kind (`def`, `theorem`, `opaque`,
|
||
`axiom`, …) in the eight certificate modules AND every declaration of this
|
||
file itself (the auditor is no longer exempt from its own audit — round-5 R1).
|
||
|
||
Standing limit, stated accurately (see TRUSTED-BASE.md item 11 for the full
|
||
trusted computing base): an audit executed by a harness cannot defend against
|
||
an author who edits that harness. `verification/lean-guard` is therefore
|
||
sha256-pinned by check.sh Phase 0, but `check.sh` itself, the toolchain env,
|
||
and `$AENEAS_HOME` remain trusted. The consumer defense is, and always was,
|
||
the pinned commit reviewed at the pin.
|
||
────────────────────────────────────────────────────────────────────────────── -/
|
||
import Proofs.ChainSpec
|
||
import Proofs.WotsSpec
|
||
import Proofs.XmssSpec
|
||
import Proofs.HtSpec
|
||
import Proofs.ForsInnerSpec
|
||
import Proofs.ForsOuterSpec
|
||
import Proofs.InputPrepSpec
|
||
import Proofs.ApexSpec
|
||
open Lean Elab Command
|
||
|
||
namespace SlhVerify.Audit
|
||
|
||
/-- Lean's three kernel axioms. -/
|
||
def kernel3 : List Name := [`propext, `Classical.choice, `Quot.sound]
|
||
|
||
def oracleF : Name := `verify_mono.oracle.f
|
||
def oracleH : Name := `verify_mono.oracle.h
|
||
def oracleTL : Name := `verify_mono.oracle.t_l
|
||
def oracleTLen : Name := `verify_mono.oracle.t_len
|
||
def oracleHMsg : Name := `verify_mono.oracle.h_msg
|
||
|
||
/-- The ONLY axioms permitted in any cone anywhere in the audited modules.
|
||
POLICY CONSTANT — folded into the SHA-256 digest (round-5 NEW-1): widening
|
||
this list changes the digest and fails the build. -/
|
||
def allowedBoundary : List Name :=
|
||
kernel3 ++ [oracleF, oracleH, oracleTL, oracleTLen, oracleHMsg]
|
||
|
||
/-- The eight certificate-bearing proof modules. POLICY CONSTANT — also folded
|
||
into the digest. (It is additionally cross-checked by `certsSeen` below:
|
||
every manifest certificate must be found inside one of these modules.) -/
|
||
def certModules : List Name :=
|
||
[`Proofs.ChainSpec, `Proofs.WotsSpec, `Proofs.XmssSpec, `Proofs.HtSpec,
|
||
`Proofs.ForsInnerSpec, `Proofs.ForsOuterSpec, `Proofs.InputPrepSpec, `Proofs.ApexSpec]
|
||
|
||
/-- Per-certificate expected cone + a 32-bit `Expr.hash` of the elaborated
|
||
statement. The hash is a DIAGNOSTIC ONLY (it names which certificate moved);
|
||
the binding digest is the SHA-256 over the canonical block. -/
|
||
def manifest : List (Name × List Name × UInt64) :=
|
||
[ (`fips205.chain_free_loop_eq, kernel3 ++ [oracleF], 2535491171),
|
||
(`fips205.wots_loop1_eq, kernel3 ++ [oracleF], 2968777228),
|
||
(`fips205.xmss_loop_eq, kernel3 ++ [oracleH], 763493610),
|
||
(`fips205.ht_loop_eq, kernel3 ++ [oracleF, oracleH, oracleTL], 1071622136),
|
||
(`fips205.fors_inner_loop_eq, kernel3 ++ [oracleH], 2102092697),
|
||
(`fips205.fors_outer_loop_eq, kernel3 ++ [oracleF, oracleH], 2749583942),
|
||
(`fips205.to_int_loop_eq, kernel3, 3797401829),
|
||
(`fips205.to_byte_loop_eq, kernel3, 3618049364),
|
||
(`fips205.wots_csum_loop_eq, kernel3, 4245103433),
|
||
(`fips205.base2b_outer_loop_eq, kernel3, 324621577),
|
||
(`fips205.slh_verify_128s_accepts_iff, kernel3 ++ [oracleF, oracleH, oracleTL, oracleTLen, oracleHMsg], 2489587792) ]
|
||
|
||
/-- Deterministic name ordering for the canonical serialization. -/
|
||
def sortNames (l : List Name) : List Name :=
|
||
((l.map toString).toArray.qsort (· < ·)).toList.map (·.toName)
|
||
|
||
/-- Whitespace-canonical: every whitespace run collapses to one space, so the
|
||
pretty-printer's line wrapping cannot perturb the digest. -/
|
||
def normWs (s : String) : String :=
|
||
(s.foldl (fun (acc : String × Bool) c =>
|
||
let c := if c.isWhitespace then ' ' else c
|
||
if c == ' ' then (if acc.2 then acc else (acc.1.push ' ', true))
|
||
else (acc.1.push c, false))
|
||
("", true)).1
|
||
|
||
/-- Is `n` a hand-written declaration living in one of the certificate modules?
|
||
These are the SPECIFICATION side — the folds the certificates are stated
|
||
against — as opposed to the extracted model in `gen/` (pinned by Phase 0). -/
|
||
def isSpecConst (env : Environment) (n : Name) : Bool :=
|
||
match env.getModuleIdxFor? n with
|
||
| some idx => certModules.contains env.header.moduleNames[idx.toNat]!
|
||
| none => false
|
||
|
||
/-- Transitive closure over specification constants, starting from a
|
||
certificate's STATEMENT and following DEFINITION bodies (a theorem
|
||
contributes its statement only). This discovers the reference folds — and
|
||
any future one — automatically, so a new specification definition cannot be
|
||
introduced without moving the digest. -/
|
||
partial def closureOf (env : Environment) (seen : NameSet) (work : List Name) : NameSet :=
|
||
match work with
|
||
| [] => seen
|
||
| n :: rest =>
|
||
if seen.contains n || !isSpecConst env n then closureOf env seen rest
|
||
else
|
||
let seen := seen.insert n
|
||
let more := match env.find? n with
|
||
| some (.defnInfo v) => v.value.getUsedConstants.toList ++ v.type.getUsedConstants.toList
|
||
| some ci => ci.type.getUsedConstants.toList
|
||
| none => []
|
||
closureOf env seen (more ++ rest)
|
||
|
||
/-- Fully-explicit (`pp.all`) rendering, whitespace-canonicalized. -/
|
||
def ppAll (e : Expr) : CommandElabM String := do
|
||
let s ← Command.liftCoreM <| Meta.MetaM.run' <|
|
||
withOptions (fun o => o.setBool `pp.all true) do
|
||
return (← Meta.ppExpr e).pretty
|
||
return normWs s
|
||
|
||
elab "auditCones" : command => do
|
||
let env ← getEnv
|
||
let mut errs : Array String := #[]
|
||
|
||
-- (0) the manifest's expected cones may reference nothing outside the policy.
|
||
for (cert, cone, _) in manifest do
|
||
for a in cone do
|
||
unless allowedBoundary.contains a do
|
||
throwError "manifest references non-boundary axiom {a} for {cert}"
|
||
|
||
-- (1) NAMED CERTS: exists ∧ is a theorem ∧ exact cone ∧ statement diagnostic.
|
||
for (cert, expected, expFp) in manifest do
|
||
match env.find? cert with
|
||
| none => errs := errs.push s!"{cert}: NOT FOUND (renamed/deleted?)"
|
||
| some (.thmInfo ci) =>
|
||
let got := (← collectAxioms cert).toList
|
||
let extras := got.filter (fun a => !expected.contains a)
|
||
let missing := expected.filter (fun a => !got.contains a)
|
||
unless extras.isEmpty && missing.isEmpty do
|
||
errs := errs.push s!"{cert}: cone extra={extras} missing={missing}"
|
||
unless ci.type.hash == expFp do
|
||
errs := errs.push s!"{cert}: STATEMENT fingerprint {ci.type.hash} ≠ committed {expFp} (statement changed?)"
|
||
| some (.axiomInfo _) => errs := errs.push s!"{cert}: is an AXIOM, not a proven theorem"
|
||
| some (.opaqueInfo _) => errs := errs.push s!"{cert}: is OPAQUE, not a proven theorem"
|
||
| some _ => errs := errs.push s!"{cert}: not a theorem"
|
||
|
||
-- (2) ENUMERATION over EVERY declaration kind — not just theorems (a
|
||
-- `def : False` passed the round-4 gate) — in the eight certificate
|
||
-- modules, AND over this file's own declarations (module index is `none`
|
||
-- during its own elaboration), so the auditor is not exempt (round-5 R1).
|
||
let manifestNames := manifest.map (·.1)
|
||
let mut nEnum := 0
|
||
let mut certsSeen : Array Name := #[]
|
||
for (nm, ci) in env.constants.toList do
|
||
let scope : Option String :=
|
||
match env.getModuleIdxFor? nm with
|
||
| some idx =>
|
||
let m := env.header.moduleNames[idx.toNat]!
|
||
if certModules.contains m then some (toString m) else none
|
||
| none => if nm.isInternal then none else some "Proofs.Audit (this file)"
|
||
match scope with
|
||
| none => pure ()
|
||
| some where_ =>
|
||
nEnum := nEnum + 1
|
||
if manifestNames.contains nm then certsSeen := certsSeen.push nm
|
||
match ci with
|
||
| .axiomInfo _ =>
|
||
-- the five oracle axioms live in gen/ (Phase-0 pinned); an axiom
|
||
-- DECLARED inside an audited module is never acceptable.
|
||
errs := errs.push s!"AXIOM DECLARED in audited scope: {nm} ({where_})"
|
||
| _ =>
|
||
let cone := (← collectAxioms nm).toList
|
||
let bad := cone.filter (fun a => !allowedBoundary.contains a)
|
||
unless bad.isEmpty do
|
||
errs := errs.push s!"UN-AUDITED declaration {nm} ({where_}) has disallowed axioms {bad}"
|
||
for cert in manifestNames do
|
||
unless certsSeen.contains cert do
|
||
errs := errs.push s!"manifest cert {cert} not found in any certificate module"
|
||
|
||
unless errs.isEmpty do
|
||
throwError "AUDIT FAILED (fail-closed):\n{String.intercalate "\n" errs.toList}"
|
||
|
||
-- (3) CANONICAL BLOCK: policy + statements + specification bodies. check.sh
|
||
-- binds to the SHA-256 of everything between the markers.
|
||
let mut lines : Array String := #[]
|
||
lines := lines.push
|
||
s!"policy|allowedBoundary={String.intercalate "," ((sortNames allowedBoundary).map toString)}|certModules={String.intercalate "," ((sortNames certModules).map toString)}"
|
||
let mut specs : NameSet := {}
|
||
for (cert, cone, _) in manifest do
|
||
let ci := (env.find? cert).get!
|
||
specs := (closureOf env {} ci.type.getUsedConstants.toList).toList.foldl (·.insert ·) specs
|
||
lines := lines.push
|
||
s!"cert|{cert}|cone={String.intercalate "," ((sortNames cone).map toString)}|type={← ppAll ci.type}"
|
||
for nm in sortNames specs.toList do
|
||
match env.find? nm with
|
||
| none => errs := errs.push s!"specification constant vanished: {nm}"
|
||
| some ci =>
|
||
let isProp ← Command.liftCoreM <| Meta.MetaM.run' <| Meta.isProp ci.type
|
||
-- proof irrelevance: a Prop-valued constant contributes its STATEMENT;
|
||
-- a data definition contributes its BODY — that is where fidelity lives.
|
||
if isProp then
|
||
lines := lines.push s!"spec|{nm}|prop|type={← ppAll ci.type}"
|
||
else
|
||
match ci with
|
||
| .defnInfo v => lines := lines.push s!"spec|{nm}|def|value={← ppAll v.value}"
|
||
| _ => lines := lines.push s!"spec|{nm}|other|type={← ppAll ci.type}"
|
||
unless errs.isEmpty do
|
||
throwError "AUDIT FAILED (fail-closed):\n{String.intercalate "\n" errs.toList}"
|
||
|
||
logInfo ("AUDIT-MANIFEST-BEGIN\n" ++ String.intercalate "\n" lines.toList ++ "\nAUDIT-MANIFEST-END")
|
||
-- check.sh prints the certificate list it gets from HERE, not from a hand-kept
|
||
-- bash array (drill finding: the one authoritative claim string was the one
|
||
-- thing nothing bound — adding a name to it printed a cert that never existed).
|
||
logInfo s!"CERTIFICATES: {String.intercalate " " (manifestNames.map toString)}"
|
||
logInfo s!"exact-cone audit PASSED: {manifest.length} certificates (cones + statements), {specs.toList.length} specification constants pinned, {nEnum} declarations enumerated clean"
|
||
|
||
end SlhVerify.Audit
|
||
|
||
open SlhVerify.Audit in
|
||
auditCones
|