mirror of
https://github.com/saymrwulf/fips205-slhdsa-verified.git
synced 2026-09-03 19:53:49 +00:00
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:
parent
337e5e9481
commit
45a2f65a2d
8 changed files with 336 additions and 154 deletions
24
README.md
24
README.md
|
|
@ -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+s−1 in turn. This rules out —
|
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 — 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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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 1–6). check.sh Phase 0 pins the model bytes to model_integrity_sha256 above."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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+s−1 in turn. This rules out — machine-checked, for the
|
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
|
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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue