review round 6: pin the auditor, purge every olean, pin gen/ as a set

Round 6 confirmed the digest redesign closed NEW-1/NEW-2/NEW-5 at the mechanism
("the first time in three rounds I have not been able to gut a certificate"),
then demonstrated two more ways to reach ALL GREEN with the committed digest
BYTE-IDENTICAL over a repository proving False. Both are fixed.

NEW-7 — the digest bound the audit's DATA, never its LOGIC. Flipping the two
fail-closed guards in Proofs/Audit.lean to `unless true` disabled every in-Lean
check; the block's inputs genuinely had not changed, so the digest still
matched. Total attacker diff: 2 files, 6 insertions. Worse, TRUSTED-BASE item 11
listed the trusted-unbound set and did NOT mention Audit.lean, so a reviewer
using it as a map of what to read by hand would have skipped the file that
computes the number it is judged by.
FIX: Proofs/Audit.lean is now sha256-pinned in harness_integrity_sha256,
symmetric with lean-guard, and item 11 says so — including the honest residue:
an author who edits the logic AND rotates its pin is caught only by reading the
diff at the pin.

NEW-8 — Phase 0's purge covered gen/ and Proofs/ while the stray check greped
only *.lean, so an ORPHAN verification/Evil.olean whose source had been DELETED
fell between them, satisfied an import, and was invisible to git status
(*.olean is gitignored).
FIX: purge every .olean under verification/, and forbid stray .lean AND .olean.

NEW-9 (partial) — gen/ was pinned by four NAMES, not as a SET, so a new file
there was neither hashed nor forbidden while LEAN_PATH contains $PWD/gen.
FIX: Phase 0 asserts the gen/*.lean file set equals the pin map exactly. This
found a real gap on its first run: Aeneas emits *_Template.lean scaffolding into
gen/ on every extraction — untracked byproducts (a fresh clone has only the four
pinned files) that nothing imports but that sat on LEAN_PATH unpinned. They are
now purged as byproducts before the set assertion.

Also, from the reviewer's §3 suggestion: the canonical block is now COMMITTED as
verification/AUDIT-MANIFEST.txt, so a digest mismatch prints a real diff instead
of writing an observed file with nothing to compare against; check.sh also fails
if the committed copy drifts from what Lean emits.

check-selftest.sh: 16 attacks, all rejected, plus the coverage check. Attacks 16
and 17 are the reviewer's two demonstrations. Attacks that mutate the audit's
DATA now re-pin Audit.lean first, so they still test the digest/enumeration
rather than being stopped by the byte pin; attack 17 deliberately does not
re-pin, because the pin is what it tests.

Housekeeping: PROVENANCE.json trailing newline restored (NEW-12).

Disclosed rather than buried: two more of my own assertion bugs this round —
attack 16 asserted the hygiene message when the correct rejection is the purge
plus a failed import, and the earlier gen/ set check surfaced the template files
only because it was written strictly. Both are the wrong-diagnostic class.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mrwulf 2026-07-28 09:22:12 +02:00
parent dce0473013
commit e6ffd16277
6 changed files with 273 additions and 26 deletions

View file

@ -69,7 +69,19 @@ proceeds and is part of every claim.
(`PROVENANCE.json → harness_integrity_sha256`); it is kept rather than (`PROVENANCE.json → harness_integrity_sha256`); it is kept rather than
removed because it is the memory cap and machine-wide lock that protect the removed because it is the memory cap and machine-wide lock that protect the
build machine (a Lean elaboration once reached 12.2 GB and took the host build machine (a Lean elaboration once reached 12.2 GB and took the host
down). Still trusted, and NOT bound by anything the button can check: down).
**`verification/Proofs/Audit.lean` is pinned the same way, and for a sharper
reason** (round-6 NEW-7): the digest it emits binds the audit's *data* — the
policy constants, the statements, the specification bodies — but nothing can
make a program hash the correctness of its own logic. Flipping this file's
two fail-closed guards to `unless true` disabled every in-Lean check while
the digest stayed BYTE-IDENTICAL, and a repository proving `False` passed
ALL GREEN. The byte pin converts that from a silent green into a build
failure; a legitimate change to the audit is now a reviewable pin rotation.
Note the residue honestly: an author who edits the logic *and* rotates its
pin in the same commit is not stopped by anything mechanical — that case is
caught only by reading the diff at the pin.
Still trusted, and NOT bound by anything the button can check:
`check.sh` itself, `~/aeneas-toolchain/env.sh`, the `$AENEAS_HOME` tree `check.sh` itself, `~/aeneas-toolchain/env.sh`, the `$AENEAS_HOME` tree
(i.e. *which* Aeneas/Lean library the proofs are checked against), `python3`, (i.e. *which* Aeneas/Lean library the proofs are checked against), `python3`,
and the Lean toolchain. An audit executed by a harness cannot defend against and the Lean toolchain. An audit executed by a harness cannot defend against

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
{ {
"_comment": "Single machine-readable pin set to reproduce the extracted Lean model in gen/SlhVerify from source. External review rounds 2-5 (2026-07-24 .. 2026-07-27). Running verification/extract.sh against the pinned source tree with the pinned Charon/Aeneas toolchain reproduces the two aeneas-generated files byte-identically (generated_artifacts_sha256 below). The *External*.lean files are hand-maintained (not regenerated) and are NOT part of that reproducibility claim but they ARE byte-pinned for integrity in model_integrity_sha256, since they carry the five oracle axioms and the discharged Step definitions. NOTE: SlhVerify.llbc is an intermediate artifact and is .gitignored, so its hash here can only be checked by a party who re-runs extract.sh.", "_comment": "Single machine-readable pin set to reproduce the extracted Lean model in gen/SlhVerify from source. External review rounds 2-5 (2026-07-24 .. 2026-07-27). Running verification/extract.sh against the pinned source tree with the pinned Charon/Aeneas toolchain reproduces the two aeneas-generated files byte-identically (generated_artifacts_sha256 below). The *External*.lean files are hand-maintained (not regenerated) and are NOT part of that reproducibility claim \u2014 but they ARE byte-pinned for integrity in model_integrity_sha256, since they carry the five oracle axioms and the discharged Step definitions. NOTE: SlhVerify.llbc is an intermediate artifact and is .gitignored, so its hash here can only be checked by a party who re-runs extract.sh.",
"parameter_set": "SLH-DSA-SHA2-128s", "parameter_set": "SLH-DSA-SHA2-128s",
"source": { "source": {
"repo": "github.com/saymrwulf/fips205-source", "repo": "github.com/saymrwulf/fips205-source",
@ -45,8 +45,9 @@
"gen/SlhVerify/Funs.lean": "7b7de55fd0206142f2678a079a6ed4462292356bc7de08ecd55cac0c76a1da9f" "gen/SlhVerify/Funs.lean": "7b7de55fd0206142f2678a079a6ed4462292356bc7de08ecd55cac0c76a1da9f"
}, },
"harness_integrity_sha256": { "harness_integrity_sha256": {
"_comment": "The compiler harness is part of the trusted computing base: check.sh shells out to lean-guard for every compile, and round-5 review demonstrated that stubbing it alone yields ALL GREEN in 3.6s over destroyed proofs. lean-guard is KEPT (it is the memory cap and machine-wide lock that protect this machine after the 12.2GB OOM incident) and sha256-pinned by Phase 0. NOT covered, and disclosed as trusted in TRUSTED-BASE.md item 11: check.sh itself, ~/aeneas-toolchain/env.sh, $AENEAS_HOME, python3, and the Lean toolchain.", "_comment": "The harness is part of the trusted computing base. check.sh shells out to lean-guard for every compile (round-5 NEW-3: stubbing it alone yielded ALL GREEN in 3.6s over destroyed proofs), and Proofs/Audit.lean is the program that COMPUTES the digest it is judged by (round-6 NEW-7: flipping two fail-closed guards to `unless true` let a repo proving False pass with the digest BYTE-IDENTICAL \u2014 the digest binds the audit's DATA, never its LOGIC). Both are sha256-pinned by Phase 0, so tampering with either is a build failure rather than a silent green, and any legitimate change is a reviewable pin rotation. STILL trusted and unbound, disclosed in TRUSTED-BASE.md item 11: check.sh itself, ~/aeneas-toolchain/env.sh, $AENEAS_HOME, python3, and the Lean toolchain.",
"lean-guard": "690d861cfd81b327604ffd5ef85599ee01b2bcd943907f5b71b8b6f99bad418f" "lean-guard": "690d861cfd81b327604ffd5ef85599ee01b2bcd943907f5b71b8b6f99bad418f",
"Proofs/Audit.lean": "6108b97d75a8a8e8e0b9bef642ba887ea1ae8b17ba5b7c11d66400bfd2847d9e"
}, },
"audit_gate": "verification/Proofs/Audit.lean \u2014 in-Lean audit (round 5). Per certificate: exact cone equality (collectAxioms) + elaborated-statement check. Enumeration: EVERY declaration kind (def/theorem/opaque/axiom \u2014 round 4 matched theorems only, so a `def : False` passed) in the eight certificate modules AND in Audit.lean itself (the auditor is not exempt). Binding digest: check.sh binds to the SHA-256 of a canonical AUDIT-MANIFEST block covering the POLICY constants (allowedBoundary + certModules \u2014 round-5 NEW-1: widening allowedBoundary previously re-opened the False-proof with the fingerprint byte-identical), every certificate's fully-elaborated statement, and every reachable specification definition's fully-elaborated BODY (round-5 NEW-2: redefining a reference fold to *be* the extracted loop previously left cone and fingerprint intact while the certificate degenerated to `loop = loop`). The 32-bit Expr.hash is retained only as a per-certificate diagnostic, not as the binding. Fail-closed; adversarially exercised by check-selftest.sh. check.sh Phase 0 additionally purges stale .olean files, forbids stray .lean outside gen/ and Proofs/, and pins model + harness bytes." "audit_gate": "verification/Proofs/Audit.lean \u2014 in-Lean audit (round 5). Per certificate: exact cone equality (collectAxioms) + elaborated-statement check. Enumeration: EVERY declaration kind (def/theorem/opaque/axiom \u2014 round 4 matched theorems only, so a `def : False` passed) in the eight certificate modules AND in Audit.lean itself (the auditor is not exempt). Binding digest: check.sh binds to the SHA-256 of a canonical AUDIT-MANIFEST block covering the POLICY constants (allowedBoundary + certModules \u2014 round-5 NEW-1: widening allowedBoundary previously re-opened the False-proof with the fingerprint byte-identical), every certificate's fully-elaborated statement, and every reachable specification definition's fully-elaborated BODY (round-5 NEW-2: redefining a reference fold to *be* the extracted loop previously left cone and fingerprint intact while the certificate degenerated to `loop = loop`). The 32-bit Expr.hash is retained only as a per-certificate diagnostic, not as the binding. Fail-closed; adversarially exercised by check-selftest.sh. check.sh Phase 0 additionally purges stale .olean files, forbids stray .lean outside gen/ and Proofs/, and pins model + harness bytes."
} }

View file

@ -277,3 +277,75 @@ a stubbed harness, and stray modules.
Note: the `_f: command not found` line in the selftest transcript above is a Note: the `_f: command not found` line in the selftest transcript above is a
cosmetic shell-quoting bug in the script's own success message (backticks inside cosmetic shell-quoting bug in the script's own success message (backticks inside
a double-quoted echo), fixed in this same commit. It did not affect any gate. a double-quoted echo), fixed in this same commit. It did not affect any gate.
## Round-6 hardening — author-agent run, 20260728T072212Z, proof repo @ (this commit)
Captured with `tee` from the real commands. Not independently executed.
PIN ROTATIONS IN THIS COMMIT (stated explicitly — round-6 NEW-11):
· harness_integrity_sha256 GAINS Proofs/Audit.lean (6108b97d…) — new pin, NEW-7.
· no model pin rotated; gen/ bytes unchanged.
### check.sh
```
fips205-slhdsa-verified — check
===============================
=== Phase 0: build hygiene + model/harness integrity ===
✓ Proofs/Audit.lean
✓ gen/SlhVerify/Funs.lean
✓ gen/SlhVerify/FunsExternal.lean
✓ gen/SlhVerify/Types.lean
✓ gen/SlhVerify/TypesExternal.lean
✓ lean-guard
=== Phase 1: compile the extracted model ===
· gen/SlhVerify/TypesExternal
· gen/SlhVerify/Types
· gen/SlhVerify/FunsExternal
· gen/SlhVerify/Funs
=== Phase 2: compile the proofs ===
· ChainSpec
· WotsSpec
· XmssSpec
· HtSpec
· ForsInnerSpec
· ForsOuterSpec
· InputPrepSpec
· ApexSpec
=== Phase 3: in-Lean audit (cones + statement fingerprints + enumeration) ===
✓ exact-cone audit PASSED
✓ audit-manifest digest matches (sha256 d83e297a49094c97…)
ALL GREEN — model compiles, proofs compile, and every certificate cone
equals EXACTLY the three kernel axioms plus its documented SHA-2 oracles.
Certificates proven: fips205.chain_free_loop_eq fips205.wots_loop1_eq fips205.xmss_loop_eq fips205.ht_loop_eq fips205.fors_inner_loop_eq fips205.fors_outer_loop_eq fips205.to_int_loop_eq fips205.to_byte_loop_eq fips205.wots_csum_loop_eq fips205.base2b_outer_loop_eq fips205.slh_verify_128s_accepts_iff
```
### check-selftest.sh (16 attacks + digest-coverage check)
```
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 — a vanished cert cannot pass as 0-axiom)
✓ attack 5 rejected (enumeration — an un-manifested False theorem cannot pass)
✓ attack 6 rejected (statement check — a gutted statement of the same cone cannot pass)
✓ attack 7 rejected (Phase 0 model-byte integrity)
✓ attack 8 rejected (audit-manifest digest — a silently-dropped cert cannot pass)
✓ attack 9 rejected (digest covers allowedBoundary — the policy cannot be widened silently)
✓ attack 10 rejected (a specification fold cannot be silently redefined to the loop)
✓ attack 11 rejected (enumeration covers every declaration kind, not just theorems)
✓ attack 12 rejected (the auditor audits itself — no exemption)
✓ attack 13 rejected (Phase 0 pins lean-guard — the harness is in the TCB and bound)
✓ attack 14 rejected (no .lean may sit outside gen/ and Proofs/)
✓ attack 16 rejected (Phase 0 purges every .olean under verification/, so an
orphan compiled module with no source cannot satisfy an import)
✓ attack 17 rejected (Phase 0 pins Audit.lean — its LOGIC cannot be silently switched off)
✓ check 15 passed (the hashed block carries all 12 reference-fold bodies,
including the recursive _f companions and their extracted-primitive calls)
SELFTEST GREEN: 16 attacks rejected + digest-coverage check — dead files, extra axioms, dropped
oracles, vanished certs, un-manifested False theorems AND defs, gutted
statements, hand-edited models, dropped manifest rows, widened policy,
specification folds redefined to the loop, a False-proof in the auditor,
a stubbed harness, and stray modules.
```

View file

@ -20,6 +20,11 @@
# previously ALL GREEN in 3.6s over destroyed proofs) # previously ALL GREEN in 3.6s over destroyed proofs)
# 14 a stray .lean beside check.sh (round-5 NEW-4: # 14 a stray .lean beside check.sh (round-5 NEW-4:
# LEAN_PATH includes $PWD, so it can join the environment ungated) # LEAN_PATH includes $PWD, so it can join the environment ungated)
# 16 an ORPHAN .olean whose source was deleted (round-6 NEW-8:
# fell between the purge and the stray check; ALL GREEN, digest identical)
# 17 the audit's fail-closed guards switched off (round-6 NEW-7:
# the digest binds the audit's DATA, never its LOGIC; two characters in
# Audit.lean defeated every gate with the digest BYTE-IDENTICAL)
# #
# Self-cleaning: every mutated file is backed up and restored, and an EXIT trap # Self-cleaning: every mutated file is backed up and restored, and an EXIT trap
# restores even on failure. Run from a clean tree. # restores even on failure. Run from a clean tree.
@ -31,10 +36,25 @@ source ~/aeneas-toolchain/env.sh
BAKS=() BAKS=()
save() { cp -p "$1" "$1.sfbak"; BAKS+=("$1"); } save() { cp -p "$1" "$1.sfbak"; BAKS+=("$1"); }
restore() { for f in "${BAKS[@]:-}"; do [ -f "$f.sfbak" ] && mv -f "$f.sfbak" "$f"; done; BAKS=(); } restore() { for f in "${BAKS[@]:-}"; do [ -f "$f.sfbak" ] && mv -f "$f.sfbak" "$f"; done; BAKS=(); }
# Proofs/Audit.lean is sha256-pinned by Phase 0 since round 6 (NEW-7). An attack
# that mutates the audit's DATA must therefore ALSO rotate that pin, otherwise it
# is stopped by the byte pin and never reaches the mechanism it means to test —
# the "rejected for an unrelated reason" defect class. `repin_audit` simulates an
# author who edits and dutifully re-pins; the digest/enumeration must still bite.
# Attack 17 deliberately does NOT re-pin: it is the test of the pin itself.
repin_audit() {
python3 - <<'PY'
import json, hashlib
p = "PROVENANCE.json"; d = json.load(open(p))
d["harness_integrity_sha256"]["Proofs/Audit.lean"] = hashlib.sha256(
open("Proofs/Audit.lean","rb").read()).hexdigest()
json.dump(d, open(p,"w"), indent=2); open(p,"a").write("\n")
PY
}
cleanup() { cleanup() {
restore restore
rm -f Proofs/Stray.lean Proofs/EvilSpec.lean Evil.lean \ rm -f Proofs/Stray.lean Proofs/EvilSpec.lean Evil.lean Evil.olean \
Proofs/*.olean gen/SlhVerify/*.olean .audit-manifest.observed 2>/dev/null Proofs/*.olean gen/SlhVerify/*.olean *.olean .audit-manifest.observed 2>/dev/null
return 0 return 0
} }
trap cleanup EXIT trap cleanup EXIT
@ -52,7 +72,7 @@ rm -f Proofs/Stray.lean
echo "✓ attack 1 rejected (dead-file gate)" echo "✓ attack 1 rejected (dead-file gate)"
# ── 2: smuggled disallowed axiom in a real cone ───────────────────────────── # ── 2: smuggled disallowed axiom in a real cone ─────────────────────────────
save check.sh; save Proofs/Audit.lean save check.sh; save Proofs/Audit.lean; save PROVENANCE.json
# NB: no imports — Phase 0 now purges every .olean, so a module injected at the # NB: no imports — Phase 0 now purges every .olean, so a module injected at the
# head of the build order cannot import one that has not been compiled yet. # head of the build order cannot import one that has not been compiled yet.
cat > Proofs/EvilSpec.lean <<'EOF' cat > Proofs/EvilSpec.lean <<'EOF'
@ -69,13 +89,14 @@ a = a.replace('import Proofs.ApexSpec', 'import Proofs.ApexSpec\nimport Proofs.E
a = a.replace(' [ (`fips205.chain_free_loop_eq', ' [ (`evil_thm, kernel3, 0),\n (`fips205.chain_free_loop_eq', 1) a = a.replace(' [ (`fips205.chain_free_loop_eq', ' [ (`evil_thm, kernel3, 0),\n (`fips205.chain_free_loop_eq', 1)
open("Proofs/Audit.lean","w").write(a) open("Proofs/Audit.lean","w").write(a)
PY PY
repin_audit
./check.sh > /tmp/sf2.out 2>&1 && fail "ATTACK 2 SUCCEEDED (smuggled axiom)" /tmp/sf2.out ./check.sh > /tmp/sf2.out 2>&1 && fail "ATTACK 2 SUCCEEDED (smuggled axiom)" /tmp/sf2.out
grep -q "evil_ax" /tmp/sf2.out || fail "ATTACK 2: rejected but evil_ax not named" /tmp/sf2.out grep -q "evil_ax" /tmp/sf2.out || fail "ATTACK 2: rejected but evil_ax not named" /tmp/sf2.out
restore; rm -f Proofs/EvilSpec.lean restore; rm -f Proofs/EvilSpec.lean
echo "✓ attack 2 rejected (extra-axiom detection — evil_ax named)" echo "✓ attack 2 rejected (extra-axiom detection — evil_ax named)"
# ── 3: dropped oracle (a subset check would pass; exact must not) ──────────── # ── 3: dropped oracle (a subset check would pass; exact must not) ────────────
save Proofs/Audit.lean save Proofs/Audit.lean; save PROVENANCE.json
python3 - <<'PY' python3 - <<'PY'
import re import re
a = open("Proofs/Audit.lean").read() a = open("Proofs/Audit.lean").read()
@ -83,18 +104,20 @@ new, n = re.subn(r'(`fips205\.to_int_loop_eq,\s*)kernel3,', r'\1kernel3 ++ [orac
assert n == 1, f"expected 1 to_int row, patched {n}" assert n == 1, f"expected 1 to_int row, patched {n}"
open("Proofs/Audit.lean","w").write(new) open("Proofs/Audit.lean","w").write(new)
PY PY
repin_audit
./check.sh > /tmp/sf3.out 2>&1 && fail "ATTACK 3 SUCCEEDED (dropped oracle — subset hole)" /tmp/sf3.out ./check.sh > /tmp/sf3.out 2>&1 && fail "ATTACK 3 SUCCEEDED (dropped oracle — subset hole)" /tmp/sf3.out
grep -q "missing=\[verify_mono.oracle.f\]" /tmp/sf3.out || fail "ATTACK 3: rejected but missing oracle not named" /tmp/sf3.out grep -q "missing=\[verify_mono.oracle.f\]" /tmp/sf3.out || fail "ATTACK 3: rejected but missing oracle not named" /tmp/sf3.out
restore restore
echo "✓ attack 3 rejected (missing-oracle detection — exact cone, not subset)" echo "✓ attack 3 rejected (missing-oracle detection — exact cone, not subset)"
# ── 4: vanished certificate ───────────────────────────────────────────────── # ── 4: vanished certificate ─────────────────────────────────────────────────
save Proofs/Audit.lean save Proofs/Audit.lean; save PROVENANCE.json
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
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.replace('`fips205.chain_free_loop_eq,', '`fips205.chain_free_loop_eq_VANISHED,', 1))
PY PY
repin_audit
./check.sh > /tmp/sf4.out 2>&1 && fail "ATTACK 4 SUCCEEDED (vanished cert)" /tmp/sf4.out ./check.sh > /tmp/sf4.out 2>&1 && fail "ATTACK 4 SUCCEEDED (vanished cert)" /tmp/sf4.out
grep -q "NOT FOUND" /tmp/sf4.out || fail "ATTACK 4: rejected but not via the existence check" /tmp/sf4.out grep -q "NOT FOUND" /tmp/sf4.out || fail "ATTACK 4: rejected but not via the existence check" /tmp/sf4.out
restore restore
@ -131,7 +154,7 @@ restore
echo "✓ attack 7 rejected (Phase 0 model-byte integrity)" echo "✓ attack 7 rejected (Phase 0 model-byte integrity)"
# ── 8: deleted manifest row (round-4 F1, set half) ────────────────────────── # ── 8: deleted manifest row (round-4 F1, set half) ──────────────────────────
save Proofs/Audit.lean save Proofs/Audit.lean; save PROVENANCE.json
python3 - <<'PY' python3 - <<'PY'
import re import re
a = open("Proofs/Audit.lean").read() a = open("Proofs/Audit.lean").read()
@ -139,6 +162,7 @@ new, n = re.subn(r'\n\s*\(`fips205\.to_int_loop_eq,.*?\),', '', a)
assert n == 1, f"expected 1 row, removed {n}" assert n == 1, f"expected 1 row, removed {n}"
open("Proofs/Audit.lean","w").write(new) open("Proofs/Audit.lean","w").write(new)
PY PY
repin_audit
./check.sh > /tmp/sf8.out 2>&1 && fail "ATTACK 8 SUCCEEDED (a dropped cert row passed)" /tmp/sf8.out ./check.sh > /tmp/sf8.out 2>&1 && fail "ATTACK 8 SUCCEEDED (a dropped cert row passed)" /tmp/sf8.out
grep -q "digest mismatch" /tmp/sf8.out || fail "ATTACK 8: rejected but not via the digest binding" /tmp/sf8.out grep -q "digest mismatch" /tmp/sf8.out || fail "ATTACK 8: rejected but not via the digest binding" /tmp/sf8.out
restore restore
@ -152,13 +176,14 @@ echo "✓ attack 8 rejected (audit-manifest digest — a silently-dropped cert c
# (which now also rejects a bare `axiom` in an audited module) cannot fire and # (which now also rejects a bare `axiom` in an audited module) cannot fire and
# the DIGEST must be what bites. `sorryAx` is used as the smuggled name because # the DIGEST must be what bites. `sorryAx` is used as the smuggled name because
# admitting it would silently legalise every `sorry` in the repository. # admitting it would silently legalise every `sorry` in the repository.
save Proofs/Audit.lean save Proofs/Audit.lean; save PROVENANCE.json
python3 - <<'PY' python3 - <<'PY'
a = open("Proofs/Audit.lean").read() a = open("Proofs/Audit.lean").read()
old = " kernel3 ++ [oracleF, oracleH, oracleTL, oracleTLen, oracleHMsg]\n" old = " kernel3 ++ [oracleF, oracleH, oracleTL, oracleTLen, oracleHMsg]\n"
assert a.count(old) == 1, "allowedBoundary shape changed" assert a.count(old) == 1, "allowedBoundary shape changed"
open("Proofs/Audit.lean","w").write(a.replace(old, old.rstrip("\n") + " ++ [`sorryAx]\n", 1)) open("Proofs/Audit.lean","w").write(a.replace(old, old.rstrip("\n") + " ++ [`sorryAx]\n", 1))
PY PY
repin_audit
./check.sh > /tmp/sf9.out 2>&1 && fail "ATTACK 9 SUCCEEDED: the axiom policy was widened and the button stayed GREEN!" /tmp/sf9.out ./check.sh > /tmp/sf9.out 2>&1 && fail "ATTACK 9 SUCCEEDED: the axiom policy was widened and the button stayed GREEN!" /tmp/sf9.out
grep -q "digest mismatch" /tmp/sf9.out || fail "ATTACK 9: rejected but not via the policy-covering digest" /tmp/sf9.out grep -q "digest mismatch" /tmp/sf9.out || fail "ATTACK 9: rejected but not via the policy-covering digest" /tmp/sf9.out
restore restore
@ -203,13 +228,14 @@ restore
echo "✓ attack 11 rejected (enumeration covers every declaration kind, not just theorems)" echo "✓ attack 11 rejected (enumeration covers every declaration kind, not just theorems)"
# ── 12: a False-proof inside the AUDITOR itself (round-5 R1) ──────────────── # ── 12: a False-proof inside the AUDITOR itself (round-5 R1) ────────────────
save Proofs/Audit.lean save Proofs/Audit.lean; save PROVENANCE.json
python3 - <<'PY' python3 - <<'PY'
a = open("Proofs/Audit.lean").read() a = open("Proofs/Audit.lean").read()
i = a.index("elab \"auditCones\"") i = a.index("elab \"auditCones\"")
open("Proofs/Audit.lean","w").write( open("Proofs/Audit.lean","w").write(
a[:i] + "axiom cheat12 : ∀ (P : Prop), P\ntheorem audit_proves_false : False := cheat12 _\n\n" + a[i:]) a[:i] + "axiom cheat12 : ∀ (P : Prop), P\ntheorem audit_proves_false : False := cheat12 _\n\n" + a[i:])
PY PY
repin_audit
./check.sh > /tmp/sf12.out 2>&1 && fail "ATTACK 12 SUCCEEDED: the auditor itself proves False, still GREEN!" /tmp/sf12.out ./check.sh > /tmp/sf12.out 2>&1 && fail "ATTACK 12 SUCCEEDED: the auditor itself proves False, still GREEN!" /tmp/sf12.out
grep -qE "audit_proves_false|AXIOM DECLARED" /tmp/sf12.out || fail "ATTACK 12: rejected but not via self-enumeration" /tmp/sf12.out grep -qE "audit_proves_false|AXIOM DECLARED" /tmp/sf12.out || fail "ATTACK 12: rejected but not via self-enumeration" /tmp/sf12.out
restore restore
@ -240,6 +266,58 @@ grep -q "BUILD HYGIENE FAILED" /tmp/sf14.out || fail "ATTACK 14: rejected but no
rm -f Evil.lean rm -f Evil.lean
echo "✓ attack 14 rejected (no .lean may sit outside gen/ and Proofs/)" echo "✓ attack 14 rejected (no .lean may sit outside gen/ and Proofs/)"
# ── 16: ORPHAN .olean WITH NO SOURCE (round-6 NEW-8) ───────────────────────
# Previously ALL GREEN, digest byte-identical, repo proving False: the round-5
# purge covered gen/ and Proofs/ while the stray check greped only *.lean, so a
# compiled module with its source DELETED fell between the two — and *.olean is
# .gitignored, so `git status` showed only the one-line import.
save Proofs/ChainSpec.lean
cat > Evil.lean <<'EOF'
axiom cheat16 : ∀ (P : Prop), P
theorem attack16_false : False := cheat16 _
EOF
( cd "$AENEAS_HOME/backends/lean" && lake env bash -c \
"cd '$HERE' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD/gen:\$PWD\" && lean -o '$HERE/Evil.olean' '$HERE/Evil.lean'" ) >/dev/null 2>&1
rm -f Evil.lean # the SOURCE is gone; only the .olean remains
printf '\nimport Evil\n' >> Proofs/ChainSpec.lean
if [ ! -f Evil.olean ]; then
echo " (note: could not build Evil.olean; attack 16 inconclusive)"; rm -f Evil.olean
else
./check.sh > /tmp/sf16.out 2>&1 && fail "ATTACK 16 SUCCEEDED: an orphan .olean injected False and stayed GREEN!" /tmp/sf16.out
# The defense is Phase 0's purge, which now covers ALL of verification/ and so
# deletes the orphan BEFORE the stray check ever sees it; `import Evil` then has
# nothing to resolve and Phase 2 fails closed. Assert that chain precisely —
# the rejection must be the import failure AND the .olean must be gone. (An
# earlier revision asserted the hygiene message and failed while the gate was
# working correctly: the same wrong-diagnostic defect this suite exists to catch.)
grep -q "FAIL: Proofs/ChainSpec" /tmp/sf16.out \
|| fail "ATTACK 16: rejected, but not by the purge+import chain" /tmp/sf16.out
[ ! -f Evil.olean ] \
|| fail "ATTACK 16: check.sh failed but the orphan .olean survived Phase 0" /tmp/sf16.out
echo "✓ attack 16 rejected (Phase 0 purges every .olean under verification/, so an"
echo " orphan compiled module with no source cannot satisfy an import)"
fi
restore; rm -f Evil.olean Evil.lean
# ── 17: TAMPER THE AUDIT'S LOGIC, NOT ITS DATA (round-6 NEW-7) ─────────────
# Switching the two fail-closed guards to `unless true` disables every in-Lean
# check while leaving the digest BYTE-IDENTICAL — the digest binds the audit's
# data, never the code that reads it. Note this attack does NOT re-pin
# Audit.lean: the sha256 pin is the only thing standing here, and that is
# exactly what is being tested.
save Proofs/Audit.lean; save Proofs/ChainSpec.lean
python3 - <<'PY'
a = open("Proofs/Audit.lean").read()
n = a.count(" unless errs.isEmpty do")
assert n >= 1, "Audit.lean fail-closed guard shape changed"
open("Proofs/Audit.lean","w").write(a.replace(" unless errs.isEmpty do", " unless true do"))
PY
printf '\n-- SELFTEST ATTACK 17\naxiom cheat17 : ∀ (P : Prop), P\ntheorem attack17_false : False := cheat17 _\n' >> Proofs/ChainSpec.lean
./check.sh > /tmp/sf17.out 2>&1 && fail "ATTACK 17 SUCCEEDED: the audit's guards were disabled and it stayed GREEN!" /tmp/sf17.out
grep -q "INTEGRITY FAILED" /tmp/sf17.out || fail "ATTACK 17: rejected but not via the Audit.lean harness pin" /tmp/sf17.out
restore
echo "✓ attack 17 rejected (Phase 0 pins Audit.lean — its LOGIC cannot be silently switched off)"
# ── 15: COVERAGE OF THE DIGEST INPUT (direct, not an attack) ─────────────── # ── 15: COVERAGE OF THE DIGEST INPUT (direct, not an attack) ───────────────
# Attack 9 proves the digest binding fires. This proves WHAT it covers: the # Attack 9 proves the digest binding fires. This proves WHAT it covers: the
# hashed block must literally contain each reference fold's definition BODY, so # hashed block must literally contain each reference fold's definition BODY, so
@ -270,7 +348,7 @@ echo "✓ check 15 passed (the hashed block carries all 12 reference-fold bodies
echo ' including the recursive _f companions and their extracted-primitive calls)' echo ' including the recursive _f companions and their extracted-primitive calls)'
echo echo
echo "SELFTEST GREEN: 14 attacks rejected + digest-coverage check — dead files, extra axioms, dropped" echo "SELFTEST GREEN: 16 attacks rejected + digest-coverage check — dead files, extra axioms, dropped"
echo "oracles, vanished certs, un-manifested False theorems AND defs, gutted" echo "oracles, vanished certs, un-manifested False theorems AND defs, gutted"
echo "statements, hand-edited models, dropped manifest rows, widened policy," echo "statements, hand-edited models, dropped manifest rows, widened policy,"
echo "specification folds redefined to the loop, a False-proof in the auditor," echo "specification folds redefined to the loop, a False-proof in the auditor,"

View file

@ -50,17 +50,25 @@ echo "==============================="
# ── Phase 0: build hygiene + model & harness integrity ─────────────────────── # ── Phase 0: build hygiene + model & harness integrity ───────────────────────
echo "=== Phase 0: build hygiene + model/harness integrity ===" echo "=== Phase 0: build hygiene + model/harness integrity ==="
# (a) Purge every .olean first. Round-5 NEW-4: the button's verdict must depend # (a) Purge EVERY .olean under verification/ — round-6 NEW-8: the round-5 purge
# on COMMITTED BYTES, never on untracked build state — a stale .olean from a # covered only gen/ and Proofs/ while the stray check greped only *.lean, so
# module that no longer exists (and *.olean is .gitignored, so invisible to # an ORPHAN `verification/Evil.olean` WITH NO SOURCE AT ALL fell between them,
# `git status`) could otherwise satisfy an import and go green. # satisfied an `import Evil`, and went ALL GREEN with the digest untouched
find "$HERE/gen" "$HERE/Proofs" -name '*.olean' -delete 2>/dev/null || true # (*.olean is .gitignored, so `git status` showed only the import line).
# (b) No Lean source may sit outside gen/ and Proofs/. LEAN_PATH includes $PWD, # The verdict must depend on COMMITTED BYTES, never on untracked build state.
# so a stray verification/*.lean can join the environment ungated (NEW-4). find "$HERE" -name '*.olean' -delete 2>/dev/null || true
STRAY=$(find "$HERE" -maxdepth 1 -name '*.lean' -printf '%f\n' 2>/dev/null || true) # Aeneas also emits `*_Template.lean` scaffolding into gen/ on every
# extraction. Those files are UNTRACKED byproducts (a fresh clone has only
# the four pinned model files), nothing imports them, and they would
# otherwise sit on LEAN_PATH unpinned — the same untracked-state problem.
# Remove them here so the gen/ file-set assertion below can be exact.
find "$HERE/gen" -name '*_Template.lean' -delete 2>/dev/null || true
# (b) No Lean source OR compiled module may sit outside gen/ and Proofs/;
# LEAN_PATH includes $PWD, so either can join the environment ungated.
STRAY=$(find "$HERE" -maxdepth 1 \( -name '*.lean' -o -name '*.olean' \) -printf '%f\n' 2>/dev/null || true)
if [ -n "$STRAY" ]; then if [ -n "$STRAY" ]; then
echo "$STRAY" | sed 's/^/ ✗ stray Lean source outside gen\/ and Proofs\/: /' echo "$STRAY" | sed 's/^/ ✗ stray Lean file outside gen\/ and Proofs\/: /'
echo "BUILD HYGIENE FAILED (a .lean outside the audited directories can join LEAN_PATH)"; exit 1 echo "BUILD HYGIENE FAILED (a .lean/.olean outside the audited directories can join LEAN_PATH)"; exit 1
fi fi
# (c) sha256-pin the extracted model AND the compiler harness. lean-guard is # (c) sha256-pin the extracted model AND the compiler harness. lean-guard is
# repo-tracked and is shelled out to for every compile, so it is part of the # repo-tracked and is shelled out to for every compile, so it is part of the
@ -84,6 +92,20 @@ for rel, want in sorted(files.items()):
print(f" ✗ {rel}: sha256 {got[:12]} ≠ pinned {want[:12]}"); bad = 1 print(f" ✗ {rel}: sha256 {got[:12]} ≠ pinned {want[:12]}"); bad = 1
else: else:
print(f" ✓ {rel}") print(f" ✓ {rel}")
# Round-6 NEW-9: pin gen/ as a SET, not as four names. A new file under gen/ was
# neither hashed nor forbidden, while LEAN_PATH contains $PWD/gen — so the
# closure's premise ("everything outside certModules is pinned or disclosed")
# was not enforced. Any .lean under gen/ must appear in the pin map.
pinned_gen = {k for k in files if k.startswith("gen/")}
actual_gen = set()
for root, _, names in os.walk(os.path.join(here, "gen")):
for n in names:
if n.endswith(".lean"):
actual_gen.add(os.path.relpath(os.path.join(root, n), here))
for extra in sorted(actual_gen - pinned_gen):
print(f" ✗ UNPINNED model file under gen/: {extra}"); bad = 1
for missing in sorted(pinned_gen - actual_gen):
print(f" ✗ pinned model file absent: {missing}"); bad = 1
sys.exit(1 if bad else 0) sys.exit(1 if bad else 0)
PY PY
@ -143,10 +165,19 @@ if [ "$GOT_SHA" != "$EXPECTED_AUDIT_SHA256" ]; then
echo " expected: $EXPECTED_AUDIT_SHA256" echo " expected: $EXPECTED_AUDIT_SHA256"
echo " observed: $GOT_SHA" echo " observed: $GOT_SHA"
echo " A policy constant, a certificate statement, or a specification" echo " A policy constant, a certificate statement, or a specification"
echo " definition changed without a reviewed rotation. The observed block was" echo " definition changed without a reviewed rotation."
echo " written to verification/.audit-manifest.observed — diff it to see what." echo " What moved (committed block vs observed):"
diff -u "$HERE/AUDIT-MANIFEST.txt" "$HERE/.audit-manifest.observed" 2>/dev/null \
| head -40 | sed 's/^/ /' || echo " (AUDIT-MANIFEST.txt absent — cannot diff)"
exit 1 exit 1
fi fi
# The digest's INPUT is committed too (round-6: a mismatch previously wrote an
# observed block with nothing to diff it against). Guard against the committed
# copy drifting from what Lean actually emits.
if ! printf '%s\n' "$BLOCK" | cmp -s - "$HERE/AUDIT-MANIFEST.txt"; then
echo "AUDIT FAILED — the committed AUDIT-MANIFEST.txt does not match the emitted block"
echo " (digest matched, so this means the committed copy is stale — refresh it)"; exit 1
fi
echo "$(grep -oF 'exact-cone audit PASSED' <<<"$AUD_OUT" | head -1)" echo "$(grep -oF 'exact-cone audit PASSED' <<<"$AUD_OUT" | head -1)"
echo " ✓ audit-manifest digest matches (sha256 ${EXPECTED_AUDIT_SHA256:0:16}…)" echo " ✓ audit-manifest digest matches (sha256 ${EXPECTED_AUDIT_SHA256:0:16}…)"