fips205-slhdsa-verified/verification/check-selftest.sh
mrwulf e6ffd16277 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>
2026-07-28 09:22:12 +02:00

355 lines
20 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Adversarial self-test of the check.sh gates (the R3-5 tradition: an audit that
# cannot fail is theater). Every attack MUST make check.sh fail, via the gate it
# targets — each assertion names a SPECIFIC diagnostic, so a rejection for an
# unrelated reason fails the test too.
#
# Attacks 1-8 are the round-3/4 set. Attacks 9-14 were authored by external
# reviewers and an independent drill, each having DEMONSTRATED the corresponding
# fail-open against an earlier gate — they are the reason this round exists:
#
# 9 widen `allowedBoundary` by one name + a False-proof (round-5 NEW-1:
# previously ALL GREEN with the fingerprint BYTE-IDENTICAL)
# 10 redefine a reference fold to BE the extracted loop (round-5 NEW-2:
# previously ALL GREEN — the certificate degenerates to `loop = loop`)
# 11 `def : False` instead of `theorem : False` (drill: the round-4
# enumeration matched `.thmInfo` only, so this passed)
# 12 a False-proof inside Audit.lean itself (round-5 R1: the
# auditor was exempt from its own enumeration)
# 13 stub `lean-guard` (round-5 NEW-3:
# previously ALL GREEN in 3.6s over destroyed proofs)
# 14 a stray .lean beside check.sh (round-5 NEW-4:
# 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
# restores even on failure. Run from a clean tree.
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"
source ~/aeneas-toolchain/env.sh
BAKS=()
save() { cp -p "$1" "$1.sfbak"; BAKS+=("$1"); }
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() {
restore
rm -f Proofs/Stray.lean Proofs/EvilSpec.lean Evil.lean Evil.olean \
Proofs/*.olean gen/SlhVerify/*.olean *.olean .audit-manifest.observed 2>/dev/null
return 0
}
trap cleanup EXIT
fail() { echo "$1"; shift; [ $# -gt 0 ] && sed 's/^/ /' "$1"; exit 1; }
echo "check-selftest: attacking the gates"
echo "===================================="
# ── 1: dead file ────────────────────────────────────────────────────────────
echo "-- stray" > Proofs/Stray.lean
./check.sh > /tmp/sf1.out 2>&1 && fail "ATTACK 1 SUCCEEDED (dead file stayed green)" /tmp/sf1.out
grep -q "DEAD FILE" /tmp/sf1.out || fail "ATTACK 1: failed, not via the dead-file gate" /tmp/sf1.out
rm -f Proofs/Stray.lean
echo "✓ attack 1 rejected (dead-file gate)"
# ── 2: smuggled disallowed axiom in a real cone ─────────────────────────────
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
# head of the build order cannot import one that has not been compiled yet.
cat > Proofs/EvilSpec.lean <<'EOF'
axiom evil_ax : True
theorem evil_thm : True := evil_ax
EOF
python3 - <<'PY'
s = open("check.sh").read()
assert 'PROOFS=(\n' in s, "check.sh PROOFS shape changed"
open("check.sh","w").write(s.replace('PROOFS=(\n', 'PROOFS=(\n "EvilSpec"\n', 1))
a = open("Proofs/Audit.lean").read()
assert 'import Proofs.ApexSpec' in a and ' [ (`fips205.chain_free_loop_eq' in a, "Audit.lean shape changed"
a = a.replace('import Proofs.ApexSpec', 'import Proofs.ApexSpec\nimport Proofs.EvilSpec', 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)
PY
repin_audit
./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
restore; rm -f Proofs/EvilSpec.lean
echo "✓ attack 2 rejected (extra-axiom detection — evil_ax named)"
# ── 3: dropped oracle (a subset check would pass; exact must not) ────────────
save Proofs/Audit.lean; save PROVENANCE.json
python3 - <<'PY'
import re
a = open("Proofs/Audit.lean").read()
new, n = re.subn(r'(`fips205\.to_int_loop_eq,\s*)kernel3,', r'\1kernel3 ++ [oracleF],', a)
assert n == 1, f"expected 1 to_int row, patched {n}"
open("Proofs/Audit.lean","w").write(new)
PY
repin_audit
./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
restore
echo "✓ attack 3 rejected (missing-oracle detection — exact cone, not subset)"
# ── 4: vanished certificate ─────────────────────────────────────────────────
save Proofs/Audit.lean; save PROVENANCE.json
python3 - <<'PY'
a = open("Proofs/Audit.lean").read()
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))
PY
repin_audit
./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
restore
echo "✓ attack 4 rejected (existence check — a vanished cert cannot pass as 0-axiom)"
# ── 5: un-manifested THEOREM proving False (round-4 F1) ─────────────────────
save Proofs/ChainSpec.lean
printf '\n-- SELFTEST ATTACK 5\naxiom cheat5 : ∀ (P : Prop), P\ntheorem repo_proves_false : False := cheat5 _\n' >> Proofs/ChainSpec.lean
./check.sh > /tmp/sf5.out 2>&1 && fail "ATTACK 5 SUCCEEDED: check.sh GREEN over a repo proving False!" /tmp/sf5.out
grep -qE "repo_proves_false|AXIOM DECLARED" /tmp/sf5.out || fail "ATTACK 5: rejected but not via the enumeration" /tmp/sf5.out
restore
echo "✓ attack 5 rejected (enumeration — an un-manifested False theorem cannot pass)"
# ── 6: gutted STATEMENT, cone preserved (round-4 F2) ────────────────────────
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) :") # a LEAF cert
j = s.index("theorem hbody_cs", i) # next declaration
open("Proofs/InputPrepSpec.lean","w").write(
s[:i] + "theorem to_byte_loop_eq : (∀ p : Prop, p ¬p) := Classical.em\n\n" + s[j:])
PY
./check.sh > /tmp/sf6.out 2>&1 && fail "ATTACK 6 SUCCEEDED (gutted statement, cone preserved)" /tmp/sf6.out
grep -q "STATEMENT fingerprint" /tmp/sf6.out || fail "ATTACK 6: rejected but not via the statement check" /tmp/sf6.out
restore
echo "✓ attack 6 rejected (statement check — a gutted statement of the same cone cannot pass)"
# ── 7: hand-edited model file (round-4 F3) ──────────────────────────────────
save gen/SlhVerify/Funs.lean
printf '\n-- SELFTEST ATTACK 7\n' >> gen/SlhVerify/Funs.lean
./check.sh > /tmp/sf7.out 2>&1 && fail "ATTACK 7 SUCCEEDED (hand-edited model passed)" /tmp/sf7.out
grep -q "INTEGRITY FAILED" /tmp/sf7.out || fail "ATTACK 7: rejected but not via Phase 0" /tmp/sf7.out
restore
echo "✓ attack 7 rejected (Phase 0 model-byte integrity)"
# ── 8: deleted manifest row (round-4 F1, set half) ──────────────────────────
save Proofs/Audit.lean; save PROVENANCE.json
python3 - <<'PY'
import re
a = open("Proofs/Audit.lean").read()
new, n = re.subn(r'\n\s*\(`fips205\.to_int_loop_eq,.*?\),', '', a)
assert n == 1, f"expected 1 row, removed {n}"
open("Proofs/Audit.lean","w").write(new)
PY
repin_audit
./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
restore
echo "✓ attack 8 rejected (audit-manifest digest — a silently-dropped cert cannot pass)"
# ── 9: WIDEN THE POLICY (round-5 NEW-1) ─────────────────────────────────────
# Previously ALL GREEN with the committed fingerprint byte-identical: the
# fingerprint covered `manifest` but never `allowedBoundary`, the very predicate
# the enumeration tests against.
# Widen the policy ALONE — no axiom is declared anywhere, so the enumeration
# (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
# admitting it would silently legalise every `sorry` in the repository.
save Proofs/Audit.lean; save PROVENANCE.json
python3 - <<'PY'
a = open("Proofs/Audit.lean").read()
old = " kernel3 ++ [oracleF, oracleH, oracleTL, oracleTLen, oracleHMsg]\n"
assert a.count(old) == 1, "allowedBoundary shape changed"
open("Proofs/Audit.lean","w").write(a.replace(old, old.rstrip("\n") + " ++ [`sorryAx]\n", 1))
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
grep -q "digest mismatch" /tmp/sf9.out || fail "ATTACK 9: rejected but not via the policy-covering digest" /tmp/sf9.out
restore
echo "✓ attack 9 rejected (digest covers allowedBoundary — the policy cannot be widened silently)"
# ── 10: REDEFINE A SPEC FOLD TO BE THE LOOP (round-5 NEW-2) ─────────────────
# The certificate keeps its exact statement, cone and type-hash, but becomes
# `loop = loop` — vacuous. Previously ALL GREEN.
save Proofs/ChainSpec.lean
python3 - <<'PY'
s = open("Proofs/ChainSpec.lean").read()
i = s.index("noncomputable def chainFoldN")
j = s.index("/-- One full loop step", i)
gut = """noncomputable def chainFoldN {N : Std.Usize} (pk_seed : Slice Std.U8) :
types.Adrs → Array Std.U8 N → Std.U32 → Nat → Result (Array Std.U8 N) :=
fun adrs tmp start s =>
verify_mono.chain_free_loop
{ start := start,
«end» := Std.U32.ofNatCore ((start.val + s) % 2 ^ 32) (Nat.mod_lt _ (by norm_num)) }
pk_seed adrs tmp
"""
open("Proofs/ChainSpec.lean","w").write(s[:i] + gut + s[j:])
PY
./check.sh > /tmp/sf10.out 2>&1 && fail "ATTACK 10 SUCCEEDED: the spec fold IS the loop, certificate is vacuous, still GREEN!" /tmp/sf10.out
# Two layers stand here, and either is a valid rejection: the existing proof no
# longer matches the redefined fold (Phase 2), and — if an attacker repairs the
# proof, as the round-5 reviewer did — the digest covers specification BODIES,
# so it moves. Attack 9 is the pure test that the digest binding fires; that the
# digest's input contains the fold bodies is verified directly (see below).
grep -qE "digest mismatch|FAIL: Proofs/ChainSpec" /tmp/sf10.out \
|| fail "ATTACK 10: rejected, but neither via the digest nor a proof break" /tmp/sf10.out
restore
echo "✓ attack 10 rejected (a specification fold cannot be silently redefined to the loop)"
# ── 11: `def : False` rather than `theorem : False` (drill finding) ─────────
save Proofs/WotsSpec.lean
printf '\n-- SELFTEST ATTACK 11\naxiom cheat11 : ∀ (P : Prop), P\ndef attack11_false : False := cheat11 _\n' >> Proofs/WotsSpec.lean
./check.sh > /tmp/sf11.out 2>&1 && fail "ATTACK 11 SUCCEEDED: a def proving False passed!" /tmp/sf11.out
grep -qE "attack11_false|AXIOM DECLARED" /tmp/sf11.out || fail "ATTACK 11: rejected but not via the all-kinds enumeration" /tmp/sf11.out
restore
echo "✓ attack 11 rejected (enumeration covers every declaration kind, not just theorems)"
# ── 12: a False-proof inside the AUDITOR itself (round-5 R1) ────────────────
save Proofs/Audit.lean; save PROVENANCE.json
python3 - <<'PY'
a = open("Proofs/Audit.lean").read()
i = a.index("elab \"auditCones\"")
open("Proofs/Audit.lean","w").write(
a[:i] + "axiom cheat12 : ∀ (P : Prop), P\ntheorem audit_proves_false : False := cheat12 _\n\n" + a[i:])
PY
repin_audit
./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
restore
echo "✓ attack 12 rejected (the auditor audits itself — no exemption)"
# ── 13: stub the compiler harness (round-5 NEW-3) ──────────────────────────
# Previously: ALL GREEN in 3.6s with the proofs destroyed. lean-guard is KEPT
# (it is this machine's memory cap) and sha256-pinned instead.
save lean-guard
cat > lean-guard <<'EOF'
#!/usr/bin/env bash
echo "exact-cone audit PASSED"
exit 0
EOF
chmod +x lean-guard
./check.sh > /tmp/sf13.out 2>&1 && fail "ATTACK 13 SUCCEEDED: a stubbed harness passed!" /tmp/sf13.out
grep -q "INTEGRITY FAILED" /tmp/sf13.out || fail "ATTACK 13: rejected but not via the harness pin" /tmp/sf13.out
restore
echo "✓ attack 13 rejected (Phase 0 pins lean-guard — the harness is in the TCB and bound)"
# ── 14: stray .lean beside check.sh (round-5 NEW-4) ────────────────────────
cat > Evil.lean <<'EOF'
axiom cheat14 : ∀ (P : Prop), P
theorem evil14 : False := cheat14 _
EOF
./check.sh > /tmp/sf14.out 2>&1 && fail "ATTACK 14 SUCCEEDED: a stray Lean module passed!" /tmp/sf14.out
grep -q "BUILD HYGIENE FAILED" /tmp/sf14.out || fail "ATTACK 14: rejected but not via the hygiene gate" /tmp/sf14.out
rm -f Evil.lean
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) ───────────────
# 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
# that any change to one necessarily moves the SHA-256 (round-5 NEW-2).
./check.sh > /tmp/sf15.out 2>&1 || fail "ATTACK 15 setup: clean tree is not green" /tmp/sf15.out
lake_out=$(cd "$AENEAS_HOME/backends/lean" 2>/dev/null && lake env bash -c \
"cd '$HERE' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD/gen:\$PWD\" && '$HERE/lean-guard' 'Proofs/Audit.lean'" 2>&1)
BLOCK=$(awk '/AUDIT-MANIFEST-BEGIN/{f=1;next} /AUDIT-MANIFEST-END/{f=0} f' <<<"$lake_out")
[ -n "$BLOCK" ] || { echo "✗ ATTACK 15: no audit block"; exit 1; }
for fold in chainFoldN wotsChainFold xmssFoldN htFoldN forsInnerFold forsOuterFold \
toIntFold toByteFold wotsCsumFold base2bOuterFold slhVerifyRoot htVerifyRoot; do
grep -q "spec|fips205.$fold|def|value=" <<<"$BLOCK" \
|| { echo "✗ CHECK 15: the digest input does NOT carry the body of $fold"; exit 1; }
done
# Lean's equation compiler splits a recursive definition: `chainFoldN` is a thin
# wrapper and the actual recursion lives in `chainFoldN._f`. BOTH are reached by
# the closure and printed, so assert the SEMANTIC content specifically — the
# extracted primitives a fold must call — rather than assuming which line holds
# it. (An earlier revision of this check asserted the body text was on the
# wrapper's line and failed while coverage was in fact correct.)
for probe in "fips205.chainFoldN._f|def|value=.*set_hash_address" \
"fips205.xmssFoldN._f|def|value=.*verify_mono.oracle.h" \
"fips205.slhVerifyRoot|def|value=.*verify_mono.oracle.h_msg"; do
grep -qE "spec\|$probe" <<<"$BLOCK" \
|| { echo "✗ CHECK 15: the digest input is missing expected body content: $probe"; exit 1; }
done
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
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 "statements, hand-edited models, dropped manifest rows, widened policy,"
echo "specification folds redefined to the loop, a False-proof in the auditor,"
echo "a stubbed harness, and stray modules."