mirror of
https://github.com/saymrwulf/fips205-slhdsa-verified.git
synced 2026-09-03 19:53:49 +00:00
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>
116 lines
6.2 KiB
Bash
Executable file
116 lines
6.2 KiB
Bash
Executable file
#!/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"
|