fips205-slhdsa-verified/verification/inventory_gate.sh

117 lines
6.2 KiB
Bash
Raw Permalink Normal View History

Phase 3c: declaration coverage in both directions, and the accounting identity Completes the round-8 hardening of this repository. Round-8 review (Claude, register keys `drv-surface-no-cones` and `accounting-certifies-enumeration`). WHAT PHASE 3 DID NOT PIN. It proves each certificate's cone is exact and that no declaration in scope carries a disallowed axiom. It does not pin WHICH declarations exist: a new one that happens to be clean, and a silently vanished one, both pass it. inventory-allowlist.txt 265 rows — the audited corpus driver-allowlist.txt 35 rows — the audit INSTRUMENT's own surface both as INV|module|name|kind|CONE, diffed in BOTH directions by inventory_gate.sh, the same implementation the ed25519 repositories use, with a tag per surface. The instrument surface carries cones because the reviewer showed enumeration is not audit: a claim planted in an instrument is counted by an accounting identity and then examined by nothing, if its row carries no cone and no allowlist covers it. Here the instrument's 35 declarations are pinned exactly as the corpus's 265 are. INTERNAL NAMES ARE NO LONGER EXEMPT from the environment walk. They were skipped, which was harmless while nothing compared that walk against the kernel's view — and became a hole the moment something did: Phase 3b reads object files, which contain the compiler's auxiliaries. Exempting them would have left the accounting identity permanently short and forced the residual to be "explained" by a constant. That is the shape of the fudge term four-fork data refuted in the ed25519 repositories, and it is refused here before it can start. THE ACCOUNTING IDENTITY, as SET CONTAINMENT and never arithmetic: every constant the kernel holds must appear in one of the two walks. The kernel gate now emits KERNEL-NAME rows so the comparison names what is missing rather than reporting a count that has to be interpreted. kernel 300 = inventory 265 + instrument 35 residual: none Negative-tested, all three rejected by name and the tree restored to green: · a deleted INV row -> UNCLASSIFIED: INV|Proofs.ApexSpec|List.allM.eq_1|theorem| · a deleted DRV row -> UNCLASSIFIED: DRV|Proofs.Audit|SlhVerify.Audit.sortNames|def · a row with no declaration behind it -> STALE: …|fips205.ghost_that_does_not_exist|… Both allowlists join the pinned harness set: not executable, so the executable-bit rule cannot reach them, and an allowlist an attacker may rewrite pins nothing. fips205-slhdsa-verified now has the ed25519 repositories' gate set: 0 hygiene, 0d correspondence, 1 model, 2 proofs, 3 in-Lean audit, 3b kernel-side axiom gate, 3c coverage + accounting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 15:34:00 +00:00
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# inventory_gate.sh — diff an observed environment inventory against the
# pinned allowlist. PORTED VERBATIM from ltl-accumulator-verified apart from
# the axiom-surface assertion, which is repo-specific: there the corpus admits
# exactly one sanctioned axiom, here it admits none.
#
# This is THE production coverage gate: check.sh Phase 2c calls it, and the
# self-test exercises this exact script — the tested logic IS the shipping
# logic.
#
# Usage: inventory_gate.sh <observed-lean-output> <allowlist-file> [<tag>]
#
# <tag> defaults to INV — the CORPUS walk. Pass DRV to gate the INSTRUMENTS'
# OWN SURFACE with this same implementation.
#
# WHY THE TAG EXISTS — round-8 review (Claude, register keys
# `drv-surface-no-cones`, `accounting-certifies-enumeration`).
#
# The accounting identity added in round 7 proved every constant the kernel
# sees is ENUMERATED by one of the two walks. The reviewer showed that
# enumeration is not audit: a claim planted in an instrument WAS enumerated —
# `DRV|LTLAccAudit.bait.smuggled|theorem` — and then nothing looked at it,
# because DRV rows carried name and kind and NO CONE, and no allowlist covered
# them. In their words, the identity "converted 36 declarations nobody
# enumerated into 36 declarations nobody examined. That is progress of one
# step, not two."
#
# The second step is here: DRV rows now carry their axiom cone and are pinned
# in a committed allowlist, by THIS gate, in both directions — exactly as the
# corpus is. One implementation, not two, because a second copy of a coverage
# gate is a second thing to drift.
#
# It also retires a heuristic. The driver-surface rule permits a theorem whose
# name extends a constant declared alongside it, since that is what the
# elaborator generates for a definition; the reviewer showed it "breaks in one
# line" — declare `def bait`, then `theorem bait.smuggled` passes. That rule is
# kept as a fast, readable first line of defence, but it is NO LONGER
# LOAD-BEARING: a planted claim now has to appear in the pinned allowlist, and
# a new row fails closed whatever it is named.
#
# Fail-closed in BOTH directions:
# UNCLASSIFIED — constant in the environment, absent from the allowlist
# (new/renamed decl, changed kind, or changed axiom cone)
# STALE — allowlist entry absent from the environment
# plus an output-integrity check: the INV-COUNT trailer emitted by
# Proofs/Inventory.lean must equal the number of INV lines actually seen,
# so a truncated or crashed run can never pass as an empty diff.
# ─────────────────────────────────────────────────────────────────────────────
set -uo pipefail
export LC_ALL=C # byte-order collation: sort/comm must agree with Lean's String order
obs_file="$1"; allow_file="$2"; TAG="${3:-INV}"
case "$TAG" in
INV) WHAT="the audited corpus"; TRAILER_TAG="INV-COUNT"; LABEL="inventory gate"; TRUNCLABEL="INVENTORY TRUNCATED" ;;
DRV) WHAT="the audit instruments"; TRAILER_TAG="DRV-COUNT"; LABEL="driver-surface gate"; TRUNCLABEL="DRIVER SURFACE TRUNCATED" ;;
*) echo " GATE MISUSE: unknown tag '$TAG' (expected INV or DRV)"; exit 1 ;;
esac
# The trailer is an OUTPUT-INTEGRITY check: it must equal the number of rows
# the driver(s) actually emitted, BEFORE de-duplication. Comparing it to the
# de-duplicated count conflates "a run was truncated" with "two rows were
# identical", and the second is a record-format defect that must be fixed at
# the source, not absorbed here. (It was: DRV rows now carry their driver.)
N_RAW=$(grep -c "^$TAG|" "$obs_file" || true)
OBS=$(grep "^$TAG|" "$obs_file" | sort -u)
N_OBS=$(printf '%s' "$OBS" | grep -c "^$TAG|" || true)
if [ "$N_RAW" -ne "$N_OBS" ]; then
echo " DUPLICATE $TAG RECORDS: $N_RAW rows collapse to $N_OBS distinct ones."
echo " Two declarations share a record, so one is covered by the other's entry:"
grep "^$TAG|" "$obs_file" | sort | uniq -d | head -5 | sed 's/^/ /'
exit 1
fi
# Each driver emits its own trailer, so DRV trailers are SUMMED; the corpus
# walk emits one and the last is taken. Either way a truncated or crashed run
# must never pass as an empty diff.
if [ "$TAG" = DRV ]; then
TRAILER=$(grep "^$TRAILER_TAG|" "$obs_file" | cut -d'|' -f2 | paste -sd+ - | bc)
else
TRAILER=$(grep "^$TRAILER_TAG|" "$obs_file" | tail -1 | cut -d'|' -f2)
fi
if [ -z "$TRAILER" ] || [ "$TRAILER" != "$N_RAW" ]; then
echo " $TRUNCLABEL: trailer=${TRAILER:-absent}, observed $N_RAW lines"
exit 1
fi
ALLOW=$(grep "^$TAG|" "$allow_file" | sort -u)
FAILGATE=0
UNCLASS=$(comm -23 <(printf '%s\n' "$OBS") <(printf '%s\n' "$ALLOW"))
STALE=$(comm -13 <(printf '%s\n' "$OBS") <(printf '%s\n' "$ALLOW"))
if [ -n "$UNCLASS" ]; then
printf '%s\n' "$UNCLASS" | sed 's/^/ UNCLASSIFIED (in environment, not allowlisted): /'
FAILGATE=1
fi
if [ -n "$STALE" ]; then
printf '%s\n' "$STALE" | sed 's/^/ STALE (allowlisted, not in environment): /'
FAILGATE=1
fi
# The audited corpus admits NO axiom declarations at all: the sanctioned
# external models live in gen/, outside every module these drivers cover, and
# are byte-pinned by Phase 0b. An axiom appearing here would be a declaration
# smuggled into the proof corpus, which Phase 2b also catches kernel-side —
# two independent gates on the same property, deliberately.
AXLINES=$(printf '%s\n' "$OBS" | grep '|axiom|' || true)
if [ -n "$AXLINES" ]; then
echo " AXIOM SURFACE DRIFT: $WHAT must declare no axioms; observed:"
printf '%s\n' "$AXLINES" | sed 's/^/ /'
FAILGATE=1
fi
# The message must describe what was actually checked. It said "single
# sanctioned axiom" when ported, which is the accumulator's policy; here the
# audited corpus permits NONE, and a success line describing a different rule
# is how an assertion quietly stops meaning anything.
[ "$FAILGATE" = 0 ] && echo " $LABEL: $N_OBS constants, environment == allowlist, zero axioms declared in $WHAT"
exit "$FAILGATE"