mirror of
https://github.com/saymrwulf/fips205-slhdsa-verified.git
synced 2026-09-04 20:03:44 +00:00
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>
191 lines
10 KiB
Bash
Executable file
191 lines
10 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 under verification/ — round-6 NEW-8: the round-5 purge
|
|
# covered only gen/ and Proofs/ while the stray check greped only *.lean, so
|
|
# an ORPHAN `verification/Evil.olean` WITH NO SOURCE AT ALL fell between them,
|
|
# satisfied an `import Evil`, and went ALL GREEN with the digest untouched
|
|
# (*.olean is .gitignored, so `git status` showed only the import line).
|
|
# The verdict must depend on COMMITTED BYTES, never on untracked build state.
|
|
find "$HERE" -name '*.olean' -delete 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
|
|
echo "$STRAY" | sed 's/^/ ✗ stray Lean file outside gen\/ and Proofs\/: /'
|
|
echo "BUILD HYGIENE FAILED (a .lean/.olean 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}")
|
|
# 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)
|
|
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."
|
|
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
|
|
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 " ✓ 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"
|