review round 4: bind the cert set, statements, and model bytes (F1/F2/F3)

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>
This commit is contained in:
mrwulf 2026-07-27 19:47:39 +02:00
parent 337e5e9481
commit 45a2f65a2d
8 changed files with 336 additions and 154 deletions

View file

@ -62,7 +62,14 @@ The trust base and residual assumptions are stated in
extracted `chain_free` loop equals the explicit s-fold hash chain, with extracted `chain_free` loop equals the explicit s-fold hash chain, with
the hash address set to i, i+1, …, i+s1 in turn. This rules out — the hash address set to i, i+1, …, i+s1 in turn. This rules out —
machine-checked, for the monomorphic SHA2-128s `verify_mono` path — an machine-checked, for the monomorphic SHA2-128s `verify_mono` path — an
off-by-one loop bound, a wrong address field, and wrong threading. Its off-by-one loop bound and wrong state threading. (It does **not** rule out
a wrong ADRS field: the reference fold is built from the same extracted
`set_hash_address` primitive the loop calls, so a wrong field would be
faithfully copied into the fold and the theorem would still hold. What the
certificate pins is what the extracted code does at each index, so a wrong
field is *visible* in the certificate, not *excluded* by it — the mapping
onto FIPS 205 Alg 5 is a human reading step, consistent with "the folds are
transliterations of the extracted loops" above.) Its
`#print axioms` cone is **exactly** `[propext, Classical.choice, `#print axioms` cone is **exactly** `[propext, Classical.choice,
Quot.sound, verify_mono.oracle.f]` — the three kernel axioms plus the one Quot.sound, verify_mono.oracle.f]` — the three kernel axioms plus the one
hash oracle it touches, and nothing else (no transpiler plumbing; the u32 hash oracle it touches, and nothing else (no transpiler plumbing; the u32
@ -170,15 +177,24 @@ same boundary.
## Scope ## Scope
**Verify path only.** The extraction cone, mirroring FIPS 205's own **Verify path only, rooted at `slh_verify_internal`.** The extraction root
algorithm tree: is `verify_mono::slh_verify_128s`, which is `slh_verify_internal_free(M, sig,
pk)` — it takes the already-assembled message digest input **M as an
argument**. So the covered cone is:
``` ```
slh_verify -> slh_verify_internal slh_verify_internal(M, …) ← the extraction ROOT (M is an input)
-> fors_pk_from_sig -> fors_pk_from_sig
-> ht_verify -> xmss_pk_from_sig -> wots_pk_from_sig -> chain -> ht_verify -> xmss_pk_from_sig -> wots_pk_from_sig -> chain
``` ```
Everything **above** this root, in `slh_verify`/`verify` (`src/lib.rs`), is
OUT of scope and is stated as such in [TRUSTED-BASE.md](TRUSTED-BASE.md): M
assembly, the pure-vs-prehash **domain-separator byte** (`0u8` for `verify`,
`1u8` for `hash_verify` — the entire cross-variant separation), the
`ctx.len() > 255` check, and signature/public-key deserialization. A reader
must NOT read `slh_verify -> slh_verify_internal` as "the top of the verify
path is covered" — it is not; the top-of-path input handling is trusted base.
Key generation and signing are out of scope (trusted base), exactly as Key generation and signing are out of scope (trusted base), exactly as
ed25519 signing was. The five verify-path hash oracles (`h_msg, f, h, ed25519 signing was. The five verify-path hash oracles (`h_msg, f, h,
t_l, t_len` — SHA-2 instantiations; `prf`/`prf_msg` are sign-side only t_l, t_len` — SHA-2 instantiations; `prf`/`prf_msg` are sign-side only

View file

@ -45,3 +45,13 @@ proceeds and is part of every claim.
`verify_mono` facade. The bridge to upstream's generic `pk.verify()` is `verify_mono` facade. The bridge to upstream's generic `pk.verify()` is
the finite in-snapshot differential test, not a machine-checked the finite in-snapshot differential test, not a machine-checked
refinement. refinement.
10. **Everything above the extraction root.** The root is
`verify_mono::slh_verify_128s = slh_verify_internal_free(M, sig, pk)`,
which takes the message-digest input **M as an argument**. The code in
`slh_verify`/`verify` (`src/lib.rs`) that runs *before* this root is NOT
covered by any certificate: the assembly of M; the pure-vs-prehash
**domain-separator byte** (`0u8` for `verify` vs `1u8` for `hash_verify`
— the whole cross-variant domain separation); the FIPS-205 `ctx.len() >
255` bound; and signature/public-key deserialization. The certificates
say nothing about this input handling — a defect there (e.g. a wrong
separator byte) would be outside every proof.

View file

@ -32,5 +32,12 @@
"gen/SlhVerify/FunsExternal.lean" "gen/SlhVerify/FunsExternal.lean"
], ],
"byte_identical_regeneration_verified": "2026-07-24", "byte_identical_regeneration_verified": "2026-07-24",
"audit_gate": "verification/Proofs/Audit.lean — in-Lean exact per-certificate cone equality via collectAxioms (fail-closed); compiled by check.sh Phase 3; adversarially exercised by check-selftest.sh" "model_integrity_sha256": {
"_comment": "All four Lean model files check.sh Phase 0 pins before compiling anything — the two aeneas-generated files AND the two hand-maintained *External files (which carry the five oracle axioms and the Step defs). A hand-edit of the model fails the build here (round-4 reviewer F3).",
"gen/SlhVerify/TypesExternal.lean": "37958beb7c90b649497a20b0802f98f0fb2d975c245eca956d7396af74d7759f",
"gen/SlhVerify/FunsExternal.lean": "73fa2a7d14b35bb6b6746534e88cba527d0e17f154f7a7d841a9c519befeea47",
"gen/SlhVerify/Types.lean": "db720b4a30f512e6048212a472e6853b24931a8121cb94c4cf7e6489754d6384",
"gen/SlhVerify/Funs.lean": "7b7de55fd0206142f2678a079a6ed4462292356bc7de08ecd55cac0c76a1da9f"
},
"audit_gate": "verification/Proofs/Audit.lean — in-Lean audit (round 4): per-certificate exact cone equality (collectAxioms) + elaborated-statement fingerprint (Expr.hash) + full enumeration of every theorem in the eight certificate modules (each cone ⊆ boundary, so an un-manifested theorem cannot smuggle an axiom) + a manifest fingerprint check.sh binds to. Fail-closed; adversarially exercised by check-selftest.sh (attacks 16). check.sh Phase 0 pins the model bytes to model_integrity_sha256 above."
} }

View file

@ -1,23 +1,37 @@
/- ────────────────────────────────────────────────────────────────────────────── /- ──────────────────────────────────────────────────────────────────────────────
Proofs/Audit.lean — the axiom-cone audit, performed INSIDE Lean. Proofs/Audit.lean — the axiom-cone + statement + coverage audit, in Lean.
Round-2 external review (2026-07-24) showed the bash `#print axioms` text History: round 2 moved the axiom check inside Lean (collectAxioms, exact
parser was still fragile: it fail-OPENED on an empty `[]` or a truncated per-cert set equality). Round 4 (third reviewer, 2026-07-24) demonstrated
(missing-`]`) report, and it only subset-checked (a *removed* oracle that exact-cone-per-cert, while sound FOR LISTED certs, left three gaps
dependency would pass unnoticed). This file removes text parsing entirely. OUTSIDE the cone check — all fail-open:
F1 the audited SET was unbound: deleting a manifest row silently drops a
cert, and adding an un-manifested theorem (e.g. `: False := cheat _`)
was never looked at → the repo could prove False under ALL GREEN.
F2 only cones were bound, never STATEMENTS: a certificate whose type was
replaced by a tautology of the same cone passed green.
F3 (handled in check.sh) the gen/ model bytes were unbound to provenance.
`collectAxioms` reads the kernel's own axiom set for each certificate. We This file now closes F1 and F2 with three layers:
assert, for every one of the eleven: (1) NAMED CERTS — each of the eleven: exists ∧ is a `theorem` ∧ cone ==
its expected set EXACTLY ∧ its elaborated-type structural fingerprint
(`Expr.hash`) == the committed value. A gutted statement changes the
fingerprint → fail (F2).
(2) FULL-MODULE ENUMERATION — EVERY theorem defined in the eight
certificate modules must have a cone ⊆ the allowed boundary. An
un-manifested `: False := cheat _` has cone {cheat} ⊄ boundary → fail,
whether or not anyone "listed" it (F1, the dangerous half).
(3) MANIFEST FINGERPRINT — a hash over the whole committed manifest
(names + cones + type fingerprints) is printed; check.sh binds to the
exact committed value, so deleting/swapping a row, or editing a cone or
a fingerprint, changes it and fails the build outside Lean too (F1, the
set-binding half).
· the certificate EXISTS and is a `theorem` (not an axiom/opaque sham, Any mismatch is a `throwError` → non-zero `lean` exit. Nothing fails open for
not a renamed/deleted name — `collectAxioms` returns `#[]` for a a listed cert, an unlisted theorem, a gutted statement, or a dropped oracle.
missing name, so existence is checked explicitly, fail-closed); The standing limit (unchanged, disclosed): an audit cannot defend against an
· its cone equals its EXPECTED set EXACTLY — extras (a smuggled axiom) author who edits the manifest AND check.sh AND the proofs together; the
AND missing (a silently dropped oracle dependency) both fail. consumer defense is the pinned commit reviewed at the pin.
Any mismatch is a `throwError`, i.e. a Lean elaboration error → non-zero
`lean` exit. There is no text to misparse and nothing fails open. This is
the single source of the axiom claim; check.sh Phase 3 just compiles it.
────────────────────────────────────────────────────────────────────────────── -/ ────────────────────────────────────────────────────────────────────────────── -/
import Proofs.ChainSpec import Proofs.ChainSpec
import Proofs.WotsSpec import Proofs.WotsSpec
@ -31,65 +45,104 @@ open Lean Elab Command
namespace SlhVerify.Audit namespace SlhVerify.Audit
/-- Lean's three kernel axioms — permitted in every cone. -/ /-- Lean's three kernel axioms. -/
def kernel3 : List Name := [`propext, `Classical.choice, `Quot.sound] def kernel3 : List Name := [`propext, `Classical.choice, `Quot.sound]
/-- The five SHA-2 verify-path hash oracles — the documented cryptographic
boundary (TRUSTED-BASE.md). No other axiom may appear anywhere. -/
def oracleF : Name := `verify_mono.oracle.f def oracleF : Name := `verify_mono.oracle.f
def oracleH : Name := `verify_mono.oracle.h def oracleH : Name := `verify_mono.oracle.h
def oracleTL : Name := `verify_mono.oracle.t_l def oracleTL : Name := `verify_mono.oracle.t_l
def oracleTLen : Name := `verify_mono.oracle.t_len def oracleTLen : Name := `verify_mono.oracle.t_len
def oracleHMsg : Name := `verify_mono.oracle.h_msg def oracleHMsg : Name := `verify_mono.oracle.h_msg
/-- The entire allowed boundary: nothing outside this set is permitted in any /-- The only axioms permitted in ANY cone in the certificate modules: kernel-3
certificate cone, and the expected table below may reference nothing else. -/ plus the five SHA-2 oracles. -/
def allowedBoundary : List Name := def allowedBoundary : List Name :=
kernel3 ++ [oracleF, oracleH, oracleTL, oracleTLen, oracleHMsg] kernel3 ++ [oracleF, oracleH, oracleTL, oracleTLen, oracleHMsg]
/-- EXACT expected cone per certificate. Ground truth captured 2026-07-24 via /-- The eight certificate-bearing proof modules. Every `theorem` defined in
`collectAxioms` (Probe.lean) and cross-checked against both round-1 these must have a cone ⊆ allowedBoundary (layer 2). -/
reviewers' independent reconstructions. Each entry is asserted for SET def certModules : List Name :=
EQUALITY, so this table is a load-bearing specification of the boundary: [`Proofs.ChainSpec, `Proofs.WotsSpec, `Proofs.XmssSpec, `Proofs.HtSpec,
changing a proof so it drops an oracle, or adds one, breaks the audit. -/ `Proofs.ForsInnerSpec, `Proofs.ForsOuterSpec, `Proofs.InputPrepSpec, `Proofs.ApexSpec]
def expectedCones : List (Name × List Name) :=
[ (`fips205.chain_free_loop_eq, kernel3 ++ [oracleF]), /-- THE COMMITTED MANIFEST: for each certificate, its exact expected cone and
(`fips205.wots_loop1_eq, kernel3 ++ [oracleF]), the structural fingerprint (`Expr.hash`) of its elaborated statement. Ground
(`fips205.xmss_loop_eq, kernel3 ++ [oracleH]), truth captured 2026-07-27 via Probe.lean; cross-checked against the round-1/2
(`fips205.ht_loop_eq, kernel3 ++ [oracleF, oracleH, oracleTL]), cone reconstructions. Changing a proof (cone) OR a statement (fingerprint)
(`fips205.fors_inner_loop_eq, kernel3 ++ [oracleH]), breaks the audit; adding/removing a row changes the manifest fingerprint that
(`fips205.fors_outer_loop_eq, kernel3 ++ [oracleF, oracleH]), check.sh binds to. -/
(`fips205.to_int_loop_eq, kernel3), def manifest : List (Name × List Name × UInt64) :=
(`fips205.to_byte_loop_eq, kernel3), [ (`fips205.chain_free_loop_eq, kernel3 ++ [oracleF], 2535491171),
(`fips205.wots_csum_loop_eq, kernel3), (`fips205.wots_loop1_eq, kernel3 ++ [oracleF], 2968777228),
(`fips205.base2b_outer_loop_eq, kernel3), (`fips205.xmss_loop_eq, kernel3 ++ [oracleH], 763493610),
(`fips205.slh_verify_128s_accepts_iff, kernel3 ++ [oracleF, oracleH, oracleTL, oracleTLen, oracleHMsg]) ] (`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) ]
/-- Canonical serialization of the committed manifest → one hash. check.sh binds
to the printed value, so any row add/remove/edit, cone change, or fingerprint
change flips it and fails the build. -/
def manifestFingerprint : UInt64 :=
let sortName (l : List Name) : List Name :=
((l.map toString).toArray.qsort (· < ·)).toList.map (·.toName)
let ser := String.intercalate ";" (manifest.map (fun (n, cone, fp) =>
s!"{n}|{String.intercalate "," ((sortName cone).map toString)}|{fp}"))
String.hash ser
elab "auditCones" : command => do elab "auditCones" : command => do
let env ← getEnv let env ← getEnv
-- (0) the expected table itself must stay within the boundary — guards a typo
-- in this file from silently widening what "allowed" means.
for (cert, expected) in expectedCones do
for a in expected do
unless allowedBoundary.contains a do
throwError "audit table references non-boundary axiom {a} for {cert}"
-- (1) per certificate: exists ∧ is a theorem ∧ cone == expected set exactly.
let mut errs : Array String := #[] let mut errs : Array String := #[]
for (cert, expected) in expectedCones do -- (0) the manifest's expected cones may reference nothing outside the boundary
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 ∧ theorem ∧ exact cone ∧ statement fingerprint
for (cert, expected, expFp) in manifest do
match env.find? cert with match env.find? cert with
| none => errs := errs.push s!"{cert}: NOT FOUND (renamed/deleted?)" | none => errs := errs.push s!"{cert}: NOT FOUND (renamed/deleted?)"
| some (.thmInfo _) => | some (.thmInfo ci) =>
let got := (← collectAxioms cert).toList let got := (← collectAxioms cert).toList
let extras := got.filter (fun a => !expected.contains a) let extras := got.filter (fun a => !expected.contains a)
let missing := expected.filter (fun a => !got.contains a) let missing := expected.filter (fun a => !got.contains a)
unless extras.isEmpty && missing.isEmpty do unless extras.isEmpty && missing.isEmpty do
errs := errs.push s!"{cert}: extra={extras} missing={missing}" 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 (.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 (.opaqueInfo _) => errs := errs.push s!"{cert}: is OPAQUE, not a proven theorem"
| some _ => errs := errs.push s!"{cert}: not a theorem" | some _ => errs := errs.push s!"{cert}: not a theorem"
-- (2) FULL-MODULE ENUMERATION: every theorem in a cert module has a clean cone.
-- This is the layer that stops an un-manifested `: False := cheat _`.
let manifestNames := manifest.map (·.1)
let mut nModuleThms := 0
let mut certsSeen : Array Name := #[]
for (nm, ci) in env.constants.toList do
match ci with
| .thmInfo _ =>
match env.getModuleIdxFor? nm with
| some idx =>
if certModules.contains env.header.moduleNames[idx.toNat]! then
nModuleThms := nModuleThms + 1
if manifestNames.contains nm then certsSeen := certsSeen.push nm
let cone := (← collectAxioms nm).toList
let bad := cone.filter (fun a => !allowedBoundary.contains a)
unless bad.isEmpty do
errs := errs.push s!"UN-AUDITED theorem {nm} (module {env.header.moduleNames[idx.toNat]!}) has disallowed axioms {bad}"
| none => pure ()
| _ => pure ()
-- every manifest cert must actually be a theorem found in a cert module
for cert in manifestNames do
unless certsSeen.contains cert do
errs := errs.push s!"manifest cert {cert} not found as a theorem in any certificate module"
unless errs.isEmpty do unless errs.isEmpty do
throwError "EXACT-CONE AUDIT FAILED (fail-closed):\n{String.intercalate "\n" errs.toList}" throwError "AUDIT FAILED (fail-closed):\n{String.intercalate "\n" errs.toList}"
logInfo s!"exact-cone audit PASSED: {expectedCones.length} certificates, each cone == its expected boundary set (kernel-3 + only the named SHA-2 oracles)" logInfo s!"exact-cone audit PASSED: {manifest.length} certificates (cones + statement fingerprints), {nModuleThms} module theorems enumerated clean; MANIFEST-FINGERPRINT: {manifestFingerprint}"
end SlhVerify.Audit end SlhVerify.Audit

View file

@ -4,10 +4,13 @@
explicit s-fold application of the hash F, with the hash-address set to explicit s-fold application of the hash F, with the hash-address set to
i, i+1, …, i+s1 in turn. This rules out — machine-checked, for the i, i+1, …, i+s1 in turn. This rules out — machine-checked, for the
monomorphic SHA2-128s `verify_mono` path (a private facade, not the monomorphic SHA2-128s `verify_mono` path (a private facade, not the
deployed generic verifier) — an off-by-one loop bound, a wrong address deployed generic verifier) — an off-by-one loop bound and wrong state
field, and wrong threading. F stays opaque threading. It does NOT rule out a wrong ADRS field: `chainFoldN` calls the
(verify_mono.oracle.f), so the certificate cone is the three kernel axioms same extracted `set_hash_address` the loop does, so a wrong field would be
+ oracle.f, and nothing else (audited by check.sh Phase 3). 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 = 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 one fold step, proven by unfolding the Aeneas `loop` fixpoint one turn

View file

@ -158,3 +158,32 @@ Done. Now run ./check.sh (Phase 1: the regenerated model must type-check).
db720b4a30f512e6048212a472e6853b24931a8121cb94c4cf7e6489754d6384 gen/SlhVerify/Types.lean db720b4a30f512e6048212a472e6853b24931a8121cb94c4cf7e6489754d6384 gen/SlhVerify/Types.lean
7b7de55fd0206142f2678a079a6ed4462292356bc7de08ecd55cac0c76a1da9f gen/SlhVerify/Funs.lean 7b7de55fd0206142f2678a079a6ed4462292356bc7de08ecd55cac0c76a1da9f gen/SlhVerify/Funs.lean
``` ```
## Round-4 hardening (third reviewer, 2026-07-27)
The third reviewer demonstrated three fail-opens OUTSIDE the cone check
(F1 unbound cert set / un-manifested theorem; F2 statements unbound; F3
model bytes unbound). All fixed. check.sh green with the hardened gate,
and the adversarial self-test now rejects eight attacks including the two
the reviewer used to make the button green over a repo proving False:
```
check.sh: ALL GREEN — MANIFEST-FINGERPRINT: 13660980750615609973
Phase 0 model-byte integrity: 4/4 gen files match PROVENANCE.json
Phase 3 audit: 11 certs (cones + statement fingerprints), 196 module theorems enumerated clean
check-selftest: attacking the gates
====================================
✓ attack 1 rejected (dead-file gate)
✓ attack 2 rejected (extra-axiom detection — evil_ax named)
✓ attack 3 rejected (missing-oracle detection — exact cone, not subset)
✓ attack 4 rejected (existence check — vanished cert cannot pass as 0-axiom)
✓ attack 5 rejected (module enumeration — an un-manifested False theorem cannot pass)
✓ attack 6 rejected (statement fingerprint — a gutted statement of the same cone cannot pass)
✓ attack 7 rejected (Phase 0 model-byte integrity — a hand-edited model cannot compile)
✓ attack 8 rejected (manifest fingerprint — a silently-dropped cert cannot pass)
SELFTEST GREEN: the gate rejects dead files, extra axioms, dropped oracles,
vanished certs, un-manifested False theorems, gutted statements, hand-edited
models, and deleted manifest rows.
```

View file

@ -1,127 +1,155 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Adversarial self-test of the check.sh gates (the R3-5 tradition: an audit that # Adversarial self-test of the check.sh gates (the R3-5 tradition: an audit that
# cannot fail is theater). The axiom audit now runs INSIDE Lean # cannot fail is theater). The audit runs INSIDE Lean (Proofs/Audit.lean); the
# (Proofs/Audit.lean, exact cone per certificate via collectAxioms), so these # model bytes are pinned in Phase 0; the certificate SET and each STATEMENT are
# attacks target that gate's actual guarantees — not the retired text parser. # bound by a manifest fingerprint. Every attack MUST make check.sh fail, via the
# Every attack MUST make check.sh fail, via the intended gate: # intended gate:
# #
# 1. DEAD FILE — a stray Proofs/*.lean not in the manifest. # 1. DEAD FILE — a stray Proofs/*.lean not in the manifest.
# 2. SMUGGLED AXIOM — a certificate whose real cone contains a disallowed # 2. SMUGGLED AXIOM — a listed cert whose real cone contains a disallowed
# axiom, declared clean. Exact-equality must report it # axiom; exact-equality reports extra=[...].
# as `extra=[...]` (the classic extra-axiom detection). # 3. DROPPED ORACLE — a listed cert whose expected cone claims an oracle its
# 3. DROPPED ORACLE — a certificate whose expected cone claims an oracle # proof does not use; exact-equality reports missing=[...]
# its real proof does NOT use. A subset checker would # (a subset checker would pass this).
# pass this; exact-equality must report `missing=[...]`. # 4. VANISHED CERT — a manifest name that no longer resolves; the explicit
# This is the property the round-2 review demanded and # existence check reports NOT FOUND (collectAxioms would
# the retired subset parser could never enforce. # otherwise return [] and pass).
# 4. VANISHED CERT — a certificate name that no longer resolves. Since # 5. UN-AUDITED FALSE — round-4 F1: an UN-manifested `theorem … : False := …`
# `collectAxioms` returns [] for a missing name (a # added to a certificate module. The full-module
# fail-open trap), the audit must report NOT FOUND. # enumeration must flag its disallowed cone. (This is the
# attack that made the pre-round-4 button green over a
# repo proving False.)
# 6. GUTTED STATEMENT — round-4 F2: a certificate's statement replaced by a
# tautology of the SAME cone. The statement fingerprint
# must differ from the committed manifest value.
# 7. MODEL TAMPER — round-4 F3: a hand-edit of a gen/ model file. Phase 0
# must reject it before anything is compiled.
# 8. DELETED ROW — round-4 F1 (set half): a manifest row removed. Audit
# still compiles, but the printed MANIFEST-FINGERPRINT
# changes and check.sh's committed binding must reject it.
# #
# Green here means: the gate genuinely rejects all four. Self-cleaning: the real # Green here means the gate genuinely rejects all eight. Self-cleaning: every
# check.sh / Proofs/Audit.lean are backed up and restored around every attack. # file a step tampers is saved and restored around that step.
set -euo pipefail set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)" HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE" cd "$HERE"
restore() { TOUCHED=()
[ -f check.sh.selftest.bak ] && mv -f check.sh.selftest.bak check.sh save() { for f in "$@"; do cp -f "$f" "$f.sfbak"; TOUCHED+=("$f"); done; }
[ -f Proofs/Audit.lean.selftest.bak ] && mv -f Proofs/Audit.lean.selftest.bak Proofs/Audit.lean restore() { for f in "${TOUCHED[@]}"; do [ -f "$f.sfbak" ] && mv -f "$f.sfbak" "$f"; done; TOUCHED=(); }
return 0
}
cleanup() { cleanup() {
restore restore
rm -f Proofs/Stray.lean Proofs/Stray.olean \ rm -f Proofs/Stray.lean Proofs/Stray.olean Proofs/EvilSpec.lean Proofs/EvilSpec.olean \
Proofs/EvilSpec.lean Proofs/EvilSpec.olean Proofs/Audit.olean Proofs/Audit.olean Proofs/ChainSpec.olean *.sfbak Proofs/*.sfbak gen/SlhVerify/*.sfbak
} }
trap cleanup EXIT trap cleanup EXIT
backup() { cp -f check.sh check.sh.selftest.bak; cp -f Proofs/Audit.lean Proofs/Audit.lean.selftest.bak; }
echo "check-selftest: attacking the gates" echo "check-selftest: attacking the gates"
echo "====================================" echo "===================================="
# ── Attack 1: dead file ───────────────────────────────────────────────────── # ── Attack 1: dead file ─────────────────────────────────────────────────────
echo "-- stray" > Proofs/Stray.lean echo "-- stray" > Proofs/Stray.lean
if ./check.sh > /tmp/selftest-dead.out 2>&1; then if ./check.sh > /tmp/sf1.out 2>&1; then echo "✗ ATTACK 1 SUCCEEDED (dead file stayed green)"; exit 1; fi
echo "✗ ATTACK 1 SUCCEEDED: check.sh stayed green with a dead file"; exit 1 grep -q "DEAD FILE" /tmp/sf1.out || { echo "✗ ATTACK 1: failed, not via dead-file gate"; cat /tmp/sf1.out; exit 1; }
fi
grep -q "DEAD FILE" /tmp/selftest-dead.out \
|| { echo "✗ ATTACK 1: failed, but not via the dead-file gate"; cat /tmp/selftest-dead.out; exit 1; }
rm -f Proofs/Stray.lean Proofs/Stray.olean rm -f Proofs/Stray.lean Proofs/Stray.olean
echo "✓ attack 1 rejected (dead-file gate works)" echo "✓ attack 1 rejected (dead-file gate)"
# ── Attack 2: smuggled disallowed axiom in a real cone ────────────────────── # ── Attack 2: smuggled disallowed axiom in a real cone ──────────────────────
# A new certificate whose cone genuinely contains `evil_ax`, declared as clean save check.sh Proofs/Audit.lean
# (kernel-3) in the expected table. Exact-equality must flag extra=[evil_ax].
backup
cat > Proofs/EvilSpec.lean <<'EOF' cat > Proofs/EvilSpec.lean <<'EOF'
import Proofs.ChainSpec import Proofs.ChainSpec
axiom evil_ax : True axiom evil_ax : True
theorem evil_thm : True := evil_ax theorem evil_thm : True := evil_ax
EOF EOF
python3 - <<'PY' python3 - <<'PY'
import re
# check.sh: add EvilSpec to PROOFS so Phase 2 builds it and the dead-file gate
# passes (inject right after the array's opening paren — no hard-coded contents).
s = open("check.sh").read() s = open("check.sh").read()
assert 'PROOFS=(\n' in s, "check.sh PROOFS array shape changed" assert 'PROOFS=(\n' in s
s = s.replace('PROOFS=(\n', 'PROOFS=(\n "EvilSpec"\n', 1) open("check.sh","w").write(s.replace('PROOFS=(\n', 'PROOFS=(\n "EvilSpec"\n', 1))
open("check.sh","w").write(s)
# Audit.lean: import EvilSpec and claim evil_thm is kernel-3 clean.
a = open("Proofs/Audit.lean").read() a = open("Proofs/Audit.lean").read()
assert 'import Proofs.ApexSpec' in a, "Audit.lean import shape changed" assert 'import Proofs.ApexSpec' in a and ' [ (' in a
a = a.replace('import Proofs.ApexSpec', 'import Proofs.ApexSpec\nimport Proofs.EvilSpec', 1) a = a.replace('import Proofs.ApexSpec', 'import Proofs.ApexSpec\nimport Proofs.EvilSpec', 1)
assert 'def expectedCones : List (Name × List Name) :=' in a and ' [ (' in a, "Audit.lean table shape changed" a = a.replace(' [ (', ' [ (`evil_thm, kernel3, 0),\n (', 1)
a = a.replace(' [ (', ' [ (`evil_thm, kernel3),\n (', 1)
open("Proofs/Audit.lean","w").write(a) open("Proofs/Audit.lean","w").write(a)
PY PY
if ./check.sh > /tmp/selftest-evil.out 2>&1; then if ./check.sh > /tmp/sf2.out 2>&1; then echo "✗ ATTACK 2 SUCCEEDED (smuggled axiom)"; exit 1; fi
echo "✗ ATTACK 2 SUCCEEDED: audit passed a smuggled disallowed axiom"; exit 1 grep -q "evil_ax" /tmp/sf2.out || { echo "✗ ATTACK 2: rejected but evil_ax not named"; cat /tmp/sf2.out; exit 1; }
fi
grep -q "AUDIT FAILED" /tmp/selftest-evil.out \
|| { echo "✗ ATTACK 2: failed, but not via the axiom audit"; cat /tmp/selftest-evil.out; exit 1; }
grep -q "evil_ax" /tmp/selftest-evil.out \
|| { echo "✗ ATTACK 2: rejected, but the audit did not name the smuggled axiom"; cat /tmp/selftest-evil.out; exit 1; }
restore; rm -f Proofs/EvilSpec.lean Proofs/EvilSpec.olean Proofs/Audit.olean restore; rm -f Proofs/EvilSpec.lean Proofs/EvilSpec.olean Proofs/Audit.olean
echo "✓ attack 2 rejected (extra-axiom detection works — evil_ax named)" echo "✓ attack 2 rejected (extra-axiom detection — evil_ax named)"
# ── Attack 3: dropped-oracle (subset would pass; exact must not) ───────────── # ── Attack 3: dropped-oracle (subset would pass; exact must not) ─────────────
# Claim to_int_loop_eq depends on oracle.f. Its real cone is kernel-3 only, so save Proofs/Audit.lean
# the audit must report missing=[verify_mono.oracle.f].
backup
python3 - <<'PY' python3 - <<'PY'
import re import re
a = open("Proofs/Audit.lean").read() a = open("Proofs/Audit.lean").read()
new, n = re.subn(r'(`fips205\.to_int_loop_eq,\s*)kernel3\)', r'\1kernel3 ++ [oracleF])', a) new, n = re.subn(r'(`fips205\.to_int_loop_eq,\s*)kernel3(,\s*\d+\))', r'\1kernel3 ++ [oracleF]\2', a)
assert n == 1, f"expected exactly one to_int table entry, patched {n}" assert n == 1, f"patched {n}"
open("Proofs/Audit.lean","w").write(new) open("Proofs/Audit.lean","w").write(new)
PY PY
if ./check.sh > /tmp/selftest-drop.out 2>&1; then if ./check.sh > /tmp/sf3.out 2>&1; then echo "✗ ATTACK 3 SUCCEEDED (dropped oracle, subset hole)"; exit 1; fi
echo "✗ ATTACK 3 SUCCEEDED: audit passed a certificate missing a claimed oracle (subset hole!)"; exit 1 grep -q "missing=\[verify_mono.oracle.f\]" /tmp/sf3.out || { echo "✗ ATTACK 3: rejected but missing oracle not named"; cat /tmp/sf3.out; exit 1; }
fi
grep -q "AUDIT FAILED" /tmp/selftest-drop.out \
|| { echo "✗ ATTACK 3: failed, but not via the axiom audit"; cat /tmp/selftest-drop.out; exit 1; }
grep -q "missing=\[verify_mono.oracle.f\]" /tmp/selftest-drop.out \
|| { echo "✗ ATTACK 3: rejected, but not by naming the missing oracle (exact-cone not enforced?)"; cat /tmp/selftest-drop.out; exit 1; }
restore; rm -f Proofs/Audit.olean restore; rm -f Proofs/Audit.olean
echo "✓ attack 3 rejected (missing-oracle detection works — exact cone enforced, not subset)" echo "✓ attack 3 rejected (missing-oracle detection — exact cone, not subset)"
# ── Attack 4: vanished certificate (collectAxioms-returns-[] trap) ────────── # ── Attack 4: vanished certificate ──────────────────────────────────────────
backup save Proofs/Audit.lean
python3 - <<'PY' python3 - <<'PY'
a = open("Proofs/Audit.lean").read() a = open("Proofs/Audit.lean").read()
assert a.count('`fips205.chain_free_loop_eq') >= 1 assert a.count('`fips205.chain_free_loop_eq') >= 1
a = a.replace('`fips205.chain_free_loop_eq,', '`fips205.chain_free_loop_eq_VANISHED,', 1) open("Proofs/Audit.lean","w").write(a.replace('`fips205.chain_free_loop_eq,', '`fips205.chain_free_loop_eq_VANISHED,', 1))
open("Proofs/Audit.lean","w").write(a)
PY PY
if ./check.sh > /tmp/selftest-vanish.out 2>&1; then if ./check.sh > /tmp/sf4.out 2>&1; then echo "✗ ATTACK 4 SUCCEEDED (vanished cert)"; exit 1; fi
echo "✗ ATTACK 4 SUCCEEDED: audit stayed green for a non-existent certificate (fail-open!)"; exit 1 grep -q "NOT FOUND" /tmp/sf4.out || { echo "✗ ATTACK 4: rejected but not via existence check"; cat /tmp/sf4.out; exit 1; }
fi
grep -q "NOT FOUND" /tmp/selftest-vanish.out \
|| { echo "✗ ATTACK 4: failed, but not via the existence check"; cat /tmp/selftest-vanish.out; exit 1; }
restore; rm -f Proofs/Audit.olean restore; rm -f Proofs/Audit.olean
echo "✓ attack 4 rejected (existence check works — a vanished cert cannot pass as 0-axiom)" echo "✓ attack 4 rejected (existence check — vanished cert cannot pass as 0-axiom)"
# ── Attack 5: un-manifested theorem proving False (round-4 F1, the big one) ──
save Proofs/ChainSpec.lean
printf '\n-- SELFTEST ATTACK 5 (round-4 F1): an un-manifested cert proving False.\naxiom cheat : ∀ (P : Prop), P\ntheorem repo_proves_false : False := cheat _\n' >> Proofs/ChainSpec.lean
if ./check.sh > /tmp/sf5.out 2>&1; then echo "✗ ATTACK 5 SUCCEEDED: check.sh GREEN over a repo proving False!"; exit 1; fi
grep -q "repo_proves_false" /tmp/sf5.out || { echo "✗ ATTACK 5: rejected but not via the module enumeration"; cat /tmp/sf5.out; exit 1; }
restore; rm -f Proofs/ChainSpec.olean Proofs/Audit.olean
echo "✓ attack 5 rejected (module enumeration — an un-manifested False theorem cannot pass)"
# ── Attack 6: gutted statement, cone preserved (round-4 F2) ─────────────────
# Gut a LEAF certificate (to_byte_loop_eq — nothing depends on it, so Phase 2
# still compiles and the fingerprint gate is what must bite). Replace its
# statement+proof with a kernel-3 tautology: same cone, different type.
save Proofs/InputPrepSpec.lean
python3 - <<'PY'
s = open("Proofs/InputPrepSpec.lean").read()
i = s.index("theorem to_byte_loop_eq (n : Std.U32) (k : Nat) :")
j = s.index("theorem hbody_cs", i) # the NEXT declaration (to_byte_loop_eq is a leaf)
gut = "theorem to_byte_loop_eq : (∀ p : Prop, p ¬p) := Classical.em\n\n"
open("Proofs/InputPrepSpec.lean","w").write(s[:i] + gut + s[j:])
PY
if ./check.sh > /tmp/sf6.out 2>&1; then echo "✗ ATTACK 6 SUCCEEDED: gutted statement passed (cone preserved)"; exit 1; fi
grep -qi "fingerprint" /tmp/sf6.out || { echo "✗ ATTACK 6: rejected but not via the statement fingerprint"; cat /tmp/sf6.out; exit 1; }
restore; rm -f Proofs/InputPrepSpec.olean Proofs/Audit.olean
echo "✓ attack 6 rejected (statement fingerprint — a gutted statement of the same cone cannot pass)"
# ── Attack 7: hand-edited model file (round-4 F3) ───────────────────────────
save gen/SlhVerify/Funs.lean
printf '\n-- SELFTEST ATTACK 7 (round-4 F3): hand-edited model.\n' >> gen/SlhVerify/Funs.lean
if ./check.sh > /tmp/sf7.out 2>&1; then echo "✗ ATTACK 7 SUCCEEDED: hand-edited model passed"; exit 1; fi
grep -q "MODEL INTEGRITY FAILED" /tmp/sf7.out || { echo "✗ ATTACK 7: rejected but not via Phase 0"; cat /tmp/sf7.out; exit 1; }
restore
echo "✓ attack 7 rejected (Phase 0 model-byte integrity — a hand-edited model cannot compile)"
# ── Attack 8: deleted manifest row (round-4 F1 set half) ────────────────────
save Proofs/Audit.lean
python3 - <<'PY'
import re
a = open("Proofs/Audit.lean").read()
new, n = re.subn(r'\n *\(`fips205\.to_int_loop_eq,[^\n]*\),', '', a)
assert n == 1, f"removed {n}"
open("Proofs/Audit.lean","w").write(new)
PY
if ./check.sh > /tmp/sf8.out 2>&1; then echo "✗ ATTACK 8 SUCCEEDED: a deleted manifest row passed (set unbound)"; exit 1; fi
grep -q "manifest fingerprint mismatch" /tmp/sf8.out || { echo "✗ ATTACK 8: rejected but not via the manifest-fingerprint binding"; cat /tmp/sf8.out; exit 1; }
restore; rm -f Proofs/Audit.olean
echo "✓ attack 8 rejected (manifest fingerprint — a silently-dropped cert cannot pass)"
echo echo
echo "SELFTEST GREEN: the audit genuinely rejects extra axioms, dropped oracles," echo "SELFTEST GREEN: the gate rejects dead files, extra axioms, dropped oracles,"
echo "vanished certificates, and dead proof files." echo "vanished certs, un-manifested False theorems, gutted statements, hand-edited"
echo "models, and deleted manifest rows."

View file

@ -3,14 +3,19 @@
# Green output == the full claim. This script is the ONLY source of the # Green output == the full claim. This script is the ONLY source of the
# word "proven" for this repo. # word "proven" for this repo.
# #
# Phase 0 — model-byte integrity: every gen/SlhVerify/*.lean must sha256-match
# PROVENANCE.json (a hand-edited model fails BEFORE it is compiled;
# round-4 review F3).
# Phase 1 — compile the extracted Lean model (gen/SlhVerify). # Phase 1 — compile the extracted Lean model (gen/SlhVerify).
# Phase 2 — compile the proof files (Proofs/). # Phase 2 — compile the proof files (Proofs/).
# Phase 3 — axiom audit, performed INSIDE Lean (Proofs/Audit.lean): each # Phase 3 — the in-Lean audit (Proofs/Audit.lean): each certificate's cone
# certificate's cone, read from the kernel via `collectAxioms`, must # (via `collectAxioms`) must EQUAL its expected set exactly AND its
# EQUAL its expected set EXACTLY — kernel-3 plus only the named SHA-2 # statement fingerprint must match the committed manifest, EVERY
# oracles. No text parsing (round-2 review closed that class of bug); # theorem in the eight cert modules must have a cone within the
# any extra, any missing, a renamed/deleted cert, or an axiom/opaque # boundary (so an un-manifested `: False := cheat _` cannot pass —
# sham is a Lean elaboration error → non-zero exit → fail-closed. # round-4 F1), and this script binds to the printed MANIFEST
# fingerprint (round-4 F2/F1). Any mismatch → non-zero exit →
# fail-closed. No text parsing of axiom cones.
set -euo pipefail set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)" HERE="$(cd "$(dirname "$0")" && pwd)"
@ -55,6 +60,27 @@ CERTS=(
echo "fips205-slhdsa-verified — check" echo "fips205-slhdsa-verified — check"
echo "===============================" echo "==============================="
# ── Phase 0: model-byte integrity (gen/ == PROVENANCE.json) ──────────────────
echo "=== Phase 0: model-byte integrity (gen/ pinned to PROVENANCE.json) ==="
python3 - "$HERE/PROVENANCE.json" "$HERE" <<'PY' || { echo "MODEL INTEGRITY FAILED (a gen/ file differs from PROVENANCE.json — hand-edited model?)"; exit 1; }
import json, sys, hashlib, os
prov = json.load(open(sys.argv[1])); here = sys.argv[2]
files = {k: v for k, v in prov.get("model_integrity_sha256", {}).items() if k.startswith("gen/")}
if not files:
print(" no model_integrity_sha256 in PROVENANCE.json (fail-closed)"); sys.exit(1)
bad = 0
for rel, want in sorted(files.items()):
p = os.path.join(here, rel)
if not os.path.exists(p):
print(f" ✗ {rel} MISSING"); bad = 1; continue
got = hashlib.sha256(open(p, 'rb').read()).hexdigest()
if got != want:
print(f" ✗ {rel}: sha256 {got[:12]} ≠ pinned {want[:12]}"); bad = 1
else:
print(f" ✓ {rel}")
sys.exit(1 if bad else 0)
PY
# ── Phase 1: model ────────────────────────────────────────────────────────── # ── Phase 1: model ──────────────────────────────────────────────────────────
echo "=== Phase 1: compile the extracted model ===" echo "=== Phase 1: compile the extracted model ==="
cd "$AENEAS_LEAN" cd "$AENEAS_LEAN"
@ -80,22 +106,32 @@ lake env bash -c "
done done
" "
# ── Phase 3: axiom audit (exact cone per certificate, inside Lean) ─────────── # ── Phase 3: in-Lean audit (cones + statements + full-module enumeration) ────
echo "=== Phase 3: axiom audit (exact cone per certificate — inside Lean) ===" echo "=== Phase 3: in-Lean audit (cones + statement fingerprints + enumeration) ==="
cd "$AENEAS_LEAN" cd "$AENEAS_LEAN"
# Proofs/Audit.lean reads each cert's cone from the kernel (collectAxioms) and # Proofs/Audit.lean checks: per-cert exact cone + statement fingerprint; every
# asserts SET EQUALITY against its expected boundary. Compiling it IS the audit: # theorem in the eight cert modules has a clean cone (no un-manifested axiom
# any mismatch throws → non-zero exit. We additionally require the explicit # smuggle); and it prints a MANIFEST-FINGERPRINT over the whole committed
# PASSED line, so a build that somehow exits 0 without running the audit still # manifest. We bind to that EXACT fingerprint below, so deleting/swapping a cert
# fails closed. # row, or editing a cone or a statement fingerprint, changes the printed value
# and fails HERE even if Lean itself exits 0 (round-4 F1/F2 set+statement bind).
# To rotate the manifest deliberately, recompute it (compile Audit.lean, read the
# printed value) and update EXPECTED_MANIFEST_FP in the same reviewable commit.
EXPECTED_MANIFEST_FP="MANIFEST-FINGERPRINT: 13660980750615609973"
AUD_OUT=$(lake env bash -c "cd '$HERE' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD/gen:\$PWD\" && LEAN_TIMEOUT=$TIMEOUT LEAN_MEM_MB=$MEM '$HERE/lean-guard' 'Proofs/Audit.lean'" 2>&1) || { AUD_OUT=$(lake env bash -c "cd '$HERE' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD/gen:\$PWD\" && LEAN_TIMEOUT=$TIMEOUT LEAN_MEM_MB=$MEM '$HERE/lean-guard' 'Proofs/Audit.lean'" 2>&1) || {
echo "$AUD_OUT" | sed 's/^/ /' echo "$AUD_OUT" | sed 's/^/ /'
echo "AXIOM AUDIT FAILED (Audit.lean did not compile — cone mismatch, missing cert, or sham)"; exit 1; } echo "AUDIT FAILED (Audit.lean did not compile — cone/statement mismatch, missing cert, sham, or un-audited theorem)"; exit 1; }
if ! grep -qF "exact-cone audit PASSED" <<<"$AUD_OUT"; then if ! grep -qF "exact-cone audit PASSED" <<<"$AUD_OUT"; then
echo "$AUD_OUT" | sed 's/^/ /' echo "$AUD_OUT" | sed 's/^/ /'
echo "AXIOM AUDIT FAILED (no PASSED line — fail-closed)"; exit 1 echo "AUDIT FAILED (no PASSED line — fail-closed)"; exit 1
fi fi
echo " ✓ exact-cone audit PASSED: each of the ${#CERTS[@]} certificate cones == its expected boundary set" if ! grep -qF "$EXPECTED_MANIFEST_FP" <<<"$AUD_OUT"; then
echo "$AUD_OUT" | sed 's/^/ /'
echo "AUDIT FAILED — manifest fingerprint mismatch."
echo " expected: $EXPECTED_MANIFEST_FP"
echo " the certificate set / a cone / a statement fingerprint changed without a reviewed manifest rotation."; exit 1
fi
echo "$(grep -oF 'exact-cone audit PASSED' <<<"$AUD_OUT" | head -1) ($EXPECTED_MANIFEST_FP)"
echo echo
echo "ALL GREEN — model compiles, proofs compile, and every certificate cone" echo "ALL GREEN — model compiles, proofs compile, and every certificate cone"