risc0-ed25519-verified/verification/Proofs/ScalarAudit.lean
mrwulf fa9da37f11 audit: bind the scalar statements, and make the accounting identity mean audit
Closes four round-7/8 findings. Certified by the round-12 sweep: five
repositories, both buttons and every self-test, 48/48 GREEN.

── `scalar-statements-unbound` (gpt, round 7, CRITICAL) ────────────────────
The main button bound its 31 certificates' elaborated statements and reachable
specification bodies. This button bound NONE of its thirteen, while
TRUSTED-BASE item 8 said the audit covers "every certificate" — false across
the 44-certificate surface. The finding was raised in round 7, lost from the
round-8 work list by an F-number collision between two reviewers, and re-raised
in round 8.

Proofs/ScalarAudit.lean is generated from each fork's OWN Audit.lean, so the
canonicalisation is provably the same code: pp.all rendering, whitespace
normalisation, transitive specification closure. check-scalar.sh Phase 3c pins
the block's digest, requires the committed copy to match byte-for-byte so a
mismatch can be DIFFED, and cross-checks the auditor's certificate set against
the button's CERTS array.

  dalek ecf3a3f8 · anza 0d942e47 · risc0 4b550a61 · betrusted 4b550a61

risc0 and betrusted share a digest and that is correct, not a collision: their
ScalarSubSpec.lean differs only in doc prose and in `black_box` entries inside
`simp only [...]` lists AFTER `:= by`. Proof scripts. They bind the same
statements over the same specifications, which is the documented scope.

selftest-scalar-statements.sh ships the two attacks the reviewer asked for:

  ok  gutted statement caught (cone unchanged)
  ok  rewritten specification body caught (name and cone unchanged)

The second rewrites a reachable reference body to `id (…)` — DEFINITIONALLY
EQUAL, so the corpus compiles and every proof typechecks and the cone is
byte-identical. Every earlier phase is blind to it.

── `drv-surface-no-cones` + `accounting-certifies-enumeration` (claude) ────
The round-7 accounting identity proved every kernel constant was ENUMERATED.
The reviewer showed enumeration is not audit: their planted claim WAS
enumerated, as DRV|LTLAccAudit.bait.smuggled|theorem with a real cone, and
nothing examined it — rows had no cone, no allowlist covered them, the
statement digest does not reach instruments, and Phase 2b gates DECLARED
AXIOMS, a different question. "Progress of one step, not two."

DRV rows now carry their axiom cone and are pinned in driver-allowlist.txt by
inventory_gate.sh with a DRV tag — the same implementation that pins the
corpus, in both directions, because a second copy of a coverage gate is a
second thing to drift. The axiom policy is per-surface and enforced per
surface: the corpus admits exactly the sanctioned boundary, the instruments
admit none, and an instrument axiom fails EVEN WHEN ALLOWLISTED.

Verified with the reviewer's own payload, both placements:
  before the walk -> UNCLASSIFIED: DRV|…|bait.smuggled|theorem|Classical.choice,Quot.sound,propext
  after  the walk -> ACCOUNTING FAILED names it (kernel-side)

── `drv-naming-heuristic` (claude, round 7) ────────────────────────────────
Retired as load-bearing rather than patched. The rule admits a theorem whose
name extends a constant declared alongside it, and "breaks in one line" —
declare `def bait`, then `theorem bait.smuggled` walks through. It stays as a
fast readable first check; membership in a committed allowlist is what now
carries the weight, and a new row fails closed whatever it is called.

── what round 11 caught, which was mine ───────────────────────────────────
DRV rows first shipped WITHOUT their originating driver. dalek and anza run
two drivers, each declaring its own `corpus`; keyed on name alone those two
distinct declarations produced one byte-identical row, `sort -u` collapsed
them, and the trailers summed to 37 against 36. The estate had already learned
this on the corpus walk — INV rows carry their module because two modules both
declare CurveFieldProofs.zero_spec — and I rebuilt the record without it.

Rows now carry their driver, and the gate FAILS CLOSED ON DUPLICATE RECORDS
naming the collision: two declarations sharing one entry means one is covered
by the other's, which is exactly how a real declaration hides. The trailer
now checks what the drivers EMITTED, not what survives de-duplication —
conflating "the run was truncated" with "two rows were identical" is what let
a record-format defect present itself as an arithmetic complaint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 12:15:26 +02:00

171 lines
8.1 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Proofs.ScalarDenote
import Proofs.ScalarLoop
import Proofs.ScalarSubSpec
import Proofs.ScalarAddSpec
import Proofs.ScalarMulSpec
import Proofs.ScalarMontSpec
import Proofs.ScalarReduceSpec
import Proofs.ScalarFullMulSpec
import Proofs.ScalarMain
import Proofs.ScalarWideSpec
import Proofs.ScalarBytesSpec
import Proofs.ScalarUnpackSpec
import Proofs.ScalarFromBytesSpec
import Lean
open Lean Elab Command
namespace Ed25519ScalarAudit
/-- Lean's three kernel axioms. -/
def kernel3 : List Name := [`propext, `Classical.choice, `Quot.sound]
/-- This fork's apex boundary: the hash oracle and wire-format symbols the
signature-level certificates are permitted to rest on, and nothing else.
POLICY CONSTANT — folded into the digest, so widening it moves the hash
and fails the build. -/
def apexExtra : List Name := [] -- the scalar layer has NO apex tier
def apexBoundary : List Name := kernel3 ++ apexExtra
/-- A constant counts as SPECIFICATION if it was declared in a `Proofs.`
module — i.e. hand-written by us, as opposed to the extracted model in
`gen/` (pinned separately by Phase 0). Derived from the environment, not
from a list, so a new specification module cannot appear unnoticed. -/
def specPrefix : String := "Proofs."
/-- Per-certificate expected cone. Arithmetic tier first, apex tier last. -/
def manifest : List (Name × List Name) :=
[ (`ScalarProofs.L_val, kernel3)
, (`ScalarProofs.sub_loop_spec, kernel3)
, (`ScalarProofs.sub_loop1_one_spec, kernel3)
, (`ScalarProofs.sub_val_spec, kernel3)
, (`ScalarProofs.add_val_spec, kernel3)
, (`ScalarProofs.mul_internal_spec, kernel3)
, (`ScalarProofs.part1_spec, kernel3)
, (`ScalarProofs.montgomery_reduce_spec, kernel3)
, (`ScalarProofs.mul_spec, kernel3)
, (`ScalarProofs.scalarImplementation, kernel3)
, (`ScalarProofs.montgomery_mul_spec, kernel3)
, (`ScalarProofs.bytes_unpack_spec, kernel3)
, (`ScalarProofs.from_bytes_wide_spec, kernel3)
]
/-- Deterministic name ordering for the canonical serialization. -/
def sortNames (l : List Name) : List Name :=
((l.map toString).toArray.qsort (· < ·)).toList.map (·.toName)
/-- Whitespace-canonical: every whitespace run collapses to one space, so the
pretty-printer's line wrapping cannot perturb the digest. -/
def normWs (s : String) : String :=
(s.foldl (fun (acc : String × Bool) c =>
let c := if c.isWhitespace then ' ' else c
if c == ' ' then (if acc.2 then acc else (acc.1.push ' ', true))
else (acc.1.push c, false))
("", true)).1
/-- Was `n` hand-written by us, in a `Proofs.` module? -/
def isSpecConst (env : Environment) (n : Name) : Bool :=
match env.getModuleIdxFor? n with
| some idx => (toString env.header.moduleNames[idx.toNat]!).startsWith specPrefix
| none => false
/-- Transitive closure over specification constants, starting from a
certificate's STATEMENT and following DEFINITION bodies (a theorem
contributes its statement only). This discovers the reference definitions —
and any future one — automatically, so a new specification cannot be
introduced, or an existing one redefined, without moving the digest. -/
partial def closureOf (env : Environment) (seen : NameSet) (work : List Name) : NameSet :=
match work with
| [] => seen
| n :: rest =>
if seen.contains n || !isSpecConst env n then closureOf env seen rest
else
let seen := seen.insert n
let more := match env.find? n with
| some (.defnInfo v) => v.value.getUsedConstants.toList ++ v.type.getUsedConstants.toList
| some ci => ci.type.getUsedConstants.toList
| none => []
closureOf env seen (more ++ rest)
/-- Fully-explicit (`pp.all`) rendering, whitespace-canonicalized. Implicit
arguments, instances and universe levels are all made visible, so two
statements that merely LOOK alike cannot share a rendering. -/
def ppAll (e : Expr) : CommandElabM String := do
let s ← Command.liftCoreM <| Meta.MetaM.run' <|
withOptions (fun o => o.setBool `pp.all true) do
return (← Meta.ppExpr e).pretty
return normWs s
elab "auditScalarStatements" : command => do
let env ← getEnv
let mut errs : Array String := #[]
-- (0) The manifest may not permit an axiom outside the two declared tiers.
-- Without this, widening a cone in the manifest would be invisible.
for (cert, cone) in manifest do
for a in cone do
unless apexBoundary.contains a do
errs := errs.push s!"manifest permits {a} for {cert}, which is outside every declared tier"
-- (1) Each certificate must EXIST, be a THEOREM, and have EXACTLY its cone.
-- Exact, not subset: a certificate that stopped depending on the hash
-- oracle is as wrong as one that acquired a new axiom.
for (cert, expected) in manifest do
match env.find? cert with
| none => errs := errs.push s!"{cert}: NOT FOUND (renamed or deleted?)"
| some (.thmInfo _) =>
let got := (← collectAxioms cert).toList
let extras := got.filter (fun a => !expected.contains a)
let missing := expected.filter (fun a => !got.contains a)
unless extras.isEmpty && missing.isEmpty do
errs := errs.push s!"{cert}: cone extra={extras} missing={missing}"
| some (.axiomInfo _) => errs := errs.push s!"{cert}: is an AXIOM, not a proven theorem"
| some (.opaqueInfo _) => errs := errs.push s!"{cert}: is OPAQUE, not a proven theorem"
| some _ => errs := errs.push s!"{cert}: is not a theorem"
unless errs.isEmpty do
throwError "AUDIT FAILED (fail-closed):\n{String.intercalate "\n" errs.toList}"
-- (2) CANONICAL BLOCK: policy, then statements, then specification bodies.
let mut lines : Array String := #[]
lines := lines.push
s!"policy|kernel3={String.intercalate "," ((sortNames kernel3).map toString)}|apexExtra={String.intercalate "," ((sortNames apexExtra).map toString)}|specPrefix={specPrefix}"
let mut specs : NameSet := {}
for (cert, cone) in manifest do
let ci := (env.find? cert).get!
specs := (closureOf env {} ci.type.getUsedConstants.toList).toList.foldl (·.insert ·) specs
lines := lines.push
s!"cert|{cert}|cone={String.intercalate "," ((sortNames cone).map toString)}|type={← ppAll ci.type}"
for nm in sortNames specs.toList do
match env.find? nm with
| none => errs := errs.push s!"specification constant vanished mid-audit: {nm}"
| some ci =>
let isProp ← Command.liftCoreM <| Meta.MetaM.run' <| Meta.isProp ci.type
-- Proof irrelevance: a Prop-valued constant contributes its STATEMENT; a
-- data definition contributes its BODY, which is where fidelity lives.
if isProp then
lines := lines.push s!"spec|{nm}|prop|type={← ppAll ci.type}"
else
match ci with
| .defnInfo v => lines := lines.push s!"spec|{nm}|def|value={← ppAll v.value}"
| _ => lines := lines.push s!"spec|{nm}|other|type={← ppAll ci.type}"
unless errs.isEmpty do
throwError "AUDIT FAILED (fail-closed):\n{String.intercalate "\n" errs.toList}"
-- FAIL CLOSED ON ABSENCE: a manifest that somehow produced no specification
-- constants would emit a block that binds statements only. That is a weaker
-- claim than this file advertises, so it is an error, not a quiet pass.
if specs.toList.isEmpty then
throwError "AUDIT FAILED: statements reached ZERO specification constants — the closure is not doing its job"
logInfo ("SCALAR-AUDIT-MANIFEST-BEGIN\n" ++ String.intercalate "\n" lines.toList ++ "\nSCALAR-AUDIT-MANIFEST-END")
-- check.sh cross-checks its own CERTS array against THIS line, so the two
-- cannot drift apart without the build noticing.
logInfo s!"AUDITED-SCALAR-CERTIFICATES: {String.intercalate " " ((manifest.map (·.1)).map toString)}"
logInfo s!"statement audit PASSED: {manifest.length} certificates (exact cones + elaborated statements), {specs.toList.length} specification constants pinned"
end Ed25519ScalarAudit
open Ed25519ScalarAudit in
auditScalarStatements