mirror of
https://github.com/saymrwulf/betrusted-ed25519-verified.git
synced 2026-09-03 20:13:47 +00:00
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>
171 lines
8.1 KiB
Text
171 lines
8.1 KiB
Text
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
|