mirror of
https://github.com/saymrwulf/dalek-ed25519-verified.git
synced 2026-09-03 20:13:48 +00:00
Every gate this repository has was executed by scripts that nothing pinned.
Round-5 review of the companion SLH-DSA repository stubbed the compiler
wrapper alone and its button printed ALL GREEN in 3.6 seconds over
deliberately destroyed proofs; flipping two guards in the audit driver
disabled every check with the digest byte-identical. Depth of checking is
worth nothing if the thing doing the checking is unbound — and every gate
added this week made that gap more valuable to an attacker, not less.
Phase 0c requires every harness file to match HARNESS.sha256. Two design
points carry the weight:
- WHICH files must be pinned is POLICY and lives in check.sh, never in the
map being consulted. If the required set were read from the pin file,
deleting an entry would silently un-pin that file. It is instead derived
from the filesystem, so a deleted entry is a set mismatch and a build
failure. That is the exact defect SLH-DSA round-6 found, closed here by
construction.
- Membership self-derives from the executable bit: anything this script can
shell out to must be pinned, so a NEW script fails closed until someone
pins it deliberately. Load-bearing files that are not executable — the
audit driver, the committed manifests, the policy tables — cannot be
discovered that way and are listed explicitly.
lean-guard is inside the set, which finally makes the standing "lean-guard
stays hash-pinned" rule a property of the repository rather than a convention.
selftest-harness.sh replays five cases, each asserting a specific diagnostic:
an edited lean-guard, a new unpinned executable, a deleted pin entry, a
missing pin file, and a positive control. It was itself negative-tested — with
the hash comparison removed it goes red on exactly that case while cheerfully
reporting "10 harness files match their pins".
TRUSTED-BASE.md states the limit at equal length to the claim: pinning a
harness from inside that harness is circular, and an author who edits a script
and refreshes its pin in the same commit passes every phase. What the pin
changes is that the edit can no longer be SILENT — it must appear in the diff
at the commit being reviewed. A green button says "this is the apparatus that
was reviewed", never "this apparatus is trustworthy".
Also fixed, found by this sweep: both self-tests compared the working tree
against its starting state with `diff <(echo "$VAR") <(command)`, which is
asymmetric — for a clean tree the variable is empty and `echo` emits a blank
line the command does not. It reported a difference precisely when nothing was
wrong, and only surfaced once P1-a was committed and Proofs/ became clean.
Both now compare as strings.
Verified green: 20 runs across the four ed25519 repositories (four buttons,
four harness self-tests, four axiom-gate self-tests, four binding self-tests,
four scalar buttons), zero red.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
135 lines
6.1 KiB
Bash
Executable file
135 lines
6.1 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# selftest-axgate.sh — adversarial self-test for check.sh Phase 2b.
|
|
#
|
|
# An untested guard is decoration. This script breaks the thing Phase 2b
|
|
# guards and asserts the gate goes red FOR THE STATED REASON — a rejection by
|
|
# some other gate, or with some other message, fails the test too.
|
|
#
|
|
# It extracts Phase 2b out of check.sh at run time, so it attacks THE SHIPPING
|
|
# GATE rather than a copy that can drift away from it.
|
|
#
|
|
# Requires: Proofs/*.olean already built (run check.sh first, or any prior
|
|
# green build). Takes ~10 s; compiles one tiny throwaway module.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
set -uo pipefail
|
|
source ~/aeneas-toolchain/env.sh
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
AENEAS_LEAN="$AENEAS_HOME/backends/lean"
|
|
TIMEOUT="${LEAN_TIMEOUT:-600}"
|
|
export LEAN_MEM_MB="${LEAN_MEM_MB:-8192}"
|
|
CORES="${LEAN_MAX_CORES:-0-3}"
|
|
|
|
ATTACK="$HERE/Proofs/ZZSelftestAttack.lean"
|
|
STASH="$(mktemp -d)"
|
|
FAILURES=0
|
|
# Recorded before anything is touched, so the restore check compares against
|
|
# reality rather than assuming a pristine checkout.
|
|
TREE_AT_START="$(cd "$(dirname "$0")/.." && git status --porcelain -- verification/Proofs)"
|
|
|
|
cleanup() {
|
|
rm -f "$ATTACK" "${ATTACK%.lean}.olean"
|
|
[ -f "$STASH/FeQ.olean" ] && mv "$STASH/FeQ.olean" "$HERE/Proofs/FeQ.olean"
|
|
rm -rf "$STASH"
|
|
rm -f "$HERE"/.axgate-*.lean "$HERE"/.axgate-*.olean
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# Phase 2b, lifted verbatim from the shipping button.
|
|
DRIVER="$STASH/phase2b.sh"
|
|
{
|
|
echo 'set -euo pipefail'
|
|
echo 'source ~/aeneas-toolchain/env.sh'
|
|
echo "HERE=\"$HERE\""
|
|
echo 'AENEAS_LEAN="$AENEAS_HOME/backends/lean"'
|
|
echo "TIMEOUT=$TIMEOUT; CORES=\"$CORES\""
|
|
sed -n '/^# ── Phase 2b/,/^# ── Phase 3/p' "$HERE/check.sh" | sed '$d'
|
|
} > "$DRIVER"
|
|
if [ "$(wc -l < "$DRIVER")" -lt 40 ]; then
|
|
echo "FATAL: could not lift Phase 2b out of check.sh — the phase markers moved."
|
|
echo "This self-test must attack the shipping gate; refusing to run against nothing."
|
|
exit 1
|
|
fi
|
|
|
|
expect() { # expect <name> <expected-rc> <required-substring>
|
|
local name="$1" want_rc="$2" want_txt="$3"
|
|
local out rc
|
|
out=$(bash "$DRIVER" 2>&1); rc=$?
|
|
if [ "$rc" -ne "$want_rc" ]; then
|
|
echo " FAIL $name: exit $rc, expected $want_rc"; FAILURES=$((FAILURES+1)); return
|
|
fi
|
|
if ! grep -qF "$want_txt" <<<"$out"; then
|
|
echo " FAIL $name: exit code right but diagnostic wrong (rejected for the wrong reason)"
|
|
echo " wanted substring: $want_txt"
|
|
echo " got: $(tr '\n' '|' <<<"$out" | cut -c1-300)"
|
|
FAILURES=$((FAILURES+1)); return
|
|
fi
|
|
echo " ok $name"
|
|
}
|
|
|
|
echo "=== selftest-axgate: attacking check.sh Phase 2b ==="
|
|
|
|
# ── 1. Baseline: the untouched repo must pass, and say how much it covered.
|
|
expect "baseline green, coverage reported" 0 "none is an axiom"
|
|
|
|
# ── 2. The attack Phase 1's grep cannot see: an indented top-level axiom.
|
|
# Lean accepts it; the repo then proves False; the source-text gate is blind.
|
|
cat > "$ATTACK" <<'EOF'
|
|
namespace ZZSelftestAttack
|
|
axiom cheat : ∀ (P : Prop), P
|
|
theorem repo_proves_false : False := cheat _
|
|
end ZZSelftestAttack
|
|
EOF
|
|
if grep -rnE '^(private |protected |noncomputable )*axiom ' "$HERE"/Proofs/*.lean >/dev/null 2>&1; then
|
|
echo " FAIL premise: Phase 1's grep sees the attack — this test no longer tests what it claims"
|
|
FAILURES=$((FAILURES+1))
|
|
else
|
|
echo " ok premise: Phase 1's source-text grep is blind to this attack"
|
|
fi
|
|
(cd "$AENEAS_LEAN" && lake env bash -c "
|
|
set -euo pipefail
|
|
cd '$HERE/gen' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD:$HERE\"
|
|
cd '$HERE'
|
|
LEAN_TIMEOUT=$TIMEOUT LEAN_MAX_CORES=$CORES '$HERE/lean-guard' 'Proofs/ZZSelftestAttack.lean'
|
|
") >/dev/null 2>&1 || { echo " FAIL setup: the attack module did not compile"; FAILURES=$((FAILURES+1)); }
|
|
expect "indented axiom caught kernel-side" 1 "AXIOM DECLARED under Proofs/"
|
|
rm -f "$ATTACK" "${ATTACK%.lean}.olean"
|
|
|
|
# ── 3. Vacuity: delete a compiled module. "Nothing found" must not pass for
|
|
# "nothing wrong" — the gate has to notice it stopped covering something.
|
|
mv "$HERE/Proofs/FeQ.olean" "$STASH/FeQ.olean"
|
|
expect "missing .olean is a failure, not a vacuous pass" 1 "COVERAGE MISMATCH"
|
|
mv "$STASH/FeQ.olean" "$HERE/Proofs/FeQ.olean"
|
|
|
|
# ── 4. Litter: neither path may leave the temp gate source or its artifact
|
|
# behind (this repo accumulated 101 orphan .olean files exactly that way).
|
|
if ls "$HERE"/.axgate-* >/dev/null 2>&1; then
|
|
echo " FAIL litter: temp gate files survived a run"; FAILURES=$((FAILURES+1))
|
|
else
|
|
echo " ok no litter left by either the green or the red path"
|
|
fi
|
|
|
|
# ── 5. Restored: the self-test must leave the working tree exactly as it found
|
|
# it. Compared against the state recorded at START, not against a pristine
|
|
# checkout — files can be legitimately uncommitted while work is in flight,
|
|
# and a test that assumes otherwise reports its own premise as a failure.
|
|
# Compare the two states AS STRINGS. Comparing `echo "$VAR"` against a raw
|
|
# command substitution is asymmetric: for a clean tree the variable is empty
|
|
# and `echo` still emits one blank line while the command emits none, so the
|
|
# check reports a spurious difference exactly when nothing is wrong.
|
|
TREE_NOW="$(cd "$HERE/.." && git status --porcelain -- verification/Proofs)"
|
|
if [ "$TREE_AT_START" != "$TREE_NOW" ]; then
|
|
echo " FAIL restore: Proofs/ differs from how this test found it:"
|
|
diff <(printf '%s\n' "$TREE_AT_START") <(printf '%s\n' "$TREE_NOW") | sed 's/^/ /'
|
|
FAILURES=$((FAILURES+1))
|
|
else
|
|
echo " ok working tree restored to its starting state"
|
|
fi
|
|
|
|
echo ""
|
|
if [ "$FAILURES" -eq 0 ]; then
|
|
echo "SELFTEST PASSED — Phase 2b rejects what it claims to reject, for the stated reason."
|
|
exit 0
|
|
fi
|
|
echo "SELFTEST FAILED: $FAILURES check(s) did not behave as claimed."
|
|
exit 1
|