mirror of
https://github.com/saymrwulf/dalek-ed25519-verified.git
synced 2026-09-03 20:13:48 +00:00
Phases 3/3b establish what each certificate RESTS ON. Neither says what it SAYS, nor what it is ABOUT. A certificate gutted to a tautology of the same axiom cone passes both; so does one whose reference definition has been redefined to BE the extracted code, at which point the theorem reads `loop = loop` and every cone is byte-identical. Phase 3c closes that. Proofs/Audit.lean emits a canonical block holding the policy constants, every certificate's fully-elaborated statement (pp.all, so implicit arguments, instances and universe levels are visible), and the body of every specification constant transitively reachable from those statements. Its SHA-256 is pinned in check.sh and the block itself is committed as AUDIT-MANIFEST.txt, so a mismatch is DIFFED, not merely reported. 31 certificates, 68 specification constants per repository. Two tiers, not one. These forks have an arithmetic tier that must stay oracle-free and an apex tier carrying this fork's hash and wire-format axioms, and the apex boundary genuinely differs per fork (dalek 8 extra names, anza 4, risc0 and betrusted 5). One shared constant would have widened the arithmetic tier to accept hash oracles, which is the most valuable property these repos have. Each auditor is generated from its own repository's policy. Phase 0b pins the extracted model. This was not a precaution: risc0 and betrusted were observed emitting BYTE-IDENTICAL audit-manifest digests (6c821b8e…) while shipping demonstrably different extracted models, their point-doubling routines differing in operation order. A statement names an extracted function; it does not contain that function's body. Binding statements is not binding the subject. Membership derives from the filesystem, so a new model file fails closed. selftest-statements.sh attacks both phases with ten cases, each asserting a specific diagnostic: an edited model body, an unlisted model file, a widened policy, a hand-edited committed block, a certificate dropped from the auditor WITH the digest refreshed to match, and a gutted statement whose cone is unchanged. It lifts the phases out of check.sh at run time, so it attacks the shipping gate rather than a copy. Two bugs found and fixed during that testing, both mine: Phase 3c read `$0` after `cd "$AENEAS_LEAN"`, and $0 is the caller's relative path; and the axgate self-test compared the tree against a pristine checkout rather than against how it found it. A third expectation was wrong rather than the code — widening the apex boundary is caught by the exact-cone requirement before the digest ever runs, which is a stronger rejection, and the test now says so. All sixteen runs green at these commits: four main buttons, four axgate self-tests, four binding self-tests, four scalar buttons. TRUSTED-BASE.md records what this binds and, at equal length, what it does not: a digest binds identity, not meaning; an author can rotate the pins in one commit and is caught by review, not by the script; and pinning the model says nothing about whether Charon and Aeneas translated the Rust faithfully. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
236 lines
12 KiB
Text
236 lines
12 KiB
Text
/- ──────────────────────────────────────────────────────────────────────────
|
||
Proofs/Audit.lean — statement + specification binding (P1-a).
|
||
|
||
WHAT THIS ADDS over check.sh Phases 2b/3/3b. Those establish that each
|
||
certificate is a theorem and rests on exactly the declared axioms. None
|
||
of them establishes WHAT THE THEOREM SAYS. A certificate gutted to a
|
||
tautology keeps its name, its kind, and its axiom cone, and passes every
|
||
one of them. Worse, the reference definitions the statements are stated
|
||
AGAINST can be redefined to be the extracted code itself, at which point
|
||
the certificate says `loop = loop` and every cone is unchanged.
|
||
|
||
So this file emits a canonical AUDIT-MANIFEST block covering:
|
||
· the POLICY constants (both cone tiers, and the module prefix that
|
||
defines what counts as a specification);
|
||
· every certificate's fully-elaborated STATEMENT (`pp.all`);
|
||
· every specification constant transitively reachable from those
|
||
statements, with its fully-elaborated DEFINITION BODY. A Prop-valued
|
||
constant contributes its statement rather than its proof term, by
|
||
proof irrelevance — the proof term is not what fidelity lives in.
|
||
|
||
check.sh binds the SHA-256 of that block, and the block itself is
|
||
committed, so a mismatch can be DIFFED against a reference rather than
|
||
merely reported.
|
||
|
||
TWO TIERS, NOT ONE. This repository has an arithmetic tier that must
|
||
stay oracle-free and an apex tier that legitimately carries this fork's
|
||
hash and wire-format axioms. The boundary differs BETWEEN FORKS. A
|
||
single global allowed-axiom list would silently widen the arithmetic
|
||
tier to accept hash oracles, which is the most valuable property these
|
||
repositories have. Hence a per-certificate expected cone below.
|
||
|
||
NO HAND-KEPT STATEMENT FINGERPRINTS. The companion SLH-DSA auditor pins
|
||
a 32-bit `Expr.hash` per certificate as a diagnostic. That table is one
|
||
more thing that can fall out of sync, and the canonical block already
|
||
covers every statement at full fidelity — a diff of the block says more
|
||
than a changed integer does. Membership is derived, not listed.
|
||
|
||
STANDING LIMIT: an audit executed by a harness cannot defend against an
|
||
author who edits that harness. The consumer defence is, and remains, the
|
||
pinned commit reviewed at the pin.
|
||
────────────────────────────────────────────────────────────────────────── -/
|
||
import Proofs.FieldMain
|
||
import Proofs.EdMain
|
||
import Proofs.DsmTableSpec
|
||
import Proofs.DsmStepSpec
|
||
import Proofs.DsmLoopSpec
|
||
import Proofs.DsmNafLoadSpec
|
||
import Proofs.DsmNafMath
|
||
import Proofs.DsmNafSpec
|
||
import Proofs.DsmMulSpec
|
||
import Proofs.ToBytesSpec
|
||
import Proofs.CompressSpec
|
||
import Proofs.ScalarPackSpec
|
||
import Proofs.SigApexSpec
|
||
import Proofs.PointLiftSpec
|
||
import Proofs.PointEqSpec
|
||
import Proofs.DecompressSpec
|
||
import Proofs.FromBytesSpec
|
||
import Proofs.DecompressMain
|
||
import Lean
|
||
open Lean Elab Command
|
||
|
||
namespace Ed25519Audit
|
||
|
||
/-- 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 :=
|
||
[`ed25519.Signature, `sha2.Sha512, `verifying.sha512_finalize_bytes, `verifying.sha512_new, `verifying.sha512_update, `ed25519.Signature.to_bytes, `signature.error.Error, `signature.error.Error.new]
|
||
|
||
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) :=
|
||
[ (`CurveFieldProofs.fieldImplementation, kernel3)
|
||
, (`CurveFieldProofs.edwardsImplementation, kernel3)
|
||
, (`CurveFieldProofs.naf_table_spec, kernel3)
|
||
, (`CurveFieldProofs.naf_select_spec, kernel3)
|
||
, (`CurveFieldProofs.proj_double_law, kernel3)
|
||
, (`CurveFieldProofs.compl_as_projective_law, kernel3)
|
||
, (`CurveFieldProofs.dsm_step_p_law, kernel3)
|
||
, (`CurveFieldProofs.dsm_step_b_law, kernel3)
|
||
, (`CurveFieldProofs.dsm_loop_spec, kernel3)
|
||
, (`CurveFieldProofs.naf_load_spec, kernel3)
|
||
, (`CurveFieldProofs.naf_exit, kernel3)
|
||
, (`CurveFieldProofs.naf_digit_loop_spec, kernel3)
|
||
, (`CurveFieldProofs.non_adjacent_form_spec, kernel3)
|
||
, (`CurveFieldProofs.run_basepoint, kernel3)
|
||
, (`CurveFieldProofs.vartime_double_base_mul_spec, kernel3)
|
||
, (`CurveFieldProofs.verify_loop_full, kernel3)
|
||
, (`CurveFieldProofs.to_bytes_spec, kernel3)
|
||
, (`CurveFieldProofs.ed_compress_spec, kernel3)
|
||
, (`ScalarProofs.from_bytes_mod_order_wide_spec, kernel3)
|
||
, (`CurveFieldProofs.vartime_dsm_basepoint_spec, kernel3)
|
||
, (`CurveFieldProofs.enc_point_inj, kernel3)
|
||
, (`CurveFieldProofs.pow_p58_spec, kernel3)
|
||
, (`CurveFieldProofs.fe_ct_eq_spec, kernel3)
|
||
, (`CurveFieldProofs.sqrt_core, kernel3)
|
||
, (`CurveFieldProofs.sqrt_ratio_i_sq_spec, kernel3)
|
||
, (`CurveFieldProofs.from_bytes_spec, kernel3)
|
||
, (`CurveFieldProofs.decompress_of_canonical, kernel3)
|
||
, (`CurveFieldProofs.verify_accepts_iff, apexBoundary)
|
||
, (`CurveFieldProofs.verify_accepts_iff_point, apexBoundary)
|
||
, (`CurveFieldProofs.verify_accepts_iff_point_eq, apexBoundary)
|
||
, (`CurveFieldProofs.verify_accepts_iff_decompress, apexBoundary)
|
||
]
|
||
|
||
/-- 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 "auditStatements" : 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 ("AUDIT-MANIFEST-BEGIN\n" ++ String.intercalate "\n" lines.toList ++ "\nAUDIT-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-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 Ed25519Audit
|
||
|
||
open Ed25519Audit in
|
||
auditStatements
|