fips205-slhdsa-verified/verification/check.sh
mrwulf dce0473013 review round 5: bind the policy, the specification bodies, and the harness
The third reviewer demonstrated that the round-4 gate was closed at the
EXPLOITS, not the MECHANISMS: it bound the manifest rows while the things he
moved sat outside the binding. Three fail-opens, each executed end-to-end with
the button printing ALL GREEN. No theorem, proof, fold, or model body changed.

NEW-1 — the policy constant was unbound. `manifestFingerprint` covered
`manifest` but never `allowedBoundary`, the sole predicate the enumeration
tests against; adding one name re-opened the False-proof with the committed
fingerprint BYTE-IDENTICAL.

NEW-2 — the specification bodies were unbound. The statement fingerprint pinned
each certificate's TYPE, which names its reference fold; redefining that fold to
*be* the extracted loop left cone and type-hash identical while the certificate
degenerated to `loop = loop`. 100% of the fidelity content lives in those defs.

NEW-3 — the harness was unbound. Stubbing verification/lean-guard alone, without
touching check.sh or the proofs, gave ALL GREEN in 3.6s over destroyed proofs.

FIX — one mechanism rather than three patches. Audit.lean now emits a canonical
AUDIT-MANIFEST block and check.sh binds to its SHA-256. The block covers the
POLICY constants, every certificate's fully-elaborated statement, and every
specification constant transitively reachable from those statements with its
fully-elaborated BODY (41 constants; the closure is computed, so a new fold
cannot appear without moving the digest; Prop-valued constants contribute their
statement, by proof irrelevance). This also retires the 32-bit Expr.hash as the
binding (NEW-5) — it survives only as a per-certificate diagnostic.

Enumeration now covers EVERY declaration kind (a `def : False` passed before)
in the eight certificate modules AND in Audit.lean itself — the auditor is no
longer exempt (round-5 R1). A bare `axiom` in audited scope is now an error.

Phase 0 purges stale .olean (the verdict must depend on committed bytes, not
.gitignored build state — NEW-4), forbids any .lean outside gen/ and Proofs/,
and sha256-pins the four model files AND lean-guard. lean-guard is KEPT rather
than removed (the reviewer's portability advice is declined by operator
decision): it is the memory cap and machine-wide lock that protect the build
machine after a 12.2GB OOM took the host down. That trade-off is documented.

check.sh's "Certificates proven:" line now comes from the audited manifest; the
hand-kept CERTS array — the one authoritative claim string nothing bound — is
deleted.

check-selftest.sh: 14 attacks, all rejected, plus a check that the hashed block
literally carries the twelve fold bodies. Attacks 9-14 are the reviewers' and an
independent drill's own exploits, turned into regression tests.

DOCS. TRUSTED-BASE gains item 11 (the REAL trusted computing base — lean-guard
pinned; check.sh, the toolchain env, $AENEAS_HOME, python3 and Lean still
trusted) and item 12 (the apex does not compose the ten). README: the audit
description rewritten; the XMSS sibling-order claim downgraded from "pins" to
"makes visible", with a new blanket non-claim covering all ten loop
certificates; the de-plumbing file claim corrected (round 1 touched only
verify_mono.rs, round 2 only helpers.rs — which is ON the deployed verify AND
sign paths, now disclosed; wots.rs was never patched).

RECORDED-RUN: three lines that stood inside a fence were a hand-written summary,
not console output — fabricated evidence in the file whose purpose is machine
evidence. They are removed and the fabrication is named in place, together with
the correction that the "INDEPENDENT RUN" block predates this gate. New rule:
nothing goes in a fence unless captured with tee/cat, and every block states its
date, pin, and who ran it. The transcripts added here follow it.

Also disclosed rather than buried: three bugs in my own test harness this round
(an olean-purge build-order break, an attack rejected by the wrong rule, and a
coverage assertion looking on the wrong line) — each would have let an attack
pass or fail for an unrelated reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 22:57:46 +02:00

160 lines
8.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# The one-button claim for this repository (rigor invariant R3).
# Green output == the full claim. This script is the ONLY source of the
# word "proven" for this repo.
#
# Phase 0 — build hygiene + integrity: purge stale .olean (the verdict must
# depend on committed bytes, not untracked build state), forbid any
# .lean outside gen/ and Proofs/, and sha256-pin the four model files
# AND the compiler harness `lean-guard` to PROVENANCE.json.
# Phase 1 — compile the extracted Lean model (gen/SlhVerify).
# Phase 2 — compile the proof files (Proofs/).
# Phase 3 — the in-Lean audit (Proofs/Audit.lean): per certificate, the cone
# (via `collectAxioms`) must EQUAL its expected set exactly; EVERY
# declaration kind in the eight cert modules and in Audit.lean itself
# must stay within the axiom boundary; and this script binds to the
# SHA-256 of the canonical AUDIT-MANIFEST block, which covers the
# POLICY constants, every certificate STATEMENT, and every reachable
# SPECIFICATION DEFINITION BODY. Any mismatch → non-zero exit →
# fail-closed. No text parsing of axiom cones.
#
# What this button does NOT bind is stated in TRUSTED-BASE.md item 11: this
# script itself, the toolchain env, $AENEAS_HOME, and the Lean toolchain.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
source ~/aeneas-toolchain/env.sh
AENEAS_LEAN="$AENEAS_HOME/backends/lean"
TIMEOUT="${LEAN_TIMEOUT:-400}"
MEM="${LEAN_MEM_MB:-4096}"
GEN_MODULES=(
"SlhVerify/TypesExternal"
"SlhVerify/Types"
"SlhVerify/FunsExternal"
"SlhVerify/Funs"
)
# Proof files, in dependency order.
PROOFS=(
"ChainSpec"
"WotsSpec"
"XmssSpec"
"HtSpec"
"ForsInnerSpec"
"ForsOuterSpec"
"InputPrepSpec"
"ApexSpec"
)
echo "fips205-slhdsa-verified — check"
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
# on COMMITTED BYTES, never on untracked build state — a stale .olean from a
# module that no longer exists (and *.olean is .gitignored, so invisible to
# `git status`) could otherwise satisfy an import and go green.
find "$HERE/gen" "$HERE/Proofs" -name '*.olean' -delete 2>/dev/null || true
# (b) No Lean source may sit outside gen/ and Proofs/. LEAN_PATH includes $PWD,
# so a stray verification/*.lean can join the environment ungated (NEW-4).
STRAY=$(find "$HERE" -maxdepth 1 -name '*.lean' -printf '%f\n' 2>/dev/null || true)
if [ -n "$STRAY" ]; then
echo "$STRAY" | sed 's/^/ ✗ stray Lean source outside gen\/ and Proofs\/: /'
echo "BUILD HYGIENE FAILED (a .lean outside the audited directories can join LEAN_PATH)"; exit 1
fi
# (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
# trusted computing base: round 5 demonstrated that stubbing it alone yields
# ALL GREEN in 3.6s over destroyed proofs. It is KEPT (it is the memory cap
# that protects this machine after the 12.2GB OOM incident) and pinned.
python3 - "$HERE/PROVENANCE.json" "$HERE" <<'PY' || { echo "INTEGRITY FAILED (a pinned file differs from PROVENANCE.json — hand-edited model or harness?)"; 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 not k.startswith("_")}
files.update({k: v for k, v in prov.get("harness_integrity_sha256", {}).items() if not k.startswith("_")})
if not files:
print(" no integrity map 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 ──────────────────────────────────────────────────────────
echo "=== Phase 1: compile the extracted model ==="
cd "$AENEAS_LEAN"
lake env bash -c "
set -euo pipefail
cd '$HERE' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD/gen:\$PWD\"
compile() { echo \" · \$1\"; LEAN_TIMEOUT=$TIMEOUT LEAN_MEM_MB=$MEM '$HERE/lean-guard' \"\${1}.lean\" >/dev/null || { echo \"FAIL: \$1\"; exit 1; }; }
for m in ${GEN_MODULES[*]}; do compile \"gen/\$m\"; done
"
# ── Phase 2: proofs ─────────────────────────────────────────────────────────
echo "=== Phase 2: compile the proofs ==="
cd "$AENEAS_LEAN"
lake env bash -c "
set -euo pipefail
cd '$HERE' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD/gen:\$PWD\"
compile() { echo \" · \$1\"; LEAN_TIMEOUT=$TIMEOUT LEAN_MEM_MB=$MEM '$HERE/lean-guard' \"Proofs/\${1}.lean\" >/dev/null || { echo \"FAIL: Proofs/\$1\"; exit 1; }; }
for m in ${PROOFS[*]}; do compile \"\$m\"; done
# no dead proof files: everything under Proofs/ must be in the manifest
# (PROOFS) or be the audit driver (Audit, compiled in Phase 3).
for f in Proofs/*.lean; do b=\$(basename \"\$f\" .lean)
case \" ${PROOFS[*]} Audit \" in *\" \$b \"*) ;; *) echo \"DEAD FILE: Proofs/\$b.lean not in manifest\"; exit 1 ;; esac
done
"
# ── Phase 3: in-Lean audit (cones + statements + full-module enumeration) ────
echo "=== Phase 3: in-Lean audit (cones + statement fingerprints + enumeration) ==="
cd "$AENEAS_LEAN"
# THE BINDING DIGEST. Audit.lean emits a canonical AUDIT-MANIFEST block holding
# the POLICY constants (allowedBoundary, certModules — round-5 NEW-1), 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 every fingerprint intact). We bind
# to the SHA-256 of that block, retiring the 32-bit Expr.hash as the load-bearing
# digest (NEW-5). To rotate deliberately: run check.sh, take the printed OBSERVED
# digest, and update this constant in the same reviewable commit.
EXPECTED_AUDIT_SHA256="d83e297a49094c970b88ce7c63ceb85d6bee0764d4623456a7225860e2298afa"
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 "AUDIT FAILED (Audit.lean did not compile — cone/statement mismatch, missing cert, sham, or un-audited declaration)"; exit 1; }
if ! grep -qF "exact-cone audit PASSED" <<<"$AUD_OUT"; then
echo "$AUD_OUT" | sed 's/^/ /'
echo "AUDIT FAILED (no PASSED line — fail-closed)"; exit 1
fi
BLOCK=$(awk '/AUDIT-MANIFEST-BEGIN/{f=1;next} /AUDIT-MANIFEST-END/{f=0} f' <<<"$AUD_OUT")
if [ -z "$BLOCK" ]; then
echo "$AUD_OUT" | sed 's/^/ /'
echo "AUDIT FAILED (no AUDIT-MANIFEST block — fail-closed)"; exit 1
fi
GOT_SHA=$(printf '%s\n' "$BLOCK" | sha256sum | cut -d' ' -f1)
if [ "$GOT_SHA" != "$EXPECTED_AUDIT_SHA256" ]; then
printf '%s\n' "$BLOCK" > "$HERE/.audit-manifest.observed"
echo "AUDIT FAILED — audit-manifest digest mismatch."
echo " expected: $EXPECTED_AUDIT_SHA256"
echo " observed: $GOT_SHA"
echo " A policy constant, a certificate statement, or a specification"
echo " definition changed without a reviewed rotation. The observed block was"
echo " written to verification/.audit-manifest.observed — diff it to see what."
exit 1
fi
echo "$(grep -oF 'exact-cone audit PASSED' <<<"$AUD_OUT" | head -1)"
echo " ✓ audit-manifest digest matches (sha256 ${EXPECTED_AUDIT_SHA256:0:16}…)"
echo
echo "ALL GREEN — model compiles, proofs compile, and every certificate cone"
echo "equals EXACTLY the three kernel axioms plus its documented SHA-2 oracles."
# The list comes from the AUDITED manifest itself, never from a hand-kept array:
# a display list nothing binds can name a certificate that does not exist.
CERT_LINE=$(sed -n 's/.*CERTIFICATES: //p' <<<"$AUD_OUT" | head -1)
[ -n "$CERT_LINE" ] || { echo "AUDIT FAILED (no CERTIFICATES line — fail-closed)"; exit 1; }
echo "Certificates proven: $CERT_LINE"