Compare commits

...

49 commits

Author SHA1 Message Date
1086a3ba02 course refresh: the notebooks enter the SLH-DSA era
The 14-notebook course predated the post-quantum campaign entirely
(coherence findings 10, 11). Now, authored in the GENERATOR and
regenerated (AGENTS.md rule):
- notebook 06: new section 'The second signature that actually shipped:
  SLH-DSA' — the deterministic co-signature since tree size 14, chosen
  because the log attests its own parameter set's verify path (leaf 18,
  11 certs); absent-not-failed for older heads; determinism as an audit
  primitive; verify-only always. Plus a runnable keygen/sign/verify/
  re-sign-byte-equality demo (honest skip below OpenSSL 3.5) and the
  --slhdsa-public-key consumer flag in the policy list.
- notebook 09: the 'post-quantum line' is now three-legged — Ed25519
  proven-verify dogfood, SLH-DSA shipped-and-attested, ML-DSA required-
  but-honest-unavailable — with the closing point that a slot stops
  being aspirational the day its verify path enters the log; stale
  16/16 provenance count -> 44/44 (leaf 13 re-attestation).
- notebook 07: policy exercise extended with the co-signature question;
  00 course map goal updated; README course listing for 06/09.
- GENERATOR DRIFT REPAIRED in passing: notebook 10's cockpit cell had
  been added to the .ipynb but never backported to the generator —
  regeneration would have silently dropped it; the cell is now IN the
  generator and round-trips (19 cells, content identical).
Suite 157 green.
2026-08-22 21:18:01 +02:00
31a9dee10a coherence sweep wave 2 (mirror template): 'additive' aligned to site vocabulary in reader-facing lines; paper+DOI line added (findings 20,21) 2026-08-22 18:41:42 +02:00
7e9e557187 coherence sweep wave 1 (pacta): the v0.11 era leaves ESTATE/llms/comments
Cross-repo audit findings 1-9,29: ESTATE snapshot to v0.15+DOI, suite
count 157, mermaid paper node, retired /paper/v0.x routes dropped from
the endpoint row, pasta row aligned to the subject repo's own claim
(field foundation proven, completion pending), llms.txt to v0.15+DOI,
paper/README supersession tail through v0.15, web.py route comment,
ltl.tex header comment no longer names the private reinvention outline
or the rejection (moat + zero-process in public files), litex-boards
dropped from the sources row (not in the pinned pool). Paper PDF bytes
untouched (comment-only tex change; v0.15 artifact stays byte-stable —
the committed PDF is the version-of-record built at v0.15 release).
2026-08-22 18:40:11 +02:00
7cc5982af3 site: the paper card finally joins the redesign
The last fossil paragraph on the page — an ePrint-era compressed table
of contents, version-patched five times, never re-read as prose. Five
undefined terms of art in one 120-word sentence, a semicolon train of
metadata, internal bookkeeping speaking to visitors ('the version is
printed on the title page'), changelog voice ('New in the August 2026
revisions'). Now: title, clean metadata line (pages, version, DOI),
and three sentences that answer the visitor's only question — should I
click: the guarantees-and-non-guarantees discipline at referee depth,
the sn=0 war story, and the claim matrix as the recommended entry
point. New law (rule 13): fact-patches require whole-card re-reads —
fact probes pass on unreadable prose.
2026-08-22 15:18:26 +02:00
57ac2095c0 web: HEAD support — link checkers and unfurlers get 200+headers, not 501
Found while verifying what /paper serves: the stdlib handler only
implemented do_GET, so every HEAD probe (mail clients, chat unfurlers,
link checkers — exactly the tools that touch the links we mail around)
got 501. do_HEAD now routes like GET with the body suppressed; all five
body writes go through one guard; regression test asserts HEAD returns
200, correct Content-Type, nonzero Content-Length, empty body.
2026-08-22 14:59:54 +02:00
03de38eaac the paper has a DOI: 10.5281/zenodo.22057482 (Zenodo, CC BY 4.0) — site card and paper README carry it 2026-08-22 14:51:55 +02:00
469a962825 paper v0.15: the dichotomy named, the question aligned
Two edits. (1) The alternatives paragraph of section 1 now states the
frame the site opens with, in paper register: each alternative fails on
one of two sides — checking cost stays with the consumer, or it
disappears because belief in a label is demanded — and the primitive
studied occupies the point between. (2) The introduction's italic
question said 'opaque provider verdict' where the abstract says
'label'; aligned to label, consistent with the v0.13 verdict/label
separation. Gate green, 25pp, page 2 eye-checked.
2026-08-17 13:40:39 +02:00
ca2a59f8a7 site: the one argument opens the page — three sentences, the dilemma and our point between
Operator order: the top value add as three sentences at the very top.
S1 names the two failure modes every alternative has (cost stays with
you, or belief is demanded); S2 places this site at the unique point
between (proof results as cheap as one signature + a handful of hashes,
without turning them into trust); S3 states the guarantee (permanently
recorded, key-bound, assumption-scoped — never believed, only held
accountable). The what-is-here paragraph follows concretely; the old
cost paragraph is absorbed (say it once).
2026-08-17 13:27:05 +02:00
7a6499c452 paper v0.14: the full-document eye pass completes the referee round
Operator order: every page of the PUBLISHED PDF inspected by eye, all
25. The flip surfaced two referee items the v0.13 batch had missed:
(1) the notation summary lacked rows for the consistency proof C and
the flag b (both load-bearing from Theorem 3 through Lemma 4) — added,
with Ext pointing at its defining subsection; the gate rejected the
first, too-wide row (130pt overfull) and the shortened one passed;
(2) the HIST chain length still shared the letter k with the Merkle
split point on facing pages — renamed to ell through the game,
Theorem 5, its proof, and Theorem 8's cost term. Gate green: v0.14,
25pp; pages 8 and 13 re-eyed; suite 156.
2026-08-17 12:54:08 +02:00
3b13fdbfde paper v0.13: the Green-persona approachability revision
A referee persona (deep crypto, shallow Lean, no prior drafts) read the
whole paper; all findings applied, ~40 edits, none touching technical
content:
- house terms defined at first use: certificate (in contributions),
  accumulator (= the log's Merkle tree + verifiers), signed view (4.2),
  pin rule/pin-store (named at their definition, 4.3), axiom cone as the
  one canonical synonym, loop-fidelity glossed, facade tied to its entry
  point, scope block named in 3.1, oracles marked 'uninterpreted
  function symbols, not random oracles'
- operator 'verdict' renamed label everywhere (Verdict stays the
  consumer algorithm); fork disambiguated (codebases vs fork evidence)
- notation: declarations T_i -> Theta_i (tier collision), HIST chain
  k kept but challenge scalar -> c and signature bytes -> R-bar
  (k/r_1 overloads resolved); tiers T1-T4 introduced in 6.1 body
- theorem-statement sensitivity: Thm 8 scoped to the recursive
  verifiers in the STATEMENT; Prop 1's 'exhibited' made conditional
  with pointer; Thm 3 carries its honest-pin note; Def 2(iii) gets the
  forgery caveat; Lemma 5 declared a restatement of Prop 2
- ghost references resolved (whole-tree root binding stated in place,
  twice); revision residue purged (Post-submission -> Subsequently
  closed; tense unified; hardened state, guarded replay, KNOWN-GAPS
  explained); 6.2 retitled 'A second instantiation'
- six triple-read sentences rewritten per referee (them-sentence,
  vacuous->trivially-by-counting with real non-vacuity guard wording,
  pin supplier, physical-execution antecedent, bridges-land, App E
  factorization)
- appendices A-D now each cited from the body; App D states its shared
  opaque boundary; FIPS 205 added to the bibliography [23] and cited
- abstract divergence sentence rewritten (divergence not 'boundary',
  past tense, closure named, 'the corresponding log entry')
Gate green: v0.13, 25pp; pages 1/16/25 eye-checked; suite 156.
2026-08-17 11:53:40 +02:00
8ab1f85556 site polish: the last four coherence fixes (de-numbered claim binding, witness in plain words, R5 out of prose, legend without boundary-exact) 2026-08-17 11:01:05 +02:00
4b45ece97b site: the term-coherence pass — every word defined before use, every table self-describing
Operator-caught: 'certificate' used before any definition, 'requirements
card' undefined, and the Attested-libraries table headed 'artifact 2 /
artifact 3' — numbers pointing at a DIFFERENT table. Fixed: certificates
defined in the Merkle intro (each leaf = one proof run, a batch of
certificates: one machine-checked theorem with its assumption list);
the requirements card is now 'a short plain-text file listing, by name,
the assumptions you accept'; table headers are self-describing (library
/ claim (attestation) / proof of inclusion (receipt) / certificates
proven); redundant gloss under the table dropped (say it once). My own
fresh re-read on top: 'head' glossed at first use, keys card says
'entry 18' (leaf comes later, at the Merkle mapping), 'Binds artifact 2'
de-numbered, 'become a witness' now plain ('hold the complete history
yourself, rung 1'), 'risk class R5' dropped from prose, the legend says
'every assumption list exactly as declared' instead of 'boundary-exact'.
2026-08-17 10:58:56 +02:00
6fcaaaee42 site: the one-story card is gone; SHA-512 introduced where it first appears
The 'Paper and log, one story' card served OUR revision history, not
the reader — removed without replacement. SHA-512 appeared unexplained;
now its first mention says why it exists in assumption lists at all
(the hash inside Ed25519, treated as an assumption, not as proven).
2026-08-17 10:46:42 +02:00
c5268ef46c site language: 'additive' retired, sibling keys get sibling names
Operator: 'additive' explains nothing to a newcomer, and Key 2 carried
its full parameter-set name while Key 1 was just 'Ed25519' — unequal
treatment. Now: 'Key 1 — Ed25519, required' / 'Key 2 — SLH-DSA
(FIPS 205), post-quantum'; the second signature is called exactly that,
'the second (post-quantum) signature'; parameter-set precision stays in
the artifacts where it belongs. Same cleanup in the OpenAPI summary and
the CLI help.
2026-08-17 10:41:18 +02:00
bcbf929045 receipt-verify checks the post-quantum co-signature — the tool now does what rung 2 promises
Operator-caught: rung 2 demanded both public keys while the shown
command consumed only the Ed25519 one — and the gap was real: pacta had
NO SLH-DSA head-signature check (only the mirror's verify.py had one;
'--require-signatures both' refers to the empty ML-DSA slot). New:
verify_receipt(slhdsa_public_key_path=...) verifies the additive
co-signature fail-closed (absent on pre-14 heads reports absent, not
failed; unavailable OpenSSL fails closed), CLI grows
--slhdsa-public-key, rung 2's command carries the flag and its muted
text explains both checks. Proven against the LIVE log: accepted:true,
slh_dsa:verified, ed25519 on the dogfood backend. New tamper test
flips a signature byte and must be rejected. Suite 156 green.
2026-08-17 10:29:42 +02:00
fdfe217d57 site css: long hashes wrap on narrow viewports (overflow-wrap:anywhere on code; pre capped at 100%) — no page-level horizontal scroll at any width 2026-08-17 10:04:56 +02:00
42c244374a site rewrite v2 + OpenAPI: the undergrad-first page
Operator critique, all points: no narrative opener (the page now begins
'This site is a public notary for machine-checked proofs...'); subjects
stated symmetrically and completely (signature-CHECKING code in four
Ed25519 libraries, an SLH-DSA implementation, and the log's own Merkle
machinery — the accumulator leaf was missing before); the redundant
'same thing, in one precise sentence' paragraph is gone; every ladder
rung now labels its preconditions ('You need: ...') before any command
and explains what the command does and what a green result means; no
forward references to the paper before its own section; the key card
opens 'Two keys sign everything in this log' — never again 'This key'
— with Key 1/Key 2 structure; the API box is REMOVED from the page and
replaced the industry-standard way: an OpenAPI 3 document served at
/openapi.json (new route + test), one footer line points to it; the
footer carries no commands; the paper card references rung 1 instead
of dropping a bare command. Suite 155 green.
2026-08-17 10:02:06 +02:00
6c65a53775 site rewrite: the trust ladder is the page's spine
Operator order: coherent, precise, accessible — an undergraduate CS
student must understand what the service provides, how to benefit, and
what to DO from each trust angle. The page now opens in plain terms
(what happened, why this site exists), keeps the precise one-sentence
as the second voice, and replaces 'Three ways to use it' with the
FIVE-rung trust ladder: (1) witness — hold the history honest, one
minute; (2) trust the reports, bind the operator — milliseconds;
(3) accept observations, reject judgment — re-derive verdicts from
recorded cones against your own card, the rung most people miss;
(4) replay the proofs yourself — ~30 min/library; (5) rebuild the whole
path — weeks, floor named (kernel/extractors/compiler; no rung 6).
Split-view comparison stated as orthogonal to all rungs. The API box is
demoted to 'For your tooling — the raw API' at the bottom with its
reason for existing stated (machine consumers; pacta builds on it).
All content guards keep their exact phrases; suite 154 green.
2026-08-16 21:00:18 +02:00
6b16eee940 test: the key-section guard follows the corrected plural heading
(The guard did its job — the suite was red after the heading fix and my
verification chain swallowed the exit code through a pipe and deployed
anyway. Chain hygiene restored: pipefail on every gate invocation.)
2026-08-16 20:55:07 +02:00
43d7f00930 site: the key section's heading now matches its content — two anchors, one required, one additive
Operator-caught logical fallacy: the h2 said 'pin this key' (singular)
and the card said 'sole identity anchor' while the same section
presents a second, additive post-quantum key ('sole' + 'Second anchor'
in one breath). The heading is now plural with the hierarchy stated;
'sole' became 'required' in both places it appeared; and 'exactly three
files' now names the optional fourth (the additive PQ key). Root cause:
the SLH-DSA block was APPENDED to the section in August without
re-reading the section against its own heading — local edit, no
whole-unit re-read.
2026-08-16 20:47:07 +02:00
51a094182a site SVG: geometry that survives growth — adaptive leaf boxes, self-sizing head box
Operator-caught, by eye: the accumulator graphic was designed for 8
leaves (fixed 112px boxes, fixed 380px head box) and has been a
shingled overlap since the log reached 19 on 2026-08-08; the head
caption spilled both sides after today's wording grew. Every 'site
verified' since was text-probes — nobody rendered pixels. Fixes:
leaf boxes now size to their span (rich >=100px, compact below, short
names, hash line dropped when narrow); the head box computes its width
from its longest line and carries the caption on two lines; edges
anchor to the actual box top. New regression test renders the tree at
8/19/33 leaves and asserts no two leaf rects overlap and the head
title fits its box. Suite 154 green. Eye-passed locally before deploy.
2026-08-16 20:22:45 +02:00
2517d79f6e paper v0.12 + estate: one numbering, everywhere — 0-based leaf indices
Operator order: the leaf-index convention adopted this morning must be
global. Paper1 now says leaf 12 in every place that said entry 13
(intro, contributions, section 7.2, related work, conclusion, artifact
availability, Appendix B retitled 'Deployed leaf-12 scope'); the
ordinal 'thirteenth entry' survives only as a gloss. v0.12, 25pp, gate
green, Appendix B page eye-checked. Site card follows (v0.12; revision
prose de-versioned so it cannot churn), paper/README history extended,
top README says leaf 12/17/18. reinvention-outline moved to the private
control repo earlier this commit-series (moat doctrine).
2026-08-16 20:04:44 +02:00
24809f8e4d paper: retire the reinvention outline from the public shelf (steering material; retrievable from git history) 2026-08-16 20:00:20 +02:00
b497b00c73 docs: the pre-Green full-scan repairs — ESTATE/llms/evidence to the 19-leaf world, leaf-index numbering unified, instruction traps closed
ESTATE.md was still the 2026-07-22 snapshot (13 leaves, v0.9, 'SLH-DSA
NOT in the log'); llms.txt still said thirteen leaves, 23 pages, and
linked the retired /paper/v0.x routes; README repeated the stdlib-only
verify.py trap the site just fixed; evidence/README called the July
capture's 16/16 attestations 'definitive' and its 8-leaf state current.
All brought to truth. Entry numbering standardized on 0-based leaf
index with 'the thirteenth entry' as the ordinal gloss (docs, lab
manual, security note, mirror README template). Operator-machine path
removed from provider/README.
2026-08-16 18:32:37 +02:00
5bea4c85cb evidence: name the capture honestly — STH at capture time (size 8), pointer to today's live state 2026-08-16 18:20:04 +02:00
4e1cb19e06 site: the self-reference boast carries its own scope — verify path proven, signing never
Operator-caught nuance: the plain-lead sentence ('the code that signs
the log is itself an entry in the log') invites the exact over-read
Green warned about — statement-scope sensitivity. The lead and the SVG
caption now state the boundary inline: the attested entry proves the
VERIFY path; no signing code is proven, here or anywhere.
2026-08-16 17:37:22 +02:00
f54e47778b site: the socratic round — every stale claim fixed, every printed command runs, first-use glosses
Operator-found (and root-caused): the v0.11 card replace had silently
no-opped on an invisible NBSP in 'vs.\xa0today' — the page said 23 pages
and still carried the July-snapshot card one paragraph under the 'New in
v0.11' sentence. Fixed by byte-anchored replace with post-assert; a
source regression test now forbids the stale markers outright and
check-paper.sh binds the page-count claim to pdfinfo.

Executability (tested by actually running them): the printed curl had no
scheme and returned empty through the redirect -> https form; the
zero-install one-liner was missing its cd -> added; 'Standard-library
Python only' contradicted the openssl requirement stated 20 lines
earlier -> made honest; 'table above' pointed at a table that is below
-> below; '~25 lines of Python' vs the 346-line fail-closed verify.py ->
'one-page core (paper Appendix C), shipped verify.py wraps it'.

Comprehensibility (fresh-eyes audit, 28 findings triaged): first-use
glosses for STH, axiom cones, certificates, pinning, split view,
pins.json, trusted base, residual-risk list, R0-R5, signature tiers;
Lean 4 linked; Merkle accumulator glossed; operator=provider declared;
the ✗-leaves explained as the failed round 1 vs clean re-attest; the
flagship self-reference gets a plain lead sentence; TOFU named and
bounded; pacta install source at the first pacta command; coined verb
'machine-re-check' uncoined; pitch parenthetical trimmed (SLH detail
lives in its own block).

Suite 153 green (new: webdocs stale-claim regression test).
2026-08-16 16:49:44 +02:00
810d6f47f1 paper form round: every defect from the socratic inspection fixed + check-paper.sh gate
Triggered by the operator's hint (references flow into App A but a full
break sits between B and C). Full-document inspection found and fixed:
- ghost page 23 (~85% blank): the fossil \clearpage before Appendix C,
  placed under an older pagination, removed; appendix policy now
  DECLARED: the block starts on a fresh page, then flows with no
  internal breaks
- claim matrix (the paper's honesty centerpiece): solid-set rows merged
  visually and narrow justified columns gaped (badness-10000 in every
  build log, never read) -- now ragged-right columns, 3pt row air,
  EUF-CMA/SHA-256 unbreakable
- Figure 3 still drew the July 13-leaf snapshot in a v0.11 paper that
  narrates 19 leaves -- extended: leaves 13-18, August-2026 brace,
  dual-signed size-19 head box, pq-styled leaf 18
- ConsRec hyphenated as Con-sRec and set in serif vs sans elsewhere ->
  math-face identifiers in the mechanization table
- 'tuple' stranded its last syllable as a whole line in Definition 1;
  'timestamp' broke as times-tamp -> mbox + \hyphenation
- thesis box hyphenated its showcase slogan -> ragged-right no-hyphen
  (first attempt justified+nohyphen was caught by the new gate itself)
- Appendix E header caps + layer-cell caps + continuation row cleanup;
  related-work 3.4pt overfull removed
- NEW check-paper.sh: fails on overfull>10pt, any badness-10000, ghost
  pages (<300 chars/page), missing version on title page, ?? refs;
  4-check selftest; renders all pages for the mandatory eye pass
All 25 pages re-rendered and flipped by eye. Gate green. Tests green.
2026-08-16 15:27:56 +02:00
46a3094216 README: heads are dual-signed since size 14, log is at 19 leaves
The transparency-log section still said 'published heads remain
Ed25519-only until the next publication' (undated, false since
2026-08-08) and 'live at 13 leaves'. Brought both to the v0.11 truth,
including the entry-12-through-18 narrative (zero-indexed, matching the
paper's leaf numbering).
2026-08-16 13:44:06 +02:00
4f9e750eeb sweep the last v0.10 remnants out of comments and the paper README tail 2026-08-16 13:19:13 +02:00
0e700841c6 paper v0.11: the paper catches up to the system it built
The freeze rationale is gone (review concluded); v0.11 describes the
LIVE deployment instead of the July snapshot:
- abstract + contributions: nineteen leaves, four Ed25519 corpora at 44
  certs, the accumulator's two attestations, leaf 18 = the SLH-DSA
  verify path; heads dual-signed since size 14 with the parameter set
  the log itself attests
- signed-tree-heads section: the additive deterministic SLH-DSA
  co-signature, ABSENT-not-failed history honesty, determinism as an
  audit primitive, the closed loop to leaf 18
- new instantiation subsection 'The SLH-DSA verify path: the method on
  second terrain': 11 certificates, acceptance characterization, the
  no-second-semantics honesty (visible-not-correct), exact cone
  enforcement, scope (mono facade + 137-case bridge, base_2b, no
  signing)
- deployment/public state: 15 August 2026, root 7ee23940, twelve heads
  8-19, per-leaf narrative through 18
- NEW Appendix E: the eleven SLH-DSA certificates with their exact
  oracle cones — the mirror of Appendix D's Ed25519 tiers
- title: v0.11; site card + snapshot note + paper/README updated
25pp, zero unresolved refs, Appendix E render-inspected by eye; full
test suite green
2026-08-16 13:14:57 +02:00
8af6667756 site QA sweep: the paper section tells today's truth, old drafts retired
Operator finding 2026-08-15: the live site still said 'v0.9 — frozen
while under journal review' and advertised the superseded v0.1/v0.2
drafts. Fixed at the source:
- webdocs.py paper card: v0.10, revised August 2026 (version now also
  printed on the PDF title page); the divergence description gains its
  closure (the sn=0 conjunct, zero divergences, three-way regression);
  the freeze-era reconciliation card shrunk to a tight snapshot-vs-today
  paragraph; fragile '12 Jupyter lectures' count dropped
- web.py: /paper serves the current paper only; all old-variant routes
  404 (git history retains the drafts); ltl-v0.1/v0.2 files removed from
  the working tree, paper/README updated
- ltl.tex: title page now reads 'Revised: August 2026 — v0.10' (the
  version travels IN the PDF, per operator order); rebuilt
- tests updated to ENFORCE the retirement (all old variants must 404);
  full suite 152 passed
2026-08-15 21:43:08 +02:00
03c9dabf36 paper v0.10: post-review revision — the freeze is over, the queue ships
The review process concluded 2026-08 (operator released the hold). Folds
in exactly the staged erratum-queue + v0.10 items, nothing else:
- corpus count sentence made historical (sixteen at the studied leaves;
  forty-four per fork since — the log records both generations)
- the 3,867/73,573 divergence finding gains its closure everywhere it
  appears: root cause = deployed verifier omitted RFC 9162 S2.1.4.2
  Step 7's terminal sn=0 condition (fixed in ddbb5a4); zero divergences
  post-fix, three-way regression
- new limitations paragraph 'Replay-harness integrity' (a wrong
  observation needs no malice)
- adversary model: defective-harness clause
- claim matrix: 'recorded cone was produced by an audit that performed
  its checks — not established' row
- title page carries 'Revised: August 2026'; submitted v0.9 (7f140356)
  preserved in git history; paper/README signpost updated
2026-08-15 14:17:03 +02:00
840e3fba2c paper folder: signpost which file is current (operator decision 2026-08-08: keep drafts + note)
paper/README.md tells a visitor that ltl.pdf is the submitted version
frozen during review, that v0.1/v0.2 are superseded history, and that
v0.3-v0.9 live in git history. Also corrects the stale web.py comment
that labeled the live pdf 'v0.3 reinvention' (it is the v0.9 submitted
version). No served bytes change; the comment fix reaches the droplet
with the next routine app update.
2026-08-08 18:44:13 +02:00
be282ab724 paper: remove the explainer — operator-private material, relocated
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 23:48:44 +02:00
6c587b572b paper: plain-language explainer for paper 1 and the upcoming paper 2
For newcomers with no Lean and little crypto: the notary-ledger reading of
the log paper, the honest-or-caught guarantee in words, the frozen-snapshot
note, the two-acts framing of paper 2 — and a straight answer to "which
buttons?", since the jargon trips exactly the reader this page is for.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 23:45:20 +02:00
993cfb85d4 site: the homepage tells the truth about tree 19 — dual anchors, honest tooling, and a note to the paper's readers
The estate doc audit scoped itself to *.md and missed the text a site visitor
actually reads: the string constants in webdocs.py. Fixed here, verified by
RENDERING the page from the live 19-leaf state and checking each block:

- The SVG head label reads "Ed25519 + SLH-DSA" when the live head is
  dual-signed, "Ed25519" when it is not — computed, not asserted.
- The trust-anchor card now carries BOTH keys the same way: full PEM,
  SHA-256 fingerprint, raw endpoint (/v1/log-slhdsa-public-key), mirror
  comparison link. The Ed25519 key stays the required anchor; the SLH-DSA
  key is the additive post-quantum one, and the card says whose proof
  subject its verify path is (leaf 18).
- The registered homepage overclaim (register: homepage-stdlib-claim) is
  closed: "stdlib-only" wording replaced with the truth — stdlib hashing,
  signature checks shell out to the openssl binary, fails closed without.
- "one signature and ~N hashes" became "one REQUIRED signature (Ed25519;
  heads from tree 14 add an additive post-quantum SLH-DSA signature)".
- The paper card is reframed (frozen under review, describes the 16 July
  snapshot, "then-thirteen-leaf") and followed by a new reader-guidance
  card (operator-ordered): the paper-era prefix is unchanged inside the
  live history — leaves 0-12 byte-identical, the paper's head still
  head #5 of sth-history — verify.py --all checks both eras at once; the
  advances are additive (44-cert re-attestations, leaf 18, dual-signed
  heads, ABSENT on older heads by design); and the 3,867 divergence the
  paper honestly reports has since been CLOSED (sn==0 fix, 2026-07-23,
  pinned count now 0) — both the divergence and the fix are part of the
  retained record.

Layout fact the render surfaced: the served log dir must contain the .pub
files (that is what /v1/log-public-key reads); the SLH-DSA pub joins the
Ed25519 one there. Suite 152/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 18:11:42 +02:00
0d83dc3ec1 web: publish the SLH-DSA verification key the same way as the Ed25519 one
/v1/log-slhdsa-public-key, mirroring /v1/log-public-key exactly: same route
shape, same two-independent-locations TOFU rule (site copy + git mirror
copy). The operator decision of 2026-08-06 required the SLH-DSA public key
published like the Ed25519 key; the mirror half shipped with the tree-19
publication, this is the site half. Suite 152/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 17:55:46 +02:00
b232265264 docs: estate-wide consistency pass (workflow audit, 36 findings, all verified before fixing)
Nine parallel readers audited every doc against measured ground truth; every
finding was re-verified against the file before any edit, and the sweep fixed
by PROPERTY, not by flag — wording the readers caught in one repo was hunted
in all siblings (the two-button README sentence existed in all four forks,
not the three flagged; likewise the cone-overclaim in TRUSTED-BASE item 1).

This repo: see the diff. Records were not rewritten; clarifications are
dated. Doc-only except where noted in the estate summary; every gated doc
change was followed by a green button run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 16:00:54 +02:00
962c0fdd41 verify.py template: check the additive SLH-DSA head signature, fail-closed where it counts
The consumer verifier now judges the slh_dsa block on every head. The status
taxonomy is the design decision, so it is spelled out and each arm tested:

  ABSENT     head predates the second signature — allowed (additive posture,
             operator decision 2026-08-06); an append-only log keeps history.
  VALID      verified against provider.slhdsa.pub.
  INVALID    present and wrong — HARD FAILURE. A bad signature is never a
             degradation.
  WRONG-KEY  head names a different key than the mirror ships — hard failure.
  NO-PUBKEY  head claims the signature, mirror ships no key — broken
             publication, hard failure.
  TOOLING    OpenSSL < 3.5 cannot read SLH-DSA keys — honest degradation:
             one loud NOTE, never counted as verified, never failed; the
             required Ed25519 checks still gate the result.

verify_selftest.py gains two adversarial cases, both mutating a full mirror
copy: one flipped signature byte must fail naming slh_dsa:INVALID; deleting
provider.slhdsa.pub while heads claim the signature must fail NO-PUBKEY. On
mirrors without slh blocks the cases record as not-applicable rather than
silently passing. First selftest run caught my own marker bug — the case
grepped for the stored failure string, which is never printed, instead of
the printed status; the printed line is now the assertion target.

README template: SLH-DSA key row, honesty paragraph (verification proven for
both algorithms, signing for neither; leaves Ed25519-signed at issuance
only), and the hardcoded head size is GONE — the README now names no tree
size at all, so this staleness class dies with the instance (it said "tree
size 13" and would have shipped that under a 19-leaf head).

Template is deliberately ahead of the published mirror until the next
publication, which overwrites the mirror from here (the sync rule's
intended direction for pre-publication work). Proven against the scratch
19-leaf mirror: verify.py --all RESULT OK [full] with ABSENT on heads 8-13
and VALID on 14-19; selftest 13/13 GREEN. Backward compat proven against a
copy of the live 13-leaf mirror: RESULT OK [full], selftest GREEN with the
slh cases n/a. Suite 152/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 15:16:58 +02:00
d28a5c080e provider: thread the optional SLH-DSA key pair through append/sth/publish
append_attestation and latest_sth pass the keys to make_signed_tree_head so
every head minted during an append cycle carries the dual signature; publish
ships provider.slhdsa.pub beside the Ed25519 key the moment it exists. All
three parameters optional and defaulting to absent: a caller without SLH-DSA
keys gets exactly the previous behaviour, heads carrying the honest
not-configured slot. Suite 152/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 13:22:41 +02:00
ab629f34d8 replay: compile the certificate corpus, not the button's instruments; forks attest 44
Closes the two halves of replay-pipeline-lags-hardened-forks.

(1) The generic replay compiled every .lean under verification/, including the
P1-b kernel-gate instruments (Proofs/Inventory*.lean) that read other modules'
object files via readModuleData and only run inside the button's environment;
it died with "object file does not exist". New RepoConfig.replay_exclude
(globs, per repo, in the config where a reviewer sees them) filters the
compile; every exclusion actually applied is recorded in the attestation's
replay.instruments_excluded, so the LEAF discloses what its replay did not
compile — a silent exclusion here would be the estate's oldest defect class.
The replay checks the certificates; the deeper self-auditing gates are the
repository's own and run there.

(2) The ed25519 profile's default list still carried the 16 pre-hardening
certificates, so a successful replay would have attested 16 and the whole
16->44 point of the re-attestation would have been lost silently. Each fork
entry now carries its own 44 — 27 main + 4 apex + 13 scalar — MEASURED from
the fork's two buttons (CERTS arrays; apex recovered by the same pattern
Phase 3c itself uses), never hand-typed. Correction to the plan on the way
through: the plan said 40; the apex tier makes it 44.

Suite: 152 passed, 0 failed, 0 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 10:24:35 +02:00
3a7a56c4e5 config: fips205-slhdsa-verified entry — certificates and cones measured from its manifest
First slh_dsa-kind repo. The 11 certificate names and every per-certificate
cone are GENERATED from the repositorys own pinned AUDIT-MANIFEST.txt, not
hand-typed; the apex resolves to kernel-3 plus the five verify-path oracles.
Exclusions carry TRUSTED-BASE items 2/9/10 into the leaf: hash oracles
assumed, signing/keygen out of scope, everything above the extraction root
(including the pure/prehash domain-separator byte) uncovered, the deployed
bridge a 137-case differential test.

Dry-run green: 11/11 proven+clean at d44b70d8, attestation signed
verified-dalek-serial.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 09:22:58 +02:00
a03662438a slhdsa: the post-quantum signing path (deterministic, parameter-locked, additive)
Phase 2b+3 of the step-3 rehearsal, under the four operator decisions of
2026-08-06: deterministic signing, separate slh_dsa block, additive posture,
keygen executed same day (key in provider state, 0600, git-ignored — verified
before generation, not after).

src/pacta/slhdsa.py — the module that did not exist (register:
pq-slot-names-unproven-algorithm). Parameter set LOCKED to SLH-DSA-SHA2-128s:
every entry point asserts the key's reported algorithm and refuses anything
else, because any other set sits outside all eleven certificates while looking
like dogfood. Deterministic via -pkeyopt deterministic:1, so the byte-level
reproducibility check that caught a real defect on the Ed25519 side survives
here. Verification runs two ways: OpenSSL, and pacta-verify-slhdsa built from
the pinned proven source — the one signature check in the estate performed by
code whose verify path the certificates cover. The proven-verifier path is
package-anchored, not cwd-relative: the lesson of signer-backend-depends-on-cwd
applied on day one, not retrofitted.

make_signed_tree_head grows optional slhdsa key parameters. With them, the head
carries a signed slh_dsa block; without, an honest not-configured slot exactly
as ml_dsa always has. ml_dsa itself is untouched. Signatures stay outside the
signed payload for both algorithms — tested by asserting the payload is
byte-identical with and without the slh_dsa key.

Honesty carried in the artifact: signing_backend says "openssl" because no
proven signer exists for any algorithm; the module docstring states that
nothing here is Lean-proven and that the certificates cover the verify path of
the extracted model only.

Tests: 7 new, suite 152 passed, 0 failed, 0 skipped — including determinism
(two signings, identical bytes), the foreign-key refusal (Ed25519 key raises),
corruption rejected by both verifiers, and the proven/OpenSSL agreement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 22:03:39 +02:00
16040b79f5 quorum: pacta-verify-slhdsa — the SLH-DSA head-checker built from the proven source
Fifth quorum member, first post-quantum one: verifies an SLH-DSA-SHA2-128s
signature by calling slh_verify_128s, the extraction root the eleven fips205
certificates cover (apex fips205.slh_verify_128s_accepts_iff). Verify-only
like the other four: quorum members judge, they never sign.

Build discipline, because "built from the proven source" is a claim that has
to survive a hostile reader: build-verify-slhdsa.sh REFUSES to build if the
pinned checkout is dirty or at any commit other than a3ce8e8, exports the
pinned commit via git archive (never a working copy), applies
expose-mono.patch to that scratch copy, and then DIFFS the patched
verify_mono.rs against the pinned one, aborting if any existing line changed
rather than being appended. The patch is a visibility keyword plus its doc
comment (the crate denies missing_docs, so pub mod alone does not compile)
and one appended argument-assembly function whose body is the crate's own
test helper. The extraction root is provably untouched. A provenance sidecar
lands beside the binary: source commit, patch hash, main.rs hash, rustc, and
a not_covered field naming what no certificate reaches — M-prime assembly
(including the pure/prehash domain-separator byte), hex/file IO, the
compiler; signing and keygen out of scope entirely.

Demonstrated against OpenSSL 3.5.5 on a throwaway key: valid signature OK
both ways, wrong message INVALID, corrupted signature INVALID. The agreement
is itself a finding — this binary assembles M' = 0x00 || 0x00 || payload
(pure variant, empty context) and OpenSSL evidently does the same.

Convention matches the other members: template + main.rs + patch + build
script tracked; rendered Cargo.toml, lock, target/ and the .build-slhdsa
scratch tree ignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 21:52:56 +02:00
5e35a533e1 dogfood: anchor the signer path to the package, not the working directory
DEFAULT_STATE_DIR was Path("dogfood")/"state" -- a relative path, so
locate_verifier() resolved against whatever directory the process started in.
The consequence was not a crash but something quieter: run the provider from
the repository root and it signs with the attested dalek build; run it from
anywhere else and the binary is not found, signing falls back to OpenSSL, and
the head records `signing_backend: openssl`. Which implementation signs the
transparency log was an accident of the current directory.

Found by re-signing published head 13 as a reproducibility check. The byte
comparison passed -- the reconstructed payload re-signed to signature_base64
exactly -- but the backend came back `openssl` while head 13 records
`verified-dalek-serial`. The swap is invisible precisely BECAUSE Ed25519 is
deterministic: both implementations emit identical bytes, nothing downstream
differs, no test fails. A silent substitution that changes no output is one
nobody notices until the outputs differ, which for a signing key is late.

(The byte-identity is also a good result in its own right: independent
cross-implementation agreement on the SIGNING side, alongside the five-way
agreement already demonstrated on the verifying side.)

Fix: anchor to the package via Path(__file__).resolve().parents[2]. Resolution
no longer depends on cwd -- demonstrated from /, /tmp and the repo root, all
three now select verified-dalek where before only the repo root did.

Added PACTA_REQUIRE_VERIFIED_SIGNER. Recording a downgrade truthfully, which
this code already did, tells you afterwards which implementation signed; it
does not let you DECIDE which one will. For signing a transparency-log head
that choice should be stated up front and enforced, not discovered in a
provenance field once the head exists. Set it and signing raises instead of
substituting OpenSSL. Off by default: every existing caller keeps the
fall-back-and-record behaviour.

Negative-tested both ways (refuses, naming the path it searched; default still
records `openssl`). Suite: 145 passed, 0 failed, 0 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 18:10:59 +02:00
cd3b1bc921 cockpit: the estate page now MEASURES instead of asserting
The /estate page was hand-written prose inside estateview.py: 32 hard-coded
fact arrays and zero places reading live data, last edited 2026-07-22. It
cannot go stale by accident — it can only go stale, because nothing connected
it to the repositories it describes. For eight days it told the operator:

  · SLH-DSA "campaign in progress", "check.sh exits non-green by design"
    — while it had 11 proven certificates, a green button, an 18-attack
      self-test and an outside reviewer's attest-with-conditions;
  · ed25519 "16 reviewed certificates"
    — while they had 31 bound certificates and 3022 inventoried constants;
  · nothing at all about five audit phases and five self-tests per repo,
    none of which existed on the day the page was last touched.

Those four claims are corrected. More importantly the page now carries a
MEASURED panel rendered from formal-verification-control's
tools/estate-progress.py, which derives every figure from the repositories at
generation time. The panel states three things a reader would otherwise have
to assume:

  · WHEN it was measured, and by what;
  · WHETHER the repositories have moved since — the snapshot records the HEADs
    it was taken against, and the panel compares them live, naming any repo
    that has moved rather than quietly showing old numbers as current;
  · WHICH PART OF THE PAGE IS MEASURED AT ALL. Everything above the panel is
    labelled, in the page itself, as hand-written prose that can be out of
    date. That label is the honest part: the map is still prose, and a reader
    should know which half is which.

If the snapshot is absent the panel says NOT MEASURED in words and prints the
command to produce one. It never renders nothing, and never falls back to
prose — a blank space and a confident-looking stale figure are the same
failure, and the second is worse.

Two numbers, never one, per PROGRESS-METRIC.md: a single figure is what let
the old metric report 100% for work nobody had attacked.

All three paths tested: current, moved-since, and absent. 145/145 tests pass,
including the sync test guarding drift between this page and ESTATE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:24:31 +02:00
f1a55721e5 ESTATE: the SLH-DSA rows said zero certificates; there are eleven
The estate document had SLH-DSA at 'zero certificates, check.sh non-green by
design' since 2026-07-22, through the entire eight-round review campaign and
the completion of the verify-path pyramid. A hand-maintained document
describing repositories it never reads will go stale exactly this way.

The durable fix is formal-verification-control/tools/estate-progress.py, which
derives its numbers from the repositories at run time and refuses to report
progress while its ledger contradicts them. These rows are corrected by hand
only because this file is prose, not a dashboard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 20:33:09 +02:00
ddbb5a4fd8 fix(verify_consistency): restore RFC 9162 Step-7 terminal sn==0 check
The deployed consistency verifier implemented the RFC 9162 2.1.4.2
bit-navigation loop but its final return checked only the two
reconstructed roots, omitting the terminal condition that the new-size
navigation counter reach zero. That condition couples the consumed proof
length to the claimed tree sizes; without it, a valid proof for one
transition verifies under a lied (power-of-two) old size. Flagship: a
valid 2->3 proof is accepted under the false claim 1->3 with the size-2
root.

Fix: add `and sn == 0` to the final return.

This is the corpus's Known Gap 14 (3,867 deployed-accepts-only cases in a
pinned 73,573-case family, recorded in public log entry 13). It was
found by the project's own differential harness; a post-appeal review
round added a faithful RFC oracle as a third comparison, which showed
the deployed verifier — not the mechanized model — was the one deviating
from RFC 9162, and traced it to the missing terminal check.

Scope: verify_consistency's only production caller is the consumer-side
pin store, reached only behind a verified head signature. Generation is
RFC-correct and unaffected; the live provider service does not run this
verifier; the published standalone verify.py has no consistency verifier.
An empirical search found 0 realizable pin-advance poisons against an
honestly pinned consumer, consistent with Known Gap 14's non-claim.

Verification:
- New fail-first three-way regression test
  test_consistency_lied_size_three_way_agreement (deployed / recursive
  ConsRec model / independent faithful RFC 9162 transliteration) over the
  honest AND lied-size families; fails pre-fix, passes post-fix.
- Historical differential tests (164,479 inclusion; 164,224 consistency)
  unchanged — the fix rejects nothing honest.
- Full suite: 145 passed, 0 failed.

Public log entry 13, the attested accumulator commit, and the IACR
submission PDF are all unchanged. Vulnerable state tagged
vulnerable/sn0-consistency-fd2f6ba. See
docs/security-2026-07-23-consistency-terminal-check.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 18:34:23 +02:00
50 changed files with 3335 additions and 3041 deletions

3
.gitignore vendored
View file

@ -23,3 +23,6 @@ dogfood/quorum/*/Cargo.lock
dogfood/quorum/*/target/
dogfood/state/quorum/
paper/eprint-submission.md
dogfood/quorum/.build-slhdsa/
dogfood/quorum/verify-slhdsa/Cargo.toml
dogfood/quorum/verify-slhdsa/Cargo.lock

View file

@ -7,10 +7,12 @@ including the two self-referential loops that make the estate hard to
keep in one head. Maintained here in pacta because pacta is the
machinery hub and the only repo that changes freely.
State snapshot (2026-07-22): log **13 leaves**, root `3488a2d0…`, key
fingerprint `874c8a00…`, paper **v0.9 camera-ready (23 pp)**, five
attested components, one campaign open (SLH-DSA — **zero
certificates**), pacta suite 144 green.
State snapshot (2026-08-16): log **19 leaves**, root `7ee23940…`, key
fingerprint `874c8a00…`, heads dual-signed (Ed25519 + SLH-DSA) since
size 14, paper **v0.15 (25 pp, revised August 2026;
DOI 10.5281/zenodo.22057482)**, six attested
components (SLH-DSA verify path = leaf 18, the first post-quantum
entry), pacta suite 157 green.
```mermaid
flowchart LR
@ -23,20 +25,20 @@ flowchart LR
s6["fips205-source"]
end
subgraph V["VERIFIED SUBJECTS"]
d["dalek-ed25519-verified<br/>16 certs · leaf 8 · signer source"]
a["anza-ed25519-verified<br/>16 certs · leaf 9"]
r["risc0-ed25519-verified<br/>16 certs · leaf 10"]
b["betrusted-ed25519-verified<br/>16 certs · leaf 11"]
d["dalek-ed25519-verified<br/>attested 44/44 (leaf 13 · signer source; earlier gens 0/4/8)"]
a["anza-ed25519-verified<br/>attested 44/44 (leaf 14; earlier gens 1/5/9)"]
r["risc0-ed25519-verified<br/>attested 44/44 (leaf 15; earlier gens 2/6/10)"]
b["betrusted-ed25519-verified<br/>attested 44/44 (leaf 16; earlier gens 3/7/11)"]
p["pasta-pallas-verified<br/>field layer only · NOT attested"]
f["fips205-slhdsa-verified<br/>campaign in progress · 0 certs · NOT attested"]
c["ltl-accumulator-verified<br/>61 certs · entry-13 subject · frozen 172a1d0"]
f["fips205-slhdsa-verified<br/>11 certs proven · attested — leaf 18 (first post-quantum)"]
c["ltl-accumulator-verified<br/>61 certs · leaf 12 subject · re-attested leaf 17 (hardened)"]
end
subgraph M["MACHINERY — pacta + operator-held"]
prov["provider service<br/>check · append · publish · site code · templates (CI-pinned)"]
sig["dogfood signer<br/>verified-dalek binary"]
lib["consumer library<br/>receipts · pin store · R0R5"]
wal["warden (code)<br/>quorum wallet · MCP · cockpit (local, read-only)"]
pap["paper<br/>v0.9 + v0.1/v0.2 archives"]
pap["paper<br/>v0.15 + DOI (archives in git history)"]
crs["course + llms.txt<br/>14 notebooks"]
key["SIGNING KEY (offline)"]
ops["operational log state<br/>the true accumulator"]
@ -81,7 +83,7 @@ flowchart LR
d ==>|"LOOP 1: built from"| sig
sig ==>|"LOOP 1: signs the log"| ops
mir ==>|"LOOP 1: contains the signer's own attestation (leaf 8)"| d
c ==>|"LOOP 2: attested as entry 13"| prov
c ==>|"LOOP 2: attested as leaf 12 (13th entry)"| prov
mir ==>|"LOOP 2: carries proofs about its own accumulator"| c
classDef src fill:#f1f3f5,stroke:#8a93a0,color:#1c2430
classDef sub fill:#e2f2e9,stroke:#1e7f4f,color:#1c2430
@ -110,7 +112,7 @@ paper says so explicitly.)
**Loop 2 — the self-attestation.** `ltl-accumulator-verified` is a Lean
corpus proving soundness of the log's own accumulator *model*
(extractors, consistency binding, per-step pin safety). It was attested
into the log as **entry 13** — the log carries kernel-checked proofs
into the log as **leaf 12** (the log's thirteenth entry) — the log carries kernel-checked proofs
about its own machinery, scoped honestly (recursive model, not the
deployed verifier; see the corpus KNOWN-GAPS ledger).
@ -118,12 +120,12 @@ deployed verifier; see the corpus KNOWN-GAPS ledger).
| Repository | Lane | Role | Mutability |
|---|---|---|---|
| `curve25519-dalek-source`, `anza-cryptography-source`, `risc0-…-source`, `betrusted-…-source`, `pasta_curves-source` (+ `xous-core`, `litex-boards` context) | upstream | pinned inputs to extraction | **frozen — never modified** |
| `fips205-source` | upstream | verbatim snapshot of `integritychain/fips205` (pure-Rust FIPS 205 / SLH-DSA); upstream pin `30bac08`, snapshot head `5dca0db` — single deviation: upstream CI workflows stripped, documented in-commit | pinned; moves only for transparent, individually-justified Aeneas-compat patches (nothing proposed upstream) |
| `dalek-` / `anza-` / `risc0-` / `betrusted-ed25519-verified` | subject | Rust source + Lean proofs; 16 certs each; attested (leaves 811, generations at 07) | frozen at attested commits; branch moves only for docs |
| `pasta-pallas-verified` | subject | field layer proven; curve layer pending; **not attested** | changes freely |
| `fips205-slhdsa-verified` | subject | SLH-DSA (FIPS 205) verify-path campaign, parameter set SLH-DSA-SHA2-128s; **campaign in progress — zero certificates, `check.sh` non-green by design**; not attested | changes freely — campaign |
| `ltl-accumulator-verified` | subject | 61-cert corpus about the log's accumulator model; **entry-13 subject**, frozen `172a1d0` | frozen; doc-only commits allowed |
| `curve25519-dalek-source`, `anza-cryptography-source`, `risc0-…-source`, `betrusted-…-source`, `pasta_curves-source` (+ `xous-core` context) | upstream | pinned inputs to extraction | **frozen — never modified** |
| `fips205-source` | upstream | verbatim snapshot of `integritychain/fips205` (pure-Rust FIPS 205 / SLH-DSA); upstream pin `30bac08`, snapshot head `a3ce8e8` — deviations: CI workflows stripped + the documented Aeneas-compat/de-plumbing patch series (each commit individually justified) | pinned; moves only for transparent, individually-justified Aeneas-compat patches (nothing proposed upstream) |
| `dalek-` / `anza-` / `risc0-` / `betrusted-ed25519-verified` | subject | Rust source + Lean proofs; 44 certs each, attested 44/44 as leaves 1316 (2026-08-08); earlier 16-cert generations at leaves 811, first generations at 07 | attested commits fixed |
| `pasta-pallas-verified` | subject | field foundation proven (sub/neg, helpers, constants, primality, denotation); field completion and curve layer pending; **not attested** | changes freely |
| `fips205-slhdsa-verified` | subject | SLH-DSA (FIPS 205) verify-path campaign, parameter set SLH-DSA-SHA2-128s; **11 certificates proven, `check.sh` green with an 18-attack self-test**; attested — leaf 18 (2026-08-08), the log's first post-quantum entry | attested commit fixed |
| `ltl-accumulator-verified` | subject | 61-cert corpus about the log's accumulator model; **leaf 12 subject**, frozen `172a1d0`; hardened model re-attested as leaf 17 (2026-08-08) | frozen; doc-only commits allowed |
| `proof-aware-crypto-tooling-agent` (this repo) | machinery | provider service, consumer library, warden (+ local read-only cockpit), dogfood signer, paper, course, tests | **changes freely — the hub** |
| `lean-transparency-log` | published | the public mirror: leaves, heads, receipts, fail-closed `verify.py` + selftest | **generated by publish** — canonical files here, templates in pacta, CI-pinned |
| `verifying-crypto-with-lean` | published | undergraduate book; zero coupling to log state | changes freely |
@ -133,7 +135,7 @@ deployed verifier; see the corpus KNOWN-GAPS ledger).
| Entity | What it is |
|---|---|
| **ltl.zkdefi.org** | droplet (caddy → docker `cloud-ltl-1`): homepage rendered from live leaves, `/v1` API, `/paper` (+`/v0.2`, `/v0.1`), key endpoint. Read-only; no key material on the server. Deployment configuration is maintained privately. |
| **ltl.zkdefi.org** | droplet (caddy → docker `cloud-ltl-1`): homepage rendered from live leaves, `/v1` API, `/paper`, key endpoint. Read-only; no key material on the server. Deployment configuration is maintained privately. |
| **Forgejo** (`cloud-forgejo-1`) | nightly (03:00) mirror of the entire saymrwulf GitHub account — disaster-recovery copy. |
| **Signing key** | offline, operator-only; fingerprint `874c8a00…`; never on the server; public half published in two independent locations. |
| **Operational log state** | `provider/state/transparency-log-main` — the true accumulator. Appends happen here; the mirror is its projection. |
@ -149,7 +151,7 @@ deployed verifier; see the corpus KNOWN-GAPS ledger).
| provider write side (check / append / publish / sign) | **on demand** | operator machine | runs only during an append ceremony, minutes at a time; the signing key is offline otherwise |
| warden (the financial agent) | **not running** | nowhere | implemented prototype: a wallet directory plus CLI/MCP/cockpit processes that exist only while explicitly started; no deployed instance, no funds watched |
| custody cockpit | **on demand** | operator machine, localhost | `pacta wallet cockpit`, stops with Ctrl-C; read-only |
| SLH-DSA campaign (`fips205-source` + `fips205-slhdsa-verified`) | **no process** | — | static repos, **zero certificates yet**; extraction/proof sessions are episodic operator-machine runs under lean-guard |
| SLH-DSA campaign (`fips205-source` + `fips205-slhdsa-verified`) | **no process** | — | static repos, **11 certificates**; extraction/proof sessions are episodic operator-machine runs under lean-guard |
| everything else (repos, paper, book, mirror, SD) | **no process** | — | static files; consumers and reviewers are external and episodic |
The human-facing interactive rendering of this whole map, runtime

View file

@ -125,11 +125,11 @@ The `notebooks/` directory contains a zero-to-hero teaching sequence for undergr
- `03_lean_replay_and_axiom_audit.ipynb`: replay versus transpilation, Lean invocation, axiom audits.
- `04_proof_hygiene_and_boundaries.ipynb`: `sorry`, local axioms, trivial targets, manifest coverage.
- `05_third_party_attestation_provider.ipynb`: provider trust transformation and signed attestations.
- `06_merkle_transparency_logs.ipynb`: RFC 9162-style Merkle proofs, STHs, Ed25519/ML-DSA policy.
- `06_merkle_transparency_logs.ipynb`: RFC 9162-style Merkle proofs, STHs, the dual-signature head policy (Ed25519 + the shipped SLH-DSA co-signature) and the fail-closed ML-DSA slot.
- `06a_provider_build_the_log.ipynb` / `06b_agent_verify_inclusion.ipynb`: the MIRRORED PAIR - one provider (builds, Lean-verifies, signs with the merkleized library, self-checks its own inclusion), many agents (verify inclusion from scratch in ~25 lines, no Lean); the domain separation is the design and the lecture structure mirrors it.
- `07_agent_consequences.ipynb`: receipt-gated artifact builds and wallet-denial policy.
- `08_capstone_research_program.ipynb`: audit the shipped R4 evidence; design the R5 discharge plan.
- `09_dogfood_verified_crypto.ipynb`: the proven-path verifier in the agent's own loop; hybrid-PQC posture.
- `09_dogfood_verified_crypto.ipynb`: the proven-path verifier in the agent's own loop; the three-legged PQ posture (Ed25519 dogfood, shipped SLH-DSA, honest ML-DSA slot).
- `10_verified_custody_wallet.ipynb`: warden - the quorum custody boundary and signing firewall, ratchet-rule (toy 3-of-3, then the real four proven forks), plus the counterparty recomputing a custody card's inclusion proof.
- `11_the_customers_eye_view.ipynb`: the counterparty's seat - what a customer can and cannot recompute about someone else's wallet from its custody card and the public log.
@ -207,14 +207,18 @@ The log uses:
- `RFC9162_SHA256` Merkle leaf/node hashing with `0x00` leaf and `0x01` node domain separation.
- Signed Tree Heads over canonical JSON tree-head payloads.
- OpenSSL Ed25519 signatures today.
- An additive `SLH-DSA-SHA2-128s` / FIPS 205 slot: verify path proven (fips205-slhdsa-verified, checked by `pacta-verify-slhdsa` built from the pinned proven source); signing deterministic and parameter-locked. Since tree size 14 (2026-08-08) every published head carries this co-signature alongside Ed25519; earlier heads keep their single signature, recorded as absent-not-failed.
- An explicit `ML-DSA-65` / FIPS 204 signature slot that is `unavailable` unless the host has a real backend. If an agent policy requires both signatures, verification fails closed.
The deployed instance ([ltl.zkdefi.org](https://ltl.zkdefi.org),
mirror [`lean-transparency-log`](https://github.com/saymrwulf/lean-transparency-log))
is live at **13 leaves**. Entry 13 (2026-07-16) attests the log's own
is live at **19 leaves** (2026-08-08). Leaf 12 (2026-07-16) attests the log's own
accumulator machinery — a kernel-checked mechanization of the log's
security analysis ([`ltl-accumulator-verified`](https://github.com/saymrwulf/ltl-accumulator-verified)),
appended into the log itself and scoped to the mechanized model. The
appended into the log itself and scoped to the mechanized model; entries
1316 re-attest the four Ed25519 forks at 44 certificates each, leaf 17
the accumulator's hardened model, and leaf 18 the SLH-DSA verify path —
the log's first post-quantum entry. The
producer path that generated it now emits a `scope` block into each leaf
(guarantees / exclusions / deployment-constraints), so a leaf carries its
own honest scope, not just a claim card.
@ -303,7 +307,7 @@ This is the intended trust transformation: local agents can avoid constructing t
## Real Evidence (shipped)
`evidence/` contains signed, transparency-logged attestations from a REAL
guarded replay of all four verified repositories: 16/16 certificates proven
guarded replay of all four verified repositories at the 2026-07 attestation generation: 16/16 certificates proven
per fork with boundary-exact axiom cones, pinned to exact repo commits,
machine-protected by `lean-guard` throughout (~30 min of Lean kernel
re-checking per fork, done ONCE - verifying the signature + inclusion
@ -320,7 +324,9 @@ The log has three synchronized faces, transport being orthogonal to trust:
2. **Git mirror** ([saymrwulf/lean-transparency-log](https://github.com/saymrwulf/lean-transparency-log), mirrored on Forgejo):
every leaf, every signed tree head (the WITNESS CHANNEL - all cloners
see the same heads), per-component receipts, the provider public key,
and a standalone stdlib-only `verify.py`. Anyone: `python3 verify.py --all`.
and a standalone `verify.py` (Python stdlib + the system `openssl`
binary; fails closed without it). Anyone, from inside a clone:
`python3 verify.py --all`.
3. **HTTP service** (deployed at `ltl.zkdefi.org`, see
`DEPLOY.md`): read-only CT-style endpoints + customer docs. The web
process never touches private keys - heads are signed offline; a
@ -358,7 +364,7 @@ pacta dogfood-status
pacta receipt-verify ... --require-verified-verifier # fail closed unless the proven path ran
```
The backend that actually verified each signature (`verified-dalek-serial` or the `openssl` fallback) is recorded in receipts' signature statuses and attestation evidence; the fallback is never silent. A provenance sidecar records the source commit, backend cfg, and the honest coverage note (the certificates cover the extraction image of this verify path; SHA-512 and the wire glue remain the documented trusted base). ML-DSA is deliberately NOT dogfooded: no proven implementation exists, so the slot stays fail-closed - the hybrid-PQC posture is one proven-classical signature plus one required-but-honest post-quantum slot.
The backend that actually verified each signature (`verified-dalek-serial` or the `openssl` fallback) is recorded in receipts' signature statuses and attestation evidence; the fallback is never silent. A provenance sidecar records the source commit, backend cfg, and the honest coverage note (the certificates cover the extraction image of this verify path; SHA-512 and the wire glue remain the documented trusted base). ML-DSA is deliberately NOT dogfooded: no proven implementation exists, so the slot stays fail-closed - the ML-DSA slot stays required-but-honest, while SLH-DSA now provides a proven-verify post-quantum capability (signing itself remains trusted base, as for Ed25519).
## Truth Boundary

View file

@ -46,7 +46,8 @@ into an alarm with a machine-checked guarantee behind it.
| SHA-512 | opaque oracle inside the theorems |
| wire parsers | outcomes are hypotheses |
| reproducible builds, side channels | not claimed (that is R5) |
| ML-DSA / PQC | fail-closed: no proven implementation exists |
| ML-DSA | fail-closed: no proven implementation exists; slot stays `not_configured` |
| SLH-DSA | verify path PROVEN (fips205-slhdsa-verified, 11 certs); signing deterministic + parameter-locked, trusted base |
The asymmetry is the point: warden is strongest exactly where it matters
most for custody — deciding whether an inbound authorization is real — and

View file

@ -47,7 +47,9 @@ cover the **verification** path. Signing is declared trusted base
(mitigated by the firewall below), SHA-512 is an oracle, wire parsing
is hypothesis, side channels and reproducible builds are the R5
frontier. ML-DSA remains fail-closed: no proven implementation exists,
so the PQC slot reports `unavailable` rather than pretending.
so that slot reports `unavailable` rather than pretending. (Update
2026-08: an SLH-DSA slot with a proven verify path now exists beside it —
the PQC story is no longer a single unavailable slot.)
## The four semantic edges (why divergence needs a taxonomy)

View file

@ -35,7 +35,7 @@ hash at the moment of divergence. That table is your suspect list.
1. **A corrupted/updated member binary.** Compare each member's current
hash against the capsule:
`sha256sum dogfood/state/quorum/pacta-verify-*` vs
`shasum -a 256 dogfood/state/quorum/pacta-verify-*` (GNU: `sha256sum`) vs
`capsule.json``members[].binary_sha256`. A mismatch on exactly the
dissenting member is the common benign case (a rebuild happened);
a mismatch you cannot explain is not benign.

View file

@ -0,0 +1,81 @@
# Security note: RFC 9162 Step-7 terminal check restored in `verify_consistency`
**Date:** 2026-07-23
**Component:** `pacta.transparency.verify_consistency` (consumer-side consistency-proof verifier)
**Severity:** low (RFC-conformance defect; no exploit found through the honest pin-store flow)
**Vulnerable commit:** tagged `vulnerable/sn0-consistency-fd2f6ba`
**Status:** fixed; found by this project's own differential fidelity harness.
## What was wrong
RFC 9162 §2.1.4.2 Step 7 requires a consistency proof to satisfy three
conditions: both reconstructed roots must match the supplied roots, **and** the
new-size navigation counter `sn` must reach `0`. That last condition couples the
consumed proof length to the claimed tree sizes. The deployed
`verify_consistency` implemented the RFC bit-navigation loop (including the
mid-loop `sn == 0` guard) but its final return checked only the two
reconstructed roots — it omitted the terminal `sn == 0` conjunct.
Consequence: when the claimed old size is a power of two, the verifier seeds the
walk with the old root and uses the sizes only as bit-navigation state, so
several distinct (false) old-size claims navigate one proof identically. A valid
proof for one transition therefore verifies under a lied size. Flagship: a valid
`2 → 3` consistency proof is accepted under the false claim `1 → 3` when paired
with the true size-2 root.
## Scope and reachability (measured, not assumed)
- The defect is confined to `verify_consistency`. Its **only** production caller
is the consumer-side pin store (`sthstore.check_sth_against_store`), which is
reached only behind a verified head signature in all three of its callers
(`attestation.py`, `cli.py`, `logclient.py`).
- Consistency-proof **generation** (`consistency_proof`) is RFC-correct and was
never affected; every honest proof the system emits verifies everywhere.
- The **live** LTL provider/mirror service does not run `verify_consistency` in
its serving path (it generates proofs, it does not verify them). The published
standalone `verify.py` has no consistency verifier at all (inclusion +
receipts only). So this is a consumer-library defect, not a live-service one.
- An empirical search for a pin-advance poison against an **honestly** pinned
consumer found **0** realizable cases: the two-root check still binds the new
root, and the pin binds `(size, root)` from the consumer's own store, so the
lied-size acceptance does not translate into a pin poison for an honest pin.
No exploit is claimed; the fix restores conformance regardless.
## The fix
One conjunct in the final return of `verify_consistency`:
```python
return old_hash == old_root_hash and new_hash == new_root_hash and sn == 0
```
## Verification
- **Fail-first regression** `test_consistency_lied_size_three_way_agreement`
(in `tests/test_paper_verifiers.py`): a **three-way** harness comparing the
deployed verifier, the recursive `ConsRec` model, and an independent faithful
RFC 9162 §2.1.4.2 transliteration, over both the honest family and the
lied-size family. It fails against the pre-fix verifier (the flagship lie is
accepted) and passes once `sn == 0` is restored.
- The historical differential tests (164,479 inclusion; 164,224 consistency)
stay green — the fix rejects nothing honest.
- Full suite: 145 passed, 0 failed.
- The independent RFC oracle is honest-complete on the tested range but is not
yet cross-checked against a second independent implementation (ATL / Sigsum);
that cross-check is a follow-up harness item.
## Honesty / provenance
This defect is the corpus's Known Gap 14: the project's own fidelity harness
**found** the divergence (3,867 deployed-accepts-only cases in a pinned
73,573-case family) and recorded it in public log leaf 12 (the thirteenth entry). What the two-way
harness could not do was assign blame — with only the deployed verifier and the
Lean model disagreeing, and both labelled "RFC 9162", the divergence was filed
as a scoped gap rather than a conformance bug. A post-appeal review round added
the missing third oracle (a faithful RFC verifier), which showed the deployed
side was the deviant one, and traced it to the omitted Step 7.
Public log leaf 12 and the attested accumulator commit are unchanged: the
historical experiment remains truthfully recorded and reproducible at the
tagged pre-fix commit. The IACR submission PDF is unchanged. The corpus
Known-Gaps ledger records this closure.

View file

@ -705,7 +705,7 @@ tripwire that screams when the map forks from itself.
Ed25519 implementation that is itself verified and attested *inside the
log it signs* (leaf 8). The tool guards the evidence; the evidence
covers the tool.
- **Loop 2, the self-attesting mechanization:** entry 13 of the log is the
- **Loop 2, the self-attesting mechanization:** leaf 12 of the log (its thirteenth entry) is the
kernel-checked mechanization of the very soundness arguments the log's
accumulator relies on. The proofs about the machinery live inside the
ledger the machinery protects — scoped honestly, with a published list
@ -975,7 +975,7 @@ boolean.
**6.1** Loop 1: the log's signing machinery runs on an Ed25519
implementation that is itself verified and attested inside the log it
signs (leaf 8). Loop 2: entry 13 is the kernel-checked mechanization of
signs (leaf 8). Loop 2: leaf 12 is the kernel-checked mechanization of
the accumulator arguments the log itself relies on — the proofs about the
machinery live in the ledger the machinery protects.
**6.2** Because "what is running" is invisible from any single machine and

View file

@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Build pacta-verify-slhdsa from the PINNED proven source.
#
# The pinned checkout is never modified. This script exports the pinned commit
# into a scratch tree, applies expose-mono.patch there, builds against that, and
# records exactly what went in. If the pinned checkout is dirty, or is not at
# the commit the attestation names, it refuses: a quorum member built from a
# tree nobody can identify is a quorum member that proves nothing.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
SRC="${FIPS205_SOURCE:-$HOME/GitClone/FormalVerification/sources/fips205-source}"
PIN="${FIPS205_PIN:-a3ce8e8}"
BUILD="${BUILD_DIR:-$HERE/.build-slhdsa}"
OUT="$HERE/verify-slhdsa/target/release/pacta-verify-slhdsa"
echo "=== pacta-verify-slhdsa: build from the proven source ==="
[ -d "$SRC/.git" ] || { echo "FATAL: '$SRC' is not a git checkout of fips205-source."; exit 2; }
HEAD_SHA="$(git -C "$SRC" rev-parse HEAD)"
case "$HEAD_SHA" in
"$PIN"*) ;;
*) echo "FATAL: pinned source is at ${HEAD_SHA:0:8}, expected $PIN."
echo " The certificates cover $PIN. Building a 'proven' verifier from any"
echo " other tree would be a claim nobody can check."; exit 1;;
esac
if [ -n "$(git -C "$SRC" status --porcelain)" ]; then
echo "FATAL: the pinned source has uncommitted changes:"
git -C "$SRC" status --porcelain | sed 's/^/ /'
echo " Refusing: the binary must correspond to a nameable tree."; exit 1
fi
echo " pinned source $SRC @ ${HEAD_SHA:0:8} (clean)"
# Export the pinned commit, never a working copy.
rm -rf "$BUILD"; mkdir -p "$BUILD"
git -C "$SRC" archive --format=tar "$HEAD_SHA" | tar -x -C "$BUILD"
echo " exported $(find "$BUILD" -type f | wc -l) files from $PIN"
# --- the two changes, applied verbatim and then VERIFIED to be present -------
LIB="$BUILD/src/lib.rs"; VM="$BUILD/src/verify_mono.rs"
grep -q '^mod verify_mono;' "$LIB" || { echo "FATAL: 'mod verify_mono;' not found in lib.rs — the source moved."; exit 1; }
# The crate is `#![deny(missing_docs)]`, so a module cannot become public
# without a doc comment. The comment is part of the visibility change, not an
# extra edit: `pub mod` alone does not compile here.
sed -i 's|^mod verify_mono;|/// Aeneas-compat monomorphic verify path: the extraction root the eleven\n/// certificates cover (apex `fips205.slh_verify_128s_accepts_iff`). Public only\n/// so a quorum binary can call the proven function; see expose-mono.patch.\npub mod verify_mono;|' "$LIB"
cat >> "$VM" <<'RUST'
/// Byte-level entry to the PROVEN root, for out-of-crate callers.
///
/// Assembles arguments only; the body is the crate's own test helper
/// `internal_inputs` followed by the call. `mprime` is FIPS 205's M' and is
/// built by the CALLER — its construction is outside every certificate
/// (TRUSTED-BASE item 10), which is why it is a parameter and not computed
/// here.
pub fn verify_mono_bytes(mprime: &[u8], sig_bytes: &[u8; 7856], pk_bytes: &[u8; 32]) -> bool {
let mut pk_seed = [0u8; 16];
let mut pk_root = [0u8; 16];
pk_seed.copy_from_slice(&pk_bytes[0..16]);
pk_root.copy_from_slice(&pk_bytes[16..32]);
let pk = SlhPublicKey { pk_seed, pk_root };
let sig = SlhDsaSig::<12, 7, 9, 14, 35, 16>::deserialize(sig_bytes);
slh_verify_128s(mprime, &sig, &pk)
}
RUST
# The extraction root must be untouched. Compare it against the pinned tree.
if ! diff <(git -C "$SRC" show "$HEAD_SHA:src/verify_mono.rs") \
<(head -n "$(git -C "$SRC" show "$HEAD_SHA:src/verify_mono.rs" | wc -l)" "$VM") > /dev/null; then
echo "FATAL: the patch altered existing lines of verify_mono.rs, not just appended."
exit 1
fi
echo " patched lib.rs visibility + verify_mono_bytes appended (existing lines unchanged)"
# --- render Cargo.toml from the template ------------------------------------
sed "s|{{SOURCE}}|$BUILD|g" "$HERE/verify-slhdsa/Cargo.toml.template" > "$HERE/verify-slhdsa/Cargo.toml"
echo " building..."
( cd "$HERE/verify-slhdsa" && cargo build --release 2>&1 | tail -5 | sed 's/^/ /' )
[ -x "$OUT" ] || { echo "FATAL: build produced no binary at $OUT"; exit 1; }
cat > "$HERE/verify-slhdsa/target/release/pacta-verify-slhdsa.provenance.json" <<JSON
{
"binary_sha256": "$(sha256sum "$OUT" | cut -d' ' -f1)",
"source_repo": "fips205-source",
"source_commit": "$HEAD_SHA",
"patch": "expose-mono.patch",
"patch_sha256": "$(sha256sum "$HERE/verify-slhdsa/expose-mono.patch" | cut -d' ' -f1)",
"main_sha256": "$(sha256sum "$HERE/verify-slhdsa/src/main.rs" | cut -d' ' -f1)",
"proven_root": "slh_verify_128s",
"parameter_set": "SLH-DSA-SHA2-128s",
"certificates": 11,
"apex": "fips205.slh_verify_128s_accepts_iff",
"not_covered": "M-prime assembly (domain separator, context length), hex/file IO, and the compiler. Signing and keygen are out of scope entirely.",
"rustc": "$(rustc --version)"
}
JSON
echo " binary $OUT"
echo " sha256 $(sha256sum "$OUT" | cut -c1-16)"
echo " provenance written beside the binary"

View file

@ -0,0 +1,13 @@
# Rendered by build-verify-slhdsa.sh — {{SOURCE}} is replaced with the exported
# copy of the PINNED proven source plus expose-mono.patch. Committed as a
# template so the repo never hardcodes a machine path.
[package]
name = "pacta-verify-slhdsa"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
fips205 = { path = "{{SOURCE}}", default-features = false, features = ["slh_dsa_sha2_128s"] }
[workspace]

View file

@ -0,0 +1,49 @@
Expose the proven verify root so a quorum binary can call it.
APPLIED TO A COPY of fips205-source at the pinned commit, never to the pinned
checkout itself. Two hunks, and the reason each is the smallest possible:
1. `mod verify_mono;` -> `pub mod verify_mono;`
A visibility keyword. Rust's `src/bin/` and `examples/` targets are
SEPARATE crates, so neither can reach a `pub(crate)` item; the module has
to be public for any binary to call into it at all.
2. A new `verify_mono_bytes` function, appended.
It only assembles arguments: split the 32-byte public key into pk_seed and
pk_root, deserialize the 7856-byte signature, call `slh_verify_128s`. The
body is copied from the crate's OWN test helper `internal_inputs`, so the
conversion is the one the crate already trusts rather than one invented
here. Exposing the argument types and their fields instead would have
meant four more visibility changes across two files.
WHAT THIS DOES NOT CHANGE. No existing line's semantics. The extraction root
`slh_verify_128s` is untouched -- same body, same callees. Module visibility and
an added sibling function do not alter the MIR of an existing function, so the
code the certificates cover compiles to what it compiled to before. What IS
true and must be said: the binary is built from `pinned commit + this patch`,
not from the pinned commit alone, and the diff below is the whole of the
difference.
--- a/src/lib.rs
+++ b/src/lib.rs
@@
-mod verify_mono; // Aeneas-compat monomorphic verify path (formal-verification campaign; additive)
+pub mod verify_mono; // Aeneas-compat monomorphic verify path (formal-verification campaign; additive)
--- a/src/verify_mono.rs
+++ b/src/verify_mono.rs
@@ (appended after slh_verify_128s)
+/// Byte-level entry to the PROVEN root, for out-of-crate callers.
+///
+/// Assembles arguments only. `mprime` is FIPS 205's M' and is built by the
+/// CALLER -- its construction is outside every certificate (TRUSTED-BASE item
+/// 10), which is exactly why it is a parameter here and not computed inside.
+pub fn verify_mono_bytes(mprime: &[u8], sig_bytes: &[u8; 7856], pk_bytes: &[u8; 32]) -> bool {
+ let mut pk_seed = [0u8; 16];
+ let mut pk_root = [0u8; 16];
+ pk_seed.copy_from_slice(&pk_bytes[0..16]);
+ pk_root.copy_from_slice(&pk_bytes[16..32]);
+ let pk = SlhPublicKey { pk_seed, pk_root };
+ let sig = SlhDsaSig::<12, 7, 9, 14, 35, 16>::deserialize(sig_bytes);
+ slh_verify_128s(mprime, &sig, &pk)
+}

View file

@ -0,0 +1,101 @@
//! warden quorum member: SLH-DSA-SHA2-128s, the Lean-proven verify path.
//!
//! Built against a copy of the PINNED proven source (`fips205-source` at the
//! commit named in the build provenance sidecar) plus `expose-mono.patch`,
//! which adds a visibility keyword and an argument-assembly function and
//! changes no existing line's semantics.
//!
//! The function this calls, `slh_verify_128s`, is the extraction root the
//! eleven certificates cover, apex `fips205.slh_verify_128s_accepts_iff`.
//! Verify-only on purpose: quorum members judge, they never sign.
//!
//! TWO THINGS THIS BINARY DOES THAT NO CERTIFICATE COVERS, stated here because
//! a reader of the output cannot see them:
//!
//! * It assembles M'. FIPS 205 hashes M' = toByte(0,1) ‖ toByte(|ctx|,1) ‖
//! ctx ‖ M, and Algorithm 20's input is already M'. Everything above the
//! extraction root -- including that leading domain-separator byte, the one
//! thing distinguishing the pure variant from prehash -- is outside every
//! proof (TRUSTED-BASE item 10). This binary implements the PURE variant
//! with EMPTY context, i.e. M' = 0x00 ‖ 0x00 ‖ payload, and refuses to
//! guess at anything else.
//! * It parses hex and reads a file.
//!
//! Usage: <pubkey-hex-32B> <sig-hex-7856B> <payload-file>
//! stdout OK / INVALID; exit 0 = accept, 1 = reject, 2 = input error.
use std::process::ExitCode;
const SIG_LEN: usize = 7856;
const PK_LEN: usize = 32;
fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
if s.len() % 2 != 0 {
return Err("odd-length hex".into());
}
(0..s.len() / 2)
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(|e| e.to_string()))
.collect()
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
if args.len() != 4 {
eprintln!("usage: {} <pubkey-hex-32B> <sig-hex-7856B> <payload-file>", args[0]);
return ExitCode::from(2);
}
let pk_bytes = match hex_decode(&args[1]) {
Ok(b) if b.len() == PK_LEN => b,
Ok(b) => {
eprintln!("error: public key must be {PK_LEN} bytes, got {}", b.len());
return ExitCode::from(2);
}
Err(e) => {
eprintln!("error: public key hex: {e}");
return ExitCode::from(2);
}
};
let sig_bytes = match hex_decode(&args[2]) {
Ok(b) if b.len() == SIG_LEN => b,
Ok(b) => {
// Size is part of the parameter set. A 7856-byte signature is
// SLH-DSA-SHA2-128s; anything else is a DIFFERENT parameter set and
// outside every certificate this binary exists to exercise. Refuse
// rather than attempt it.
eprintln!("error: signature must be {SIG_LEN} bytes (SLH-DSA-SHA2-128s), got {}", b.len());
return ExitCode::from(2);
}
Err(e) => {
eprintln!("error: signature hex: {e}");
return ExitCode::from(2);
}
};
let payload = match std::fs::read(&args[3]) {
Ok(p) => p,
Err(e) => {
eprintln!("error: cannot read payload file {}: {e}", args[3]);
return ExitCode::from(2);
}
};
// M' for the PURE variant with empty context: two length/domain bytes then
// the message. Built here, not proven anywhere.
let mut mprime = Vec::with_capacity(payload.len() + 2);
mprime.push(0u8); // domain separator: 0 = pure, 1 = prehash
mprime.push(0u8); // |ctx| = 0
mprime.extend_from_slice(&payload);
let mut sig_arr = [0u8; SIG_LEN];
sig_arr.copy_from_slice(&sig_bytes);
let mut pk_arr = [0u8; PK_LEN];
pk_arr.copy_from_slice(&pk_bytes);
if fips205::verify_mono::verify_mono_bytes(&mprime, &sig_arr, &pk_arr) {
println!("OK");
ExitCode::from(0)
} else {
println!("INVALID");
ExitCode::from(1)
}
}

View file

@ -4,18 +4,24 @@ Signed, transparency-logged attestations from a REAL guarded replay of the
four `saymrwulf/*-ed25519-verified` repositories (2026-07-06/07, ~30 min of
Lean re-checking per fork under `lean-guard` memory caps).
This directory is a dated capture, kept byte-stable on purpose: every file
verifies against the head it cites. The live log has since grown (19 leaves,
dual-signed heads); `python3 verify.py --all` in the public mirror re-checks
today's full state.
| File | What it is |
|---|---|
| `<fork>-ed25519.attestation.yaml` | Signed proof-check attestation: repo commit, environment, machine-protection block, and all 16 certificates with their OBSERVED axiom cones (the four apex tiers carry the fork's exact documented boundary) |
| `<fork>-ed25519.receipt.yaml` | RFC 9162-style inclusion receipt binding the attestation into the transparency log (tree size 8), with a consistency anchor |
| `provider.ed25519.pub` | The provider's public key (the PRIVATE key never leaves `provider/state/`, which is gitignored) |
| `log-metadata.json`, `latest-sth.yaml` | Log identity and the latest Signed Tree Head |
| `log-metadata.json`, `latest-sth.yaml` | Log identity and the Signed Tree Head at capture time (tree size 8) |
| `log-audit.txt` | Monitor self-check output (recomputed root matches the signed root) |
The log holds EIGHT leaves: entries 0-3 are the first run's attestations,
At the time of this capture (2026-07) the log held eight leaves: entries 0-3 are the first run's attestations,
which honestly record an AUDIT FAILURE (two bugs in pacta's audit step,
since fixed - see the commit history); entries 4-7 are the definitive
16/16-proven attestations. An append-only trust ledger keeps its bad day.
since fixed - see the commit history); entries 4-7 are that run's
16/16-proven attestations (the forks were later re-attested at 44/44 as
leaves 13-16; the live log now holds nineteen leaves). An append-only trust ledger keeps its bad day.
Re-verify everything yourself (signature via the proven-path verifier if
built, STH pinning, freshness):

View file

@ -16,6 +16,86 @@ repos:
- compiler correctness
- SIMD/AVX/accelerator paths
# certificates + imports MEASURED from the two buttons (CERTS arrays;
# apex recovered the way Phase 3c itself recovers it). replay_exclude:
# the button-owned audit instruments the replay does not compile —
# disclosed per-leaf in replay.instruments_excluded.
certificates:
- CurveFieldProofs.fieldImplementation
- CurveFieldProofs.edwardsImplementation
- CurveFieldProofs.naf_table_spec
- CurveFieldProofs.naf_select_spec
- CurveFieldProofs.proj_double_law
- CurveFieldProofs.compl_as_projective_law
- CurveFieldProofs.dsm_step_p_law
- CurveFieldProofs.dsm_step_b_law
- CurveFieldProofs.dsm_loop_spec
- CurveFieldProofs.naf_load_spec
- CurveFieldProofs.naf_exit
- CurveFieldProofs.naf_digit_loop_spec
- CurveFieldProofs.non_adjacent_form_spec
- CurveFieldProofs.run_basepoint
- CurveFieldProofs.vartime_double_base_mul_spec
- CurveFieldProofs.verify_loop_full
- CurveFieldProofs.to_bytes_spec
- CurveFieldProofs.ed_compress_spec
- ScalarProofs.from_bytes_mod_order_wide_spec
- CurveFieldProofs.vartime_dsm_basepoint_spec
- CurveFieldProofs.enc_point_inj
- CurveFieldProofs.pow_p58_spec
- CurveFieldProofs.fe_ct_eq_spec
- CurveFieldProofs.sqrt_core
- CurveFieldProofs.sqrt_ratio_i_sq_spec
- CurveFieldProofs.from_bytes_spec
- CurveFieldProofs.decompress_of_canonical
- CurveFieldProofs.verify_accepts_iff
- CurveFieldProofs.verify_accepts_iff_decompress
- CurveFieldProofs.verify_accepts_iff_point
- CurveFieldProofs.verify_accepts_iff_point_eq
- ScalarProofs.L_val
- ScalarProofs.sub_loop_spec
- ScalarProofs.cond_add_l_one_spec
- ScalarProofs.sub_val_spec
- ScalarProofs.add_val_spec
- ScalarProofs.mul_internal_spec
- ScalarProofs.part1_spec
- ScalarProofs.montgomery_reduce_spec
- ScalarProofs.mul_spec
- ScalarProofs.scalarImplementation
- ScalarProofs.montgomery_mul_spec
- ScalarProofs.bytes_unpack_spec
- ScalarProofs.from_bytes_wide_spec
axiom_imports:
- Proofs.FieldMain
- Proofs.EdMain
- Proofs.ScalarMain
- Proofs.ToBytesSpec
- Proofs.CompressSpec
- Proofs.ScalarPackSpec
- Proofs.DsmMulSpec
- Proofs.SigApexSpec
- Proofs.PointLiftSpec
- Proofs.PointEqSpec
- Proofs.DecompressSpec
- Proofs.FromBytesSpec
- Proofs.DecompressMain
- Proofs.ScalarDenote
- Proofs.ScalarLoop
- Proofs.ScalarSubSpec
- Proofs.ScalarAddSpec
- Proofs.ScalarMulSpec
- Proofs.ScalarMontSpec
- Proofs.ScalarReduceSpec
- Proofs.ScalarFullMulSpec
- Proofs.ScalarWideSpec
- Proofs.ScalarBytesSpec
- Proofs.ScalarUnpackSpec
- Proofs.ScalarFromBytesSpec
replay_exclude:
- "Proofs/Audit.lean"
- "Proofs/ScalarAudit.lean"
- "Proofs/AxiomCheck.lean"
- "Proofs/Inventory*.lean"
- name: anza-ed25519-verified
url: https://github.com/saymrwulf/anza-ed25519-verified.git
kind: ed25519
@ -32,6 +112,86 @@ repos:
- the default HEEA/Zebra verify() path
- accelerator paths
# certificates + imports MEASURED from the two buttons (CERTS arrays;
# apex recovered the way Phase 3c itself recovers it). replay_exclude:
# the button-owned audit instruments the replay does not compile —
# disclosed per-leaf in replay.instruments_excluded.
certificates:
- CurveFieldProofs.fieldImplementation
- CurveFieldProofs.edwardsImplementation
- CurveFieldProofs.naf_table_spec
- CurveFieldProofs.naf_select_spec
- CurveFieldProofs.proj_double_law
- CurveFieldProofs.compl_as_projective_law
- CurveFieldProofs.dsm_step_p_law
- CurveFieldProofs.dsm_step_b_law
- CurveFieldProofs.dsm_loop_spec
- CurveFieldProofs.naf_load_spec
- CurveFieldProofs.naf_exit
- CurveFieldProofs.naf_digit_loop_spec
- CurveFieldProofs.non_adjacent_form_spec
- CurveFieldProofs.run_basepoint
- CurveFieldProofs.vartime_double_base_mul_spec
- CurveFieldProofs.verify_loop_full
- CurveFieldProofs.to_bytes_spec
- CurveFieldProofs.ed_compress_spec
- ScalarProofs.from_bytes_mod_order_wide_spec
- CurveFieldProofs.vartime_dsm_basepoint_spec
- CurveFieldProofs.enc_point_inj
- CurveFieldProofs.pow_p58_spec
- CurveFieldProofs.fe_ct_eq_spec
- CurveFieldProofs.sqrt_core
- CurveFieldProofs.sqrt_ratio_i_sq_spec
- CurveFieldProofs.from_bytes_spec
- CurveFieldProofs.decompress_of_canonical
- CurveFieldProofs.verify_accepts_iff
- CurveFieldProofs.verify_accepts_iff_decompress
- CurveFieldProofs.verify_accepts_iff_point
- CurveFieldProofs.verify_accepts_iff_point_eq
- ScalarProofs.L_val
- ScalarProofs.sub_loop_spec
- ScalarProofs.cond_add_l_one_spec
- ScalarProofs.sub_val_spec
- ScalarProofs.add_val_spec
- ScalarProofs.mul_internal_spec
- ScalarProofs.part1_spec
- ScalarProofs.montgomery_reduce_spec
- ScalarProofs.mul_spec
- ScalarProofs.scalarImplementation
- ScalarProofs.montgomery_mul_spec
- ScalarProofs.bytes_unpack_spec
- ScalarProofs.from_bytes_wide_spec
axiom_imports:
- Proofs.FieldMain
- Proofs.EdMain
- Proofs.ScalarMain
- Proofs.ToBytesSpec
- Proofs.CompressSpec
- Proofs.ScalarPackSpec
- Proofs.DsmMulSpec
- Proofs.SigApexSpec
- Proofs.PointLiftSpec
- Proofs.PointEqSpec
- Proofs.DecompressSpec
- Proofs.FromBytesSpec
- Proofs.DecompressMain
- Proofs.ScalarDenote
- Proofs.ScalarLoop
- Proofs.ScalarSubSpec
- Proofs.ScalarAddSpec
- Proofs.ScalarMulSpec
- Proofs.ScalarMontSpec
- Proofs.ScalarReduceSpec
- Proofs.ScalarFullMulSpec
- Proofs.ScalarWideSpec
- Proofs.ScalarBytesSpec
- Proofs.ScalarUnpackSpec
- Proofs.ScalarFromBytesSpec
replay_exclude:
- "Proofs/Audit.lean"
- "Proofs/ScalarAudit.lean"
- "Proofs/AxiomCheck.lean"
- "Proofs/Inventory*.lean"
- name: risc0-ed25519-verified
url: https://github.com/saymrwulf/risc0-ed25519-verified.git
kind: ed25519
@ -43,6 +203,86 @@ repos:
lean_guard: verification/lean-guard
backend_warning: pure Rust path only; do not treat zkVM accelerator/syscall path as verified
# certificates + imports MEASURED from the two buttons (CERTS arrays;
# apex recovered the way Phase 3c itself recovers it). replay_exclude:
# the button-owned audit instruments the replay does not compile —
# disclosed per-leaf in replay.instruments_excluded.
certificates:
- CurveFieldProofs.fieldImplementation
- CurveFieldProofs.edwardsImplementation
- CurveFieldProofs.naf_table_spec
- CurveFieldProofs.naf_select_spec
- CurveFieldProofs.proj_double_law
- CurveFieldProofs.compl_as_projective_law
- CurveFieldProofs.dsm_step_p_law
- CurveFieldProofs.dsm_step_b_law
- CurveFieldProofs.dsm_loop_spec
- CurveFieldProofs.naf_load_spec
- CurveFieldProofs.naf_exit
- CurveFieldProofs.naf_digit_loop_spec
- CurveFieldProofs.non_adjacent_form_spec
- CurveFieldProofs.run_basepoint
- CurveFieldProofs.vartime_double_base_mul_spec
- CurveFieldProofs.verify_loop_full
- CurveFieldProofs.to_bytes_spec
- CurveFieldProofs.ed_compress_spec
- ScalarProofs.from_bytes_mod_order_wide_spec
- CurveFieldProofs.vartime_dsm_basepoint_spec
- CurveFieldProofs.enc_point_inj
- CurveFieldProofs.pow_p58_spec
- CurveFieldProofs.fe_ct_eq_spec
- CurveFieldProofs.sqrt_core
- CurveFieldProofs.sqrt_ratio_i_sq_spec
- CurveFieldProofs.from_bytes_spec
- CurveFieldProofs.decompress_of_canonical
- CurveFieldProofs.verify_accepts_iff
- CurveFieldProofs.verify_accepts_iff_decompress
- CurveFieldProofs.verify_accepts_iff_point
- CurveFieldProofs.verify_accepts_iff_point_eq
- ScalarProofs.L_val
- ScalarProofs.sub_loop_spec
- ScalarProofs.sub_loop1_one_spec
- ScalarProofs.sub_val_spec
- ScalarProofs.add_val_spec
- ScalarProofs.mul_internal_spec
- ScalarProofs.part1_spec
- ScalarProofs.montgomery_reduce_spec
- ScalarProofs.mul_spec
- ScalarProofs.scalarImplementation
- ScalarProofs.montgomery_mul_spec
- ScalarProofs.bytes_unpack_spec
- ScalarProofs.from_bytes_wide_spec
axiom_imports:
- Proofs.FieldMain
- Proofs.EdMain
- Proofs.ScalarMain
- Proofs.ToBytesSpec
- Proofs.CompressSpec
- Proofs.ScalarPackSpec
- Proofs.DsmMulSpec
- Proofs.SigApexSpec
- Proofs.PointLiftSpec
- Proofs.PointEqSpec
- Proofs.DecompressSpec
- Proofs.FromBytesSpec
- Proofs.DecompressMain
- Proofs.ScalarDenote
- Proofs.ScalarLoop
- Proofs.ScalarSubSpec
- Proofs.ScalarAddSpec
- Proofs.ScalarMulSpec
- Proofs.ScalarMontSpec
- Proofs.ScalarReduceSpec
- Proofs.ScalarFullMulSpec
- Proofs.ScalarWideSpec
- Proofs.ScalarBytesSpec
- Proofs.ScalarUnpackSpec
- Proofs.ScalarFromBytesSpec
replay_exclude:
- "Proofs/Audit.lean"
- "Proofs/ScalarAudit.lean"
- "Proofs/AxiomCheck.lean"
- "Proofs/Inventory*.lean"
- name: betrusted-ed25519-verified
url: https://github.com/saymrwulf/betrusted-ed25519-verified.git
kind: ed25519
@ -54,6 +294,86 @@ repos:
lean_guard: verification/lean-guard
backend_warning: pure Rust path only; do not treat Engine25519/hardware accelerator as verified
# certificates + imports MEASURED from the two buttons (CERTS arrays;
# apex recovered the way Phase 3c itself recovers it). replay_exclude:
# the button-owned audit instruments the replay does not compile —
# disclosed per-leaf in replay.instruments_excluded.
certificates:
- CurveFieldProofs.fieldImplementation
- CurveFieldProofs.edwardsImplementation
- CurveFieldProofs.naf_table_spec
- CurveFieldProofs.naf_select_spec
- CurveFieldProofs.proj_double_law
- CurveFieldProofs.compl_as_projective_law
- CurveFieldProofs.dsm_step_p_law
- CurveFieldProofs.dsm_step_b_law
- CurveFieldProofs.dsm_loop_spec
- CurveFieldProofs.naf_load_spec
- CurveFieldProofs.naf_exit
- CurveFieldProofs.naf_digit_loop_spec
- CurveFieldProofs.non_adjacent_form_spec
- CurveFieldProofs.run_basepoint
- CurveFieldProofs.vartime_double_base_mul_spec
- CurveFieldProofs.verify_loop_full
- CurveFieldProofs.to_bytes_spec
- CurveFieldProofs.ed_compress_spec
- ScalarProofs.from_bytes_mod_order_wide_spec
- CurveFieldProofs.vartime_dsm_basepoint_spec
- CurveFieldProofs.enc_point_inj
- CurveFieldProofs.pow_p58_spec
- CurveFieldProofs.fe_ct_eq_spec
- CurveFieldProofs.sqrt_core
- CurveFieldProofs.sqrt_ratio_i_sq_spec
- CurveFieldProofs.from_bytes_spec
- CurveFieldProofs.decompress_of_canonical
- CurveFieldProofs.verify_accepts_iff
- CurveFieldProofs.verify_accepts_iff_decompress
- CurveFieldProofs.verify_accepts_iff_point
- CurveFieldProofs.verify_accepts_iff_point_eq
- ScalarProofs.L_val
- ScalarProofs.sub_loop_spec
- ScalarProofs.sub_loop1_one_spec
- ScalarProofs.sub_val_spec
- ScalarProofs.add_val_spec
- ScalarProofs.mul_internal_spec
- ScalarProofs.part1_spec
- ScalarProofs.montgomery_reduce_spec
- ScalarProofs.mul_spec
- ScalarProofs.scalarImplementation
- ScalarProofs.montgomery_mul_spec
- ScalarProofs.bytes_unpack_spec
- ScalarProofs.from_bytes_wide_spec
axiom_imports:
- Proofs.FieldMain
- Proofs.EdMain
- Proofs.ScalarMain
- Proofs.ToBytesSpec
- Proofs.CompressSpec
- Proofs.ScalarPackSpec
- Proofs.DsmMulSpec
- Proofs.SigApexSpec
- Proofs.PointLiftSpec
- Proofs.PointEqSpec
- Proofs.DecompressSpec
- Proofs.FromBytesSpec
- Proofs.DecompressMain
- Proofs.ScalarDenote
- Proofs.ScalarLoop
- Proofs.ScalarSubSpec
- Proofs.ScalarAddSpec
- Proofs.ScalarMulSpec
- Proofs.ScalarMontSpec
- Proofs.ScalarReduceSpec
- Proofs.ScalarFullMulSpec
- Proofs.ScalarWideSpec
- Proofs.ScalarBytesSpec
- Proofs.ScalarUnpackSpec
- Proofs.ScalarFromBytesSpec
replay_exclude:
- "Proofs/Audit.lean"
- "Proofs/ScalarAudit.lean"
- "Proofs/AxiomCheck.lean"
- "Proofs/Inventory*.lean"
- name: pasta-pallas-verified
url: https://github.com/saymrwulf/pasta-pallas-verified.git
kind: pasta_pallas
@ -221,3 +541,56 @@ repos:
LTLAcc.take_append_drop: []
LTLAcc.take_drop_prefix: ["propext", "Classical.choice", "Quot.sound"]
LTLAcc.take_take_le: ["propext", "Quot.sound"]
- name: fips205-slhdsa-verified
url: https://github.com/saymrwulf/fips205-slhdsa-verified.git
kind: slh_dsa
verification_dir: verification
verified_backend: verify-mono/sha2-128s
env_script: ~/aeneas-toolchain/env.sh
lean_project_dir: $AENEAS_HOME/backends/lean
lean_guard: verification/lean-guard
backend_warning: >-
proved subject is the private verify_mono facade; the bridge to the
deployed generic pk.verify() is a 137-case differential test, not a
machine-checked refinement (TRUSTED-BASE item 9)
known_exclusions:
- the five verify-path hash oracles h_msg/f/h/t_l/t_len (assumed, not proven against FIPS 180-4)
- signing and key generation (out of extraction scope entirely)
- "everything above the extraction root: M' assembly, the pure/prehash domain-separator byte, ctx length bound, deserialization (TRUSTED-BASE item 10)"
- the base_2b inner loop (threaded opaquely, no certificate)
- parameter sets other than SLH-DSA-SHA2-128s
- compiler correctness and side channels
axiom_imports:
- Proofs.ApexSpec
- Proofs.ChainSpec
- Proofs.ForsInnerSpec
- Proofs.ForsOuterSpec
- Proofs.HtSpec
- Proofs.InputPrepSpec
- Proofs.WotsSpec
- Proofs.XmssSpec
certificates:
- fips205.base2b_outer_loop_eq
- fips205.chain_free_loop_eq
- fips205.fors_inner_loop_eq
- fips205.fors_outer_loop_eq
- fips205.ht_loop_eq
- fips205.slh_verify_128s_accepts_iff
- fips205.to_byte_loop_eq
- fips205.to_int_loop_eq
- fips205.wots_csum_loop_eq
- fips205.wots_loop1_eq
- fips205.xmss_loop_eq
certificate_axioms:
fips205.base2b_outer_loop_eq: ["Classical.choice", "Quot.sound", "propext"]
fips205.chain_free_loop_eq: ["Classical.choice", "Quot.sound", "propext", "verify_mono.oracle.f"]
fips205.fors_inner_loop_eq: ["Classical.choice", "Quot.sound", "propext", "verify_mono.oracle.h"]
fips205.fors_outer_loop_eq: ["Classical.choice", "Quot.sound", "propext", "verify_mono.oracle.f", "verify_mono.oracle.h"]
fips205.ht_loop_eq: ["Classical.choice", "Quot.sound", "propext", "verify_mono.oracle.f", "verify_mono.oracle.h", "verify_mono.oracle.t_l"]
fips205.slh_verify_128s_accepts_iff: ["Classical.choice", "Quot.sound", "propext", "verify_mono.oracle.f", "verify_mono.oracle.h", "verify_mono.oracle.h_msg", "verify_mono.oracle.t_l", "verify_mono.oracle.t_len"]
fips205.to_byte_loop_eq: ["Classical.choice", "Quot.sound", "propext"]
fips205.to_int_loop_eq: ["Classical.choice", "Quot.sound", "propext"]
fips205.wots_csum_loop_eq: ["Classical.choice", "Quot.sound", "propext"]
fips205.wots_loop1_eq: ["Classical.choice", "Quot.sound", "propext", "verify_mono.oracle.f"]
fips205.xmss_loop_eq: ["Classical.choice", "Quot.sound", "propext", "verify_mono.oracle.h"]

View file

@ -19,14 +19,14 @@
## Live evidence
- Transparency log (RFC 9162): https://ltl.zkdefi.org — signed replay attestations of the Lean proofs. Thirteen leaves: three replay generations over four Ed25519 forks, plus entry 13 attesting the Lean mechanization of the log's own accumulator model. The mirror ships a fail-closed offline verifier (verify.py --all covers every leaf, signed head, and receipt) with an adversarial self-test.
- The paper: https://ltl.zkdefi.org/paper — "Accountable Distribution of Machine-Checked Correctness Evidence: A Transparency Model and the Lean Transparency Log" (23 pages: trust decomposition, scheme-level accountability games with explicit reductions, live deployment, and the measured model/deployment divergence reported as a result). Earlier versions archived at /paper/v0.2 (19 pages) and /paper/v0.1 (4 pages).
- Transparency log (RFC 9162): https://ltl.zkdefi.org — signed replay attestations of the Lean proofs. Nineteen leaves: three replay generations over four Ed25519 forks (newest at 44/44 certificates, leaves 13-16), the Lean mechanization of the log's own accumulator model (leaves 12 and 17), and the first post-quantum leaf - the SLH-DSA (FIPS 205) verify path (leaf 18); heads dual-signed (Ed25519 + SLH-DSA) since size 14. The mirror ships a fail-closed offline verifier (verify.py --all covers every leaf, signed head, and receipt) with an adversarial self-test.
- The paper: https://ltl.zkdefi.org/paper — "Accountable Distribution of Machine-Checked Correctness Evidence: A Transparency Model and the Lean Transparency Log" (25 pages, v0.15, revised August 2026, DOI 10.5281/zenodo.22057482: trust decomposition, scheme-level accountability games with explicit reductions, live deployment, and the measured model/deployment divergence reported as a result). Superseded drafts remain retrievable from the repository git history.
## For agents
warden speaks MCP over stdio: `pacta wallet mcp --wallet <dir>`. Tools:
`wallet_status`, `verify_inbound`, `request_signature`, `custody_card`,
`posture_challenge`, `list_incidents`, `explain_refusal`. Errors are
`posture_challenge`, `list_incidents`, `explain_refusal`, `airgap_pending`. Errors are
structured objects (code / missing / remediation). The custody card at
`.well-known/custody-card.json` is self-proving: it embeds transparency-log
inclusion proofs a counterparty recomputes rather than trusts.
@ -36,4 +36,4 @@ inclusion proofs a counterparty recomputes rather than trusts.
Verification paths are certificate-covered; signing is trusted base (the
attested artifact, fenced by the firewall). SHA-512 is an opaque oracle;
wire parsers are hypotheses; reproducible builds and side channels are R5,
not claimed; ML-DSA (PQC) fails closed — no proven implementation exists.
not claimed; ML-DSA fails closed — no proven implementation exists; the SLH-DSA verify path IS proven (verify only; signing remains trusted base).

View file

@ -1,188 +1,168 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# PACTA Curriculum: From Zero to Hero\n",
"\n",
"This curriculum teaches proof-aware cryptographic tooling from first principles to a research-grade professional workflow. It is designed for undergraduate students who know some programming and discrete math, but have not yet worked with formal verification, Lean, certificate transparency, or autonomous-agent risk gates.\n",
"\n",
"The practical anchor is PACTA: Proof-Aware Crypto Tooling Agent. The goal is not to build a trading bot. The goal is to teach an agent, and the engineer supervising it, to ask:\n",
"\n",
"> Does this theorem cover the exact code path that will protect funds?\n",
"\n",
"The course takes that question seriously. Every notebook connects theory to a runnable artifact in this repository.\n"
]
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# PACTA Curriculum: From Zero to Hero\n",
"\n",
"This curriculum teaches proof-aware cryptographic tooling from first principles to a research-grade professional workflow. It is designed for undergraduate students who know some programming and discrete math, but have not yet worked with formal verification, Lean, certificate transparency, or autonomous-agent risk gates.\n",
"\n",
"The practical anchor is PACTA: Proof-Aware Crypto Tooling Agent. The goal is not to build a trading bot. The goal is to teach an agent, and the engineer supervising it, to ask:\n",
"\n",
"> Does this theorem cover the exact code path that will protect funds?\n",
"\n",
"The course takes that question seriously. Every notebook connects theory to a runnable artifact in this repository.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Learning Objectives\n",
"\n",
"By the end of the sequence, a strong student should be able to:\n",
"\n",
"- Explain why cryptographic implementation proofs have theorem boundaries.\n",
"- Distinguish formal proof evidence from tests, audits, marketing claims, and operational controls.\n",
"- Read a PACTA claim card and understand its guarantees, preconditions, exclusions, trusted base, and risk level.\n",
"- Reproduce a local Lean replay or diagnose why local replay is unavailable.\n",
"- Perform a proof hygiene scan and explain why `sorry`, local axioms, and trivial theorem targets are dangerous.\n",
"- Explain how a third-party proof-checking provider changes the trusted base.\n",
"- Implement and verify RFC 9162-style Merkle inclusion and consistency proofs.\n",
"- Explain why Signed Tree Heads need accountable signatures, why Ed25519 is useful here, why ML-DSA requires a real backend, and how the shipped SLH-DSA co-signature differs from both.\n",
"- Design policy gates that convert verification evidence into consequences.\n",
"- Read R4 four-tier apex evidence, name its residual blockers, and write a research plan toward R5 production assurance.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Prerequisites\n",
"\n",
"Recommended background:\n",
"\n",
"- Python basics: functions, dictionaries, lists, files, subprocesses.\n",
"- Discrete math: modular arithmetic, induction, trees, hashes.\n",
"- Basic cryptography vocabulary: public keys, signatures, hashes, finite fields.\n",
"- Basic command-line usage on macOS or Linux.\n",
"\n",
"Not required at the start:\n",
"\n",
"- Lean.\n",
"- Rust internals.\n",
"- Elliptic curve implementation expertise.\n",
"- Certificate transparency expertise.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import sys\n",
"\n",
"repo_root = Path.cwd()\n",
"if not (repo_root / \"src\" / \"pacta\").exists():\n",
" repo_root = repo_root.parent\n",
"sys.path.insert(0, str(repo_root / \"src\"))\n",
"\n",
"print(repo_root)\n",
"print((repo_root / \"README.md\").exists())\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Course Map\n",
"\n",
"1. `01_threat_model_and_truth_boundary.ipynb`\n",
" Learn the product problem, the security boundary, and the difference between verified arithmetic and verified wallets.\n",
"\n",
"2. `02_claim_cards_and_risk_model.ipynb`\n",
" Study PACTA claim cards, risk levels R0-R5, and how claim serialization supports machine decisions.\n",
"\n",
"3. `03_lean_replay_and_axiom_audit.ipynb`\n",
" Learn how local Lean replay works, why PACTA avoids transpilation, and what an axiom audit proves.\n",
"\n",
"4. `04_proof_hygiene_and_boundaries.ipynb`\n",
" Learn to scan proof artifacts for `sorry`, local `axiom`, trivial theorem statements, and missing manifest coverage.\n",
"\n",
"5. `05_third_party_attestation_provider.ipynb`\n",
" Learn how a proof-checking service can transform hard local verification into provider trust.\n",
"\n",
"6. `06_merkle_transparency_logs.ipynb`, then the MIRRORED PAIR\n",
" `06a_provider_build_the_log.ipynb` / `06b_agent_verify_inclusion.ipynb`\n",
"\n",
" The trust architecture has exactly two domains - ONE provider\n",
" who builds and signs the authenticated structure (and pays the\n",
" Lean bill), MANY agents who verify inclusion proofs in\n",
" milliseconds. The course mirrors that split structurally: 6a is\n",
" written entirely in the provider's voice, 6b entirely in the\n",
" agent's. If you cannot say which notebook a step belongs to,\n",
" you have not understood the step.\n",
" Build the Merkle accumulator intuition behind inclusion proofs, consistency proofs, and Signed Tree Heads.\n",
"\n",
"7. `07_agent_consequences.ipynb`\n",
" Connect evidence to action: build a lower-layer Rust capsule only when policy gates pass.\n",
"\n",
"8. `08_capstone_research_program.ipynb`\n",
" Design a PhD-level roadmap for closing the gaps from R4 toward R5.\n",
"\n",
"9. `09_dogfood_verified_crypto.ipynb`\n",
"\n",
" Eat your own dogfood: run the agent's own signature checks through the proven code path, and hold the post-quantum line honestly.\n",
"\n",
"## The Ratchet Rule\n",
"\n",
"This course follows the same didactic contract as its companion book (*Verifying Cryptography with Lean 4*): **every load-bearing idea is worked at least twice** - once at napkin scale (trees of three leaves, toy claim cards you can score in your head) and once at real scale (the shipped R4 claim cards, real receipts, the real proven-path verifier), with nothing hidden in between. The napkin run teaches the moves; the real-size run proves the moves are the whole story. If a step ever feels like a leap, back up one cell: the smaller rung is there.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Assessment Model\n",
"\n",
"Each notebook contains:\n",
"\n",
"- A lecture section for concepts.\n",
"- A lab section with runnable code.\n",
"- Checkpoints that force precise answers.\n",
"- Exercises for mastery.\n",
"- Research prompts for advanced students.\n",
"\n",
"The capstone asks students to produce a defensible assurance case, not a slogan.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## References\n",
"\n",
"- RFC 9162, Certificate Transparency Version 2.0: https://datatracker.ietf.org/doc/html/rfc9162\n",
"- RFC 8032, Edwards-Curve Digital Signature Algorithm: https://datatracker.ietf.org/doc/html/rfc8032\n",
"- NIST FIPS 204, Module-Lattice-Based Digital Signature Standard: https://csrc.nist.gov/pubs/fips/204/final\n",
"- PACTA README: `../README.md`\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Learning Objectives\n",
"\n",
"By the end of the sequence, a strong student should be able to:\n",
"\n",
"- Explain why cryptographic implementation proofs have theorem boundaries.\n",
"- Distinguish formal proof evidence from tests, audits, marketing claims, and operational controls.\n",
"- Read a PACTA claim card and understand its guarantees, preconditions, exclusions, trusted base, and risk level.\n",
"- Reproduce a local Lean replay or diagnose why local replay is unavailable.\n",
"- Perform a proof hygiene scan and explain why `sorry`, local axioms, and trivial theorem targets are dangerous.\n",
"- Explain how a third-party proof-checking provider changes the trusted base.\n",
"- Implement and verify RFC 9162-style Merkle inclusion and consistency proofs.\n",
"- Explain why Signed Tree Heads need accountable signatures, why Ed25519 is useful here, and why ML-DSA requires a real backend.\n",
"- Design policy gates that convert verification evidence into consequences.\n",
"- Read R4 four-tier apex evidence, name its residual blockers, and write a research plan toward R5 production assurance.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Prerequisites\n",
"\n",
"Recommended background:\n",
"\n",
"- Python basics: functions, dictionaries, lists, files, subprocesses.\n",
"- Discrete math: modular arithmetic, induction, trees, hashes.\n",
"- Basic cryptography vocabulary: public keys, signatures, hashes, finite fields.\n",
"- Basic command-line usage on macOS or Linux.\n",
"\n",
"Not required at the start:\n",
"\n",
"- Lean.\n",
"- Rust internals.\n",
"- Elliptic curve implementation expertise.\n",
"- Certificate transparency expertise.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import sys\n",
"\n",
"repo_root = Path.cwd()\n",
"if not (repo_root / \"src\" / \"pacta\").exists():\n",
" repo_root = repo_root.parent\n",
"sys.path.insert(0, str(repo_root / \"src\"))\n",
"\n",
"print(repo_root)\n",
"print((repo_root / \"README.md\").exists())\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Course Map\n",
"\n",
"1. `01_threat_model_and_truth_boundary.ipynb`\n",
" Learn the product problem, the security boundary, and the difference between verified arithmetic and verified wallets.\n",
"\n",
"2. `02_claim_cards_and_risk_model.ipynb`\n",
" Study PACTA claim cards, risk levels R0-R5, and how claim serialization supports machine decisions.\n",
"\n",
"3. `03_lean_replay_and_axiom_audit.ipynb`\n",
" Learn how local Lean replay works, why PACTA avoids transpilation, and what an axiom audit proves.\n",
"\n",
"4. `04_proof_hygiene_and_boundaries.ipynb`\n",
" Learn to scan proof artifacts for `sorry`, local `axiom`, trivial theorem statements, and missing manifest coverage.\n",
"\n",
"5. `05_third_party_attestation_provider.ipynb`\n",
" Learn how a proof-checking service can transform hard local verification into provider trust.\n",
"\n",
"6. `06_merkle_transparency_logs.ipynb`, then the MIRRORED PAIR\n",
" `06a_provider_build_the_log.ipynb` / `06b_agent_verify_inclusion.ipynb`\n",
"\n",
" The trust architecture has exactly two domains - ONE provider\n",
" who builds and signs the authenticated structure (and pays the\n",
" Lean bill), MANY agents who verify inclusion proofs in\n",
" milliseconds. The course mirrors that split structurally: 6a is\n",
" written entirely in the provider's voice, 6b entirely in the\n",
" agent's. If you cannot say which notebook a step belongs to,\n",
" you have not understood the step.\n",
" Build the Merkle accumulator intuition behind inclusion proofs, consistency proofs, and Signed Tree Heads.\n",
"\n",
"7. `07_agent_consequences.ipynb`\n",
" Connect evidence to action: build a lower-layer Rust capsule only when policy gates pass.\n",
"\n",
"8. `08_capstone_research_program.ipynb`\n",
" Design a PhD-level roadmap for closing the gaps from R4 toward R5.\n",
"\n",
"9. `09_dogfood_verified_crypto.ipynb`\n",
"\n",
" Eat your own dogfood: run the agent's own signature checks through the proven code path, and hold the post-quantum line honestly.\n",
"\n",
"## The Ratchet Rule\n",
"\n",
"This course follows the same didactic contract as its companion book (*Verifying Cryptography with Lean 4*): **every load-bearing idea is worked at least twice** - once at napkin scale (trees of three leaves, toy claim cards you can score in your head) and once at real scale (the shipped R4 claim cards, real receipts, the real proven-path verifier), with nothing hidden in between. The napkin run teaches the moves; the real-size run proves the moves are the whole story. If a step ever feels like a leap, back up one cell: the smaller rung is there.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Assessment Model\n",
"\n",
"Each notebook contains:\n",
"\n",
"- A lecture section for concepts.\n",
"- A lab section with runnable code.\n",
"- Checkpoints that force precise answers.\n",
"- Exercises for mastery.\n",
"- Research prompts for advanced students.\n",
"\n",
"The capstone asks students to produce a defensible assurance case, not a slogan.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## References\n",
"\n",
"- RFC 9162, Certificate Transparency Version 2.0: https://datatracker.ietf.org/doc/html/rfc9162\n",
"- RFC 8032, Edwards-Curve Digital Signature Algorithm: https://datatracker.ietf.org/doc/html/rfc8032\n",
"- NIST FIPS 204, Module-Lattice-Based Digital Signature Standard: https://csrc.nist.gov/pubs/fips/204/final\n",
"- PACTA README: `../README.md`\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The course's operational twin: the cockpit and the lab manual\n",
"\n",
"These notebooks are the *code-level* track: you build the machinery with your own hands.\n",
"Two sibling surfaces teach the *operational* track on the very same system:\n",
"\n",
"- **The custody cockpit** — `pacta wallet cockpit --demo` serves a read-only bridge of six\n",
" role stations (proposer, quorum bench, operator, cryptographer, architect, newcomer) over\n",
" live evidence, with a tmux-style deck (`/deck`) showing every station in parallel.\n",
"- **The lab manual** (`docs/warden-lab-manual.md`, served at `/manual`) — a study-club course\n",
" that teaches each *role* hands-on: labs with checkpoints, a safe ledger-tamper drill,\n",
" self-tests, and a capstone incident walked through all six chairs.\n",
"\n",
"Recommended weave: notebooks 0009 first (build it), then the lab manual (run it as a crew),\n",
"with notebook 10 and the manual's warden sessions read side by side."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
"nbformat": 4,
"nbformat_minor": 5
}

View file

@ -128,7 +128,8 @@
"Policy matters:\n",
"\n",
"- `require-signatures ed25519`: verify Ed25519 and allow ML-DSA to be unavailable.\n",
"- `require-signatures both`: require Ed25519 and ML-DSA verified. If ML-DSA is unavailable, fail closed.\n"
"- `require-signatures both`: require Ed25519 and ML-DSA verified. If ML-DSA is unavailable, fail closed.\n",
"- `--slhdsa-public-key <pem>`: additionally verify the second (post-quantum) SLH-DSA co-signature on the head; heads before tree size 14 report `absent` (allowed), a present-but-wrong signature fails closed.\n"
]
},
{
@ -156,6 +157,59 @@
"ML-DSA adds post-quantum robustness for the accumulator signature layer. But it must be a real signature, not an aspirational label. If a host lacks ML-DSA, the correct result is an explicit blocker.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The second signature that actually shipped: SLH-DSA\n",
"\n",
"Since tree size 14, every head of the LIVE log carries a second,\n",
"deterministic **SLH-DSA-SHA2-128s** (FIPS 205) signature beside the\n",
"required Ed25519 one. This is not the ML-DSA slot above - it is a\n",
"hash-based scheme, and it was chosen because the estate has PROVEN\n",
"its verify path (eleven certificates, log leaf 18): the log\n",
"co-signs with the parameter set whose verification path it itself\n",
"attests. Three design facts worth internalizing:\n",
"\n",
"1. Heads published before size 14 carry no co-signature, and\n",
" verifiers report them `ABSENT` rather than failing them - an\n",
" append-only log keeps the history of its own signature-scheme\n",
" upgrades.\n",
"2. The co-signature is deterministic on purpose: re-signing the\n",
" same payload is byte-comparable, so \"same input, same\n",
" signature\" becomes a diff you can run, not an assurance you\n",
" must trust.\n",
"3. Signing is still never proven - here, as everywhere in this\n",
" estate, certificates cover the VERIFY path only.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Runnable where OpenSSL >= 3.5 is present; honest skip otherwise.\n",
"import tempfile\n",
"from pathlib import Path\n",
"\n",
"from pacta import slhdsa\n",
"\n",
"tmp = Path(tempfile.mkdtemp(prefix=\"nb06-slhdsa-\"))\n",
"try:\n",
" slhdsa.generate_slhdsa_keypair(tmp / \"slh.key\", tmp / \"slh.pub\")\n",
"except Exception as exc:\n",
" print(\"SLH-DSA unavailable on this host (OpenSSL >= 3.5 needed):\", exc)\n",
"else:\n",
" payload = b\"canonical STH payload bytes\"\n",
" block = slhdsa.slh_dsa_signature_block(payload, tmp / \"slh.key\", tmp / \"slh.pub\")\n",
" ok, err = slhdsa.verify_payload_slhdsa(payload, block[\"signature_base64\"], tmp / \"slh.pub\")\n",
" print(\"co-signature verifies:\", ok, err or \"\")\n",
" block2 = slhdsa.slh_dsa_signature_block(payload, tmp / \"slh.key\", tmp / \"slh.pub\")\n",
" print(\"deterministic (byte-equal re-sign):\",\n",
" block[\"signature_base64\"] == block2[\"signature_base64\"])\n"
]
},
{
"cell_type": "markdown",
"metadata": {},

View file

@ -201,6 +201,7 @@
"- Modify a claim card to R2 and show that `build-library` is refused.\n",
"- Explain why a denial artifact is useful for auditability.\n",
"- Design a policy where an agent requires `both` Ed25519 and ML-DSA signatures for production deployment but allows Ed25519-only in a local lab.\n",
"- Extend it: when should the agent also require the SLH-DSA co-signature, given that heads before tree size 14 legitimately lack it?\n",
"- Write a downstream Rust pseudo-code snippet that imports the generated capsule before enabling a code path.\n"
]
}

View file

@ -18,7 +18,7 @@
"- State precisely which parts of the dogfood verifier are certificate-covered and which are its trusted base.\n",
"- Extract a raw Ed25519 key from an OpenSSL PEM by hand (napkin) and mechanically (real).\n",
"- Demonstrate backend dispatch and the fail-closed `--require-verified-verifier` policy.\n",
"- Defend the hybrid post-quantum posture: one proven-classical signature plus one required-but-honest ML-DSA slot.\n"
"- Defend the three-legged post-quantum posture: proven-classical Ed25519, the shipped SLH-DSA co-signature with its attested verify path, and a required-but-honest ML-DSA slot.\n"
]
},
{
@ -122,7 +122,8 @@
"leaf, against the very tree it is about to sign. The verdict is\n",
"embedded in the signature block (`signing_provenance`: backend,\n",
"library commit, leaf index, `self_inclusion: verified`,\n",
"certificates 16/16). Lectures 6a/6b walk both sides of this.\n",
"certificates 44/44 - the signer's source family was re-attested at 44\n",
"certificates as leaf 13). Lectures 6a/6b walk both sides of this.\n",
"Honesty note unchanged: the library's VERIFY path is\n",
"certificate-covered; the signing path is declared trusted base -\n",
"but it is the attested artifact, not an un-attested third\n",
@ -130,12 +131,13 @@
"\n",
"## The post-quantum line, held honestly\n",
"\n",
"The dogfood loop deliberately does NOT extend to ML-DSA. There is no formally verified ML-DSA implementation in this corpus, and pretending otherwise would poison the whole posture. The hybrid strategy is therefore asymmetric on purpose:\n",
"The posture has three legs now, and each is exactly as strong as it claims:\n",
"\n",
"- **Ed25519 (classical): proven path.** The signature everyone can check today runs on certificate-covered code.\n",
"- **ML-DSA-65 (post-quantum): required, honest, unavailable-until-real.** The tree-head slot exists in every signed structure; `--require-signatures both` fails CLOSED on hosts without a real FIPS 204 backend; and when a real backend lands, the policy flips on without a schema change.\n",
"- **Ed25519 (classical): proven verify path, dogfooded.** The signature everyone can check today runs on certificate-covered code.\n",
"- **SLH-DSA-SHA2-128s (post-quantum): shipped and attested.** Since tree size 14 every live head carries a second, deterministic SLH-DSA co-signature. The estate proved the VERIFY path of a pinned Rust FIPS 205 implementation (eleven certificates) and appended that attestation as leaf 18 - so the co-signature uses exactly the parameter set the log itself attests. Consumers check it with `pacta receipt-verify ... --slhdsa-public-key provider.slhdsa.pub` or the mirror's `verify.py`. Signing remains unproven - verify paths only, always.\n",
"- **ML-DSA-65 (lattice PQ): required, honest, unavailable-until-real.** The tree-head slot exists in every signed structure; `--require-signatures both` fails CLOSED on hosts without a real FIPS 204 backend; when a real backend lands, the policy flips on without a schema change.\n",
"\n",
"A migration strategy that records \"we cannot do this yet\" as a deployment blocker is strictly stronger than one that ships a placeholder. Blockers get fixed; placeholders get trusted.\n"
"A migration strategy that records \"we cannot do this yet\" as a deployment blocker is strictly stronger than one that ships a placeholder. Blockers get fixed; placeholders get trusted. And the SLH-DSA leg shows the endgame: a slot stops being aspirational the day its verify path enters the log.\n"
]
},
{

View file

@ -1,364 +1,364 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Lecture 10: The Verified-Custody Wallet (warden)\n",
"\n",
"Everything so far *decided* which cryptographic code to trust.\n",
"This lecture *acts* on the decision: we build a custody boundary\n",
"out of the four proven curve25519-dalek forks and use it to\n",
"guard signatures - inbound and outbound.\n",
"\n",
"The one idea: **inbound acceptance requires a unanimous quorum of\n",
"provably-equivalent verifiers, and every outbound signature must\n",
"pass the same quorum before it is released.**\n",
"\n",
"We keep the course's ratchet rule: every load-bearing idea runs\n",
"twice - napkin scale by hand, then real scale against the live\n",
"system - and both are executable here.\n"
]
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Lecture 10: The Verified-Custody Wallet (warden)\n",
"\n",
"Everything so far *decided* which cryptographic code to trust.\n",
"This lecture *acts* on the decision: we build a custody boundary\n",
"out of the four proven curve25519-dalek forks and use it to\n",
"guard signatures - inbound and outbound.\n",
"\n",
"The one idea: **inbound acceptance requires a unanimous quorum of\n",
"provably-equivalent verifiers, and every outbound signature must\n",
"pass the same quorum before it is released.**\n",
"\n",
"We keep the course's ratchet rule: every load-bearing idea runs\n",
"twice - napkin scale by hand, then real scale against the live\n",
"system - and both are executable here.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Learning Objectives\n",
"\n",
"- Explain why a *unanimous* quorum of provably-equivalent\n",
" verifiers turns disagreement into evidence of a fault, and why\n",
" majority voting would hide exactly that fault.\n",
"- Classify a quorum divergence as a documented semantic edge\n",
" (note) versus unexplained (tamper -> latch).\n",
"- Describe the outbound signing firewall as verify-after-sign\n",
" with a proven verifier, and state warden's honest asymmetry\n",
" (verify custody-grade, sign trusted base).\n",
"- Recompute a custody card's inclusion proof as a counterparty -\n",
" trust by recomputation, not by assertion.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Why a quorum, when one proof would do?\n",
"\n",
"Each member is *proven* to decide the same predicate,\n",
"`accept(A,m,R,s) \u21d4 decompress(R) = [k](\u2212A) + [s]B`. So on the\n",
"proven domain they cannot disagree about *meaning*. Classic\n",
"N-version programming hopes independent code won't share a bug;\n",
"we do not hope - we know the semantics coincide, so a runtime\n",
"disagreement is not opinion, it is **evidence of a fault**: a\n",
"corrupted build, a memory error, or tampering. The quorum turns\n",
"\"the verifiers differed\" into an alarm with a theorem behind it.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Napkin scale: a 3-of-3 quorum with toy verifiers\n",
"\n",
"Forget real curves for a moment. Model three verifiers as\n",
"functions and watch the boundary logic: unanimity accepts,\n",
"any disagreement fails closed and is classified.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def toy_quorum(verdicts):\n",
" kinds = set(verdicts.values())\n",
" if kinds == {\"accept\"}:\n",
" return \"unanimous-accept\", True\n",
" if kinds == {\"reject\"}:\n",
" return \"unanimous-reject\", False\n",
" return \"divergence -> FAIL CLOSED + incident\", False\n",
"\n",
"print(toy_quorum({\"dalek\": \"accept\", \"anza\": \"accept\", \"risc0\": \"accept\"}))\n",
"print(toy_quorum({\"dalek\": \"reject\", \"anza\": \"reject\", \"risc0\": \"reject\"}))\n",
"print(toy_quorum({\"dalek\": \"accept\", \"anza\": \"reject\", \"risc0\": \"accept\"}))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The third line is the whole point: a lone dissenter does not get\n",
"out-voted. Acceptance needs *everyone*; anything else is a\n",
"refusal plus a recorded incident. Majority voting would hide\n",
"exactly the fault we most want to see.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The divergence taxonomy\n",
"\n",
"The forks are *allowed* to differ on documented degenerate\n",
"inputs (anza rejects `A = 0` and a legacy excluded-small-order-R\n",
"list). We still fail closed; the taxonomy only grades the alarm:\n",
"\n",
"- **semantic-edge** - they differ AND a documented edge flag\n",
" applies (small-order R, non-canonical s, zero key): severity\n",
" *note*.\n",
"- **unexplained** - they differ with no documented reason, or a\n",
" member errored: severity *tamper* -> custody **latches**.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys, pathlib\n",
"for parent in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents]:\n",
" if (parent / \"src\" / \"pacta\").exists():\n",
" sys.path.insert(0, str(parent / \"src\")); ROOT = parent; break\n",
"\n",
"from pacta.quorum import semantic_edge_flags, SMALL_ORDER_ENCODINGS\n",
"\n",
"small_order_R = sorted(SMALL_ORDER_ENCODINGS)[0]\n",
"print(\"edge flags for a small-order R:\",\n",
" semantic_edge_flags(b\"\\x02\" * 32, small_order_R + b\"\\x00\" * 32))\n",
"print(\"edge flags for an ordinary sig:\",\n",
" semantic_edge_flags(b\"\\x02\" * 32, b\"\\x01\" * 64))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A divergence on the first input is a documented edge (note); a\n",
"divergence on the second has no excuse (tamper). Same fail-closed\n",
"verdict, very different alarm.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Real scale: the four proven forks, if built\n",
"\n",
"If you have run `pacta wallet build-quorum`, the next cell drives\n",
"the **real** four-fork quorum: sign a payload with the dogfood\n",
"(attested) signer, then watch all four proven verifiers agree on\n",
"accept, and on reject for a flipped byte. If the binaries are not\n",
"built, we say so and skip - honestly, the way the wallet itself\n",
"fails closed.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pacta.quorum import load_quorum, binary_path\n",
"\n",
"built = [b for b in (\"dalek\", \"anza\", \"risc0\", \"betrusted\") if binary_path(b).exists()]\n",
"if len(built) < 2:\n",
" print(\"quorum not built (need >=2). Run: pacta wallet build-quorum --sources-root <...>\")\n",
"else:\n",
" import tempfile, os\n",
" from pacta.dogfood import locate_verifier, pem_public_key_to_raw, sign_payload_dogfood\n",
" from pacta.signing import generate_ed25519_keypair\n",
" v = locate_verifier()\n",
" if v is None:\n",
" print(\"dogfood signer not built; run pacta dogfood-build\")\n",
" else:\n",
" d = tempfile.mkdtemp()\n",
" key, pub = os.path.join(d, \"k.pem\"), os.path.join(d, \"k.pub\")\n",
" generate_ed25519_keypair(key, pub)\n",
" payload = b\"curriculum lecture 10 payload\"\n",
" sig = sign_payload_dogfood(payload, key, v)\n",
" pk = pem_public_key_to_raw(pub)\n",
" q = load_quorum(min_members=2)\n",
" print(\"members:\", sorted(q.members))\n",
" good = q.verify(payload, sig, pk)\n",
" print(\"valid signature ->\", good.classification, \"accepted =\", good.accepted)\n",
" bad = q.verify(payload, bytes([sig[0] ^ 0xFF]) + sig[1:], pk)\n",
" print(\"one flipped byte ->\", bad.classification, \"accepted =\", bad.accepted)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The signing firewall: verify-after-sign, but proven\n",
"\n",
"Outbound is `intent -> sign -> firewall -> release`. The fresh\n",
"signature faces the same quorum; only unanimity releases it. A\n",
"rejected self-signature is *quarantined, never returned*, and\n",
"custody latches. This is the textbook fault-injection\n",
"countermeasure - verify a signer's output before trusting it -\n",
"with the verifier upgraded to machine-checked code.\n",
"\n",
"Note the honest asymmetry: the *verify* paths are certificate-\n",
"covered (custody-grade), but the *signing* step is trusted base -\n",
"the attested artifact, not a third implementation. The firewall\n",
"is exactly how we fence that weaker edge.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Two voices, one boundary (the domain split, again)\n",
"\n",
"Lecture 06 split provider and agent. warden inherits the split:\n",
"\n",
"- **The operator voice** seals the capsule: it runs the R4 gate,\n",
" pins the attested source commits, and stores the transparency-\n",
" log receipts that authorized each member.\n",
"- **The counterparty (agent) voice** never trusts the operator's\n",
" adjectives. It reads the *custody card* and recomputes the\n",
" inclusion proofs itself - trust by recomputation.\n",
"\n",
"The next cell is the counterparty side: given a card, verify a\n",
"member's inclusion proof with nothing but stdlib hashing.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Counterparty-side check of ONE member's inclusion proof.\n",
"# (Works whenever you have a wallet + its fetched evidence; here\n",
"# we show the primitive the card relies on.)\n",
"from pacta.transparency import verify_inclusion, leaf_bytes_for_attestation\n",
"import json, glob\n",
"\n",
"ev = sorted(glob.glob(str(ROOT / \"examples\" / \"wallet-evidence\" / \"*.attestation.json\")))\n",
"if not ev:\n",
" print(\"no bundled evidence; fetch with `pacta log-fetch` to try live\")\n",
"else:\n",
" att = json.load(open(ev[0]))\n",
" rec = json.load(open(ev[0].replace(\".attestation.\", \".receipt.\")))\n",
" ok = verify_inclusion(\n",
" leaf_bytes_for_attestation(att),\n",
" rec[\"leaf_index\"], rec[\"tree_size\"],\n",
" [bytes.fromhex(h) for h in rec[\"inclusion_proof\"]],\n",
" bytes.fromhex(rec[\"sth\"][\"root_hash\"]),\n",
" )\n",
" print(f\"{att['subject']['component']}: inclusion recomputes ->\", ok)\n",
" print(\"The counterparty believed no adjective; it recomputed a Merkle root.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Corrupt a member, watch the pin catch it (executable)\n",
"\n",
"The wallet seals each member's SHA-256 into its capsule. The\n",
"next cell stages a COPY of a real member binary in a temp\n",
"directory, \"seals\" its hash the way the capsule does, appends\n",
"one byte (a supply-chain attack in miniature), and re-checks.\n",
"Nothing on your machine is modified.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import hashlib, shutil, tempfile, pathlib\n",
"from pacta.quorum import binary_path\n",
"\n",
"member = binary_path(\"dalek\")\n",
"if not member.exists():\n",
" print(\"quorum not built; run pacta wallet build-quorum first\")\n",
"else:\n",
" stage = pathlib.Path(tempfile.mkdtemp()) / member.name\n",
" shutil.copy2(member, stage)\n",
" sealed = hashlib.sha256(stage.read_bytes()).hexdigest() # capsule pin\n",
" print(\"sealed :\", sealed[:24], \"...\")\n",
" with stage.open(\"ab\") as f:\n",
" f.write(b\"\\x00\") # the attack\n",
" current = hashlib.sha256(stage.read_bytes()).hexdigest()\n",
" print(\"current:\", current[:24], \"...\")\n",
" if current != sealed:\n",
" print(\"PIN CAUGHT IT: wallet.quorum() would refuse to assemble ->\")\n",
" print(\" 'quorum member dalek binary hash changed since the capsule was sealed'\")\n",
" else:\n",
" print(\"impossible: SHA-256 collision\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"One appended byte and the wallet refuses to even *assemble* the\n",
"quorum - before any verification runs. Note what this control\n",
"is and is not: it stops binary substitution *between* wallet\n",
"sessions; an attacker with live root outranks it (see\n",
"docs/threat-model.md, attacker #7 - that is what the choir and\n",
"the airgap profiles are for).\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Exercises\n",
"\n",
"- Change `toy_quorum` to majority voting and write two sentences\n",
" on exactly which attack that lets through.\n",
"- Extend the corrupt-a-member cell: corrupt the capsule JSON\n",
" itself instead of the binary. What catches that, and when?\n",
" (Hint: nothing does until the ledger genesis is compared -\n",
" write down the exact trust statement the capsule hash in the\n",
" genesis entry provides.)\n",
"- The signing path is trusted base. Write the strongest *true*\n",
" sentence you can about warden's outbound safety, and the\n",
" strongest *false* one a marketer would write - and name the\n",
" word that makes the second one false.\n",
"- Design `warden-treasury`: which member re-verifies Solana\n",
" transactions, and what exactly the RPC provider is still\n",
" trusted for after you do.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The human surface: see this wallet through the cockpit\n",
"\n",
"Everything this notebook built programmatically has a read-only human console:\n",
"\n",
"```\n",
"pacta wallet cockpit --demo # throwaway demo wallet, zero setup\n",
"pacta wallet cockpit --wallet DIR # the wallet you just sealed here\n",
"```\n",
"\n",
"Open `/deck` for all six role stations live in parallel (the quorum bench you built is the\n",
"indigo pane; the ledger you hash-chained is re-verified on every page load), and `/manual`\n",
"for the lab-manual sessions that teach each role \u2014 Session 4's tamper drill breaks a *copy*\n",
"of a ledger exactly like this notebook's and watches two independent surfaces catch it.\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Learning Objectives\n",
"\n",
"- Explain why a *unanimous* quorum of provably-equivalent\n",
" verifiers turns disagreement into evidence of a fault, and why\n",
" majority voting would hide exactly that fault.\n",
"- Classify a quorum divergence as a documented semantic edge\n",
" (note) versus unexplained (tamper -> latch).\n",
"- Describe the outbound signing firewall as verify-after-sign\n",
" with a proven verifier, and state warden's honest asymmetry\n",
" (verify custody-grade, sign trusted base).\n",
"- Recompute a custody card's inclusion proof as a counterparty -\n",
" trust by recomputation, not by assertion.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Why a quorum, when one proof would do?\n",
"\n",
"Each member is *proven* to decide the same predicate,\n",
"`accept(A,m,R,s) ⇔ decompress(R) = [k](A) + [s]B`. So on the\n",
"proven domain they cannot disagree about *meaning*. Classic\n",
"N-version programming hopes independent code won't share a bug;\n",
"we do not hope - we know the semantics coincide, so a runtime\n",
"disagreement is not opinion, it is **evidence of a fault**: a\n",
"corrupted build, a memory error, or tampering. The quorum turns\n",
"\"the verifiers differed\" into an alarm with a theorem behind it.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Napkin scale: a 3-of-3 quorum with toy verifiers\n",
"\n",
"Forget real curves for a moment. Model three verifiers as\n",
"functions and watch the boundary logic: unanimity accepts,\n",
"any disagreement fails closed and is classified.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def toy_quorum(verdicts):\n",
" kinds = set(verdicts.values())\n",
" if kinds == {\"accept\"}:\n",
" return \"unanimous-accept\", True\n",
" if kinds == {\"reject\"}:\n",
" return \"unanimous-reject\", False\n",
" return \"divergence -> FAIL CLOSED + incident\", False\n",
"\n",
"print(toy_quorum({\"dalek\": \"accept\", \"anza\": \"accept\", \"risc0\": \"accept\"}))\n",
"print(toy_quorum({\"dalek\": \"reject\", \"anza\": \"reject\", \"risc0\": \"reject\"}))\n",
"print(toy_quorum({\"dalek\": \"accept\", \"anza\": \"reject\", \"risc0\": \"accept\"}))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The third line is the whole point: a lone dissenter does not get\n",
"out-voted. Acceptance needs *everyone*; anything else is a\n",
"refusal plus a recorded incident. Majority voting would hide\n",
"exactly the fault we most want to see.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The divergence taxonomy\n",
"\n",
"The forks are *allowed* to differ on documented degenerate\n",
"inputs (anza rejects `A = 0` and a legacy excluded-small-order-R\n",
"list). We still fail closed; the taxonomy only grades the alarm:\n",
"\n",
"- **semantic-edge** - they differ AND a documented edge flag\n",
" applies (small-order R, non-canonical s, zero key): severity\n",
" *note*.\n",
"- **unexplained** - they differ with no documented reason, or a\n",
" member errored: severity *tamper* -> custody **latches**.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys, pathlib\n",
"for parent in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents]:\n",
" if (parent / \"src\" / \"pacta\").exists():\n",
" sys.path.insert(0, str(parent / \"src\")); ROOT = parent; break\n",
"\n",
"from pacta.quorum import semantic_edge_flags, SMALL_ORDER_ENCODINGS\n",
"\n",
"small_order_R = sorted(SMALL_ORDER_ENCODINGS)[0]\n",
"print(\"edge flags for a small-order R:\",\n",
" semantic_edge_flags(b\"\\x02\" * 32, small_order_R + b\"\\x00\" * 32))\n",
"print(\"edge flags for an ordinary sig:\",\n",
" semantic_edge_flags(b\"\\x02\" * 32, b\"\\x01\" * 64))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A divergence on the first input is a documented edge (note); a\n",
"divergence on the second has no excuse (tamper). Same fail-closed\n",
"verdict, very different alarm.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Real scale: the four proven forks, if built\n",
"\n",
"If you have run `pacta wallet build-quorum`, the next cell drives\n",
"the **real** four-fork quorum: sign a payload with the dogfood\n",
"(attested) signer, then watch all four proven verifiers agree on\n",
"accept, and on reject for a flipped byte. If the binaries are not\n",
"built, we say so and skip - honestly, the way the wallet itself\n",
"fails closed.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pacta.quorum import load_quorum, binary_path\n",
"\n",
"built = [b for b in (\"dalek\", \"anza\", \"risc0\", \"betrusted\") if binary_path(b).exists()]\n",
"if len(built) < 2:\n",
" print(\"quorum not built (need >=2). Run: pacta wallet build-quorum --sources-root <...>\")\n",
"else:\n",
" import tempfile, os\n",
" from pacta.dogfood import locate_verifier, pem_public_key_to_raw, sign_payload_dogfood\n",
" from pacta.signing import generate_ed25519_keypair\n",
" v = locate_verifier()\n",
" if v is None:\n",
" print(\"dogfood signer not built; run pacta dogfood-build\")\n",
" else:\n",
" d = tempfile.mkdtemp()\n",
" key, pub = os.path.join(d, \"k.pem\"), os.path.join(d, \"k.pub\")\n",
" generate_ed25519_keypair(key, pub)\n",
" payload = b\"curriculum lecture 10 payload\"\n",
" sig = sign_payload_dogfood(payload, key, v)\n",
" pk = pem_public_key_to_raw(pub)\n",
" q = load_quorum(min_members=2)\n",
" print(\"members:\", sorted(q.members))\n",
" good = q.verify(payload, sig, pk)\n",
" print(\"valid signature ->\", good.classification, \"accepted =\", good.accepted)\n",
" bad = q.verify(payload, bytes([sig[0] ^ 0xFF]) + sig[1:], pk)\n",
" print(\"one flipped byte ->\", bad.classification, \"accepted =\", bad.accepted)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The signing firewall: verify-after-sign, but proven\n",
"\n",
"Outbound is `intent -> sign -> firewall -> release`. The fresh\n",
"signature faces the same quorum; only unanimity releases it. A\n",
"rejected self-signature is *quarantined, never returned*, and\n",
"custody latches. This is the textbook fault-injection\n",
"countermeasure - verify a signer's output before trusting it -\n",
"with the verifier upgraded to machine-checked code.\n",
"\n",
"Note the honest asymmetry: the *verify* paths are certificate-\n",
"covered (custody-grade), but the *signing* step is trusted base -\n",
"the attested artifact, not a third implementation. The firewall\n",
"is exactly how we fence that weaker edge.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Two voices, one boundary (the domain split, again)\n",
"\n",
"Lecture 06 split provider and agent. warden inherits the split:\n",
"\n",
"- **The operator voice** seals the capsule: it runs the R4 gate,\n",
" pins the attested source commits, and stores the transparency-\n",
" log receipts that authorized each member.\n",
"- **The counterparty (agent) voice** never trusts the operator's\n",
" adjectives. It reads the *custody card* and recomputes the\n",
" inclusion proofs itself - trust by recomputation.\n",
"\n",
"The next cell is the counterparty side: given a card, verify a\n",
"member's inclusion proof with nothing but stdlib hashing.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Counterparty-side check of ONE member's inclusion proof.\n",
"# (Works whenever you have a wallet + its fetched evidence; here\n",
"# we show the primitive the card relies on.)\n",
"from pacta.transparency import verify_inclusion, leaf_bytes_for_attestation\n",
"import json, glob\n",
"\n",
"ev = sorted(glob.glob(str(ROOT / \"examples\" / \"wallet-evidence\" / \"*.attestation.json\")))\n",
"if not ev:\n",
" print(\"no bundled evidence; fetch with `pacta log-fetch` to try live\")\n",
"else:\n",
" att = json.load(open(ev[0]))\n",
" rec = json.load(open(ev[0].replace(\".attestation.\", \".receipt.\")))\n",
" ok = verify_inclusion(\n",
" leaf_bytes_for_attestation(att),\n",
" rec[\"leaf_index\"], rec[\"tree_size\"],\n",
" [bytes.fromhex(h) for h in rec[\"inclusion_proof\"]],\n",
" bytes.fromhex(rec[\"sth\"][\"root_hash\"]),\n",
" )\n",
" print(f\"{att['subject']['component']}: inclusion recomputes ->\", ok)\n",
" print(\"The counterparty believed no adjective; it recomputed a Merkle root.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Corrupt a member, watch the pin catch it (executable)\n",
"\n",
"The wallet seals each member's SHA-256 into its capsule. The\n",
"next cell stages a COPY of a real member binary in a temp\n",
"directory, \"seals\" its hash the way the capsule does, appends\n",
"one byte (a supply-chain attack in miniature), and re-checks.\n",
"Nothing on your machine is modified.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import hashlib, shutil, tempfile, pathlib\n",
"from pacta.quorum import binary_path\n",
"\n",
"member = binary_path(\"dalek\")\n",
"if not member.exists():\n",
" print(\"quorum not built; run pacta wallet build-quorum first\")\n",
"else:\n",
" stage = pathlib.Path(tempfile.mkdtemp()) / member.name\n",
" shutil.copy2(member, stage)\n",
" sealed = hashlib.sha256(stage.read_bytes()).hexdigest() # capsule pin\n",
" print(\"sealed :\", sealed[:24], \"...\")\n",
" with stage.open(\"ab\") as f:\n",
" f.write(b\"\\x00\") # the attack\n",
" current = hashlib.sha256(stage.read_bytes()).hexdigest()\n",
" print(\"current:\", current[:24], \"...\")\n",
" if current != sealed:\n",
" print(\"PIN CAUGHT IT: wallet.quorum() would refuse to assemble ->\")\n",
" print(\" 'quorum member dalek binary hash changed since the capsule was sealed'\")\n",
" else:\n",
" print(\"impossible: SHA-256 collision\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"One appended byte and the wallet refuses to even *assemble* the\n",
"quorum - before any verification runs. Note what this control\n",
"is and is not: it stops binary substitution *between* wallet\n",
"sessions; an attacker with live root outranks it (see\n",
"docs/threat-model.md, attacker #7 - that is what the choir and\n",
"the airgap profiles are for).\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Exercises\n",
"\n",
"- Change `toy_quorum` to majority voting and write two sentences\n",
" on exactly which attack that lets through.\n",
"- Extend the corrupt-a-member cell: corrupt the capsule JSON\n",
" itself instead of the binary. What catches that, and when?\n",
" (Hint: nothing does until the ledger genesis is compared -\n",
" write down the exact trust statement the capsule hash in the\n",
" genesis entry provides.)\n",
"- The signing path is trusted base. Write the strongest *true*\n",
" sentence you can about warden's outbound safety, and the\n",
" strongest *false* one a marketer would write - and name the\n",
" word that makes the second one false.\n",
"- Design `warden-treasury`: which member re-verifies Solana\n",
" transactions, and what exactly the RPC provider is still\n",
" trusted for after you do.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The human surface: see this wallet through the cockpit\n",
"\n",
"Everything this notebook built programmatically has a read-only human console:\n",
"\n",
"```\n",
"pacta wallet cockpit --demo # throwaway demo wallet, zero setup\n",
"pacta wallet cockpit --wallet DIR # the wallet you just sealed here\n",
"```\n",
"\n",
"Open `/deck` for all six role stations live in parallel (the quorum bench you built is the\n",
"indigo pane; the ledger you hash-chained is re-verified on every page load), and `/manual`\n",
"for the lab-manual sessions that teach each role — Session 4's tamper drill breaks a *copy*\n",
"of a ledger exactly like this notebook's and watches two independent surfaces catch it."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
"nbformat": 4,
"nbformat_minor": 5
}

View file

@ -1,242 +1,229 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Lecture 11: The Customer's-Eye View — You Hold the Ruler\n",
"\n",
"Every earlier lecture looked at the log from the *operator's*\n",
"side: replaying proofs, signing attestations, building the\n",
"tree. This one flips the telescope. You are now a customer — an\n",
"agent, a wallet, a developer — who wants to trust one of the\n",
"four verified Ed25519 forks and has no theorem prover, no Lean,\n",
"no desire to spend hours re-checking anything. What actually\n",
"happens on your side? The answer reframes the whole system, and\n",
"it is the most intuitive on-ramp to it.\n"
]
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Lecture 11: The Customer's-Eye View \u2014 You Hold the Ruler\n",
"\n",
"Every earlier lecture looked at the log from the *operator's*\n",
"side: replaying proofs, signing attestations, building the\n",
"tree. This one flips the telescope. You are now a customer \u2014 an\n",
"agent, a wallet, a developer \u2014 who wants to trust one of the\n",
"four verified Ed25519 forks and has no theorem prover, no Lean,\n",
"no desire to spend hours re-checking anything. What actually\n",
"happens on your side? The answer reframes the whole system, and\n",
"it is the most intuitive on-ramp to it.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Learning Objectives\n",
"\n",
"- Separate the three roles that a verification touches:\n",
" *measurement* (operator), *publication* (the log), and\n",
" *judgment* (you) \u2014 and see that only the last is yours.\n",
"- Understand the \"allowed axioms\" list as a **requirements\n",
" card** you own, can read, and could write yourself.\n",
"- Explain why a self-written card meeting the supply *exactly*\n",
" is engineered, not coincidental.\n",
"- State the three honest outcomes when your card is stricter\n",
" than the supply: relax (itemized), walk away, or grow the\n",
" supply.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The card is a requirements card \u2014 and ideally you write it\n",
"\n",
"The list of axioms a certificate is *allowed* to rest on is not\n",
"handed to you by the operator at verification time. It ships\n",
"inside your own tooling (the ed25519 profile), on your disk,\n",
"versioned by you. A maximally paranoid customer ignores the\n",
"shipped copy and writes the card from first principles:\n",
"\n",
"> *\"I accept Lean's three foundational axioms, because that is\n",
"> what the proof kernel's logic IS. For the signature-tier\n",
"> theorems I accept named placeholders for SHA-512 and the wire\n",
"> format, because RFC 8032 tells me those parts exist and I can\n",
"> see they are declared, not smuggled. Nothing else.\"*\n",
"\n",
"That is a wish-list: the assumptions you are willing to live\n",
"under. Call it your ruler.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys, pathlib\n",
"for parent in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents]:\n",
" if (parent / \"src\" / \"pacta\").exists():\n",
" sys.path.insert(0, str(parent / \"src\")); break\n",
"\n",
"# A customer writes their OWN card, from first principles - no\n",
"# peeking at the operator. Lean's three, plus named oracle slots.\n",
"my_card = {\n",
" \"foundational\": {\"propext\", \"Classical.choice\", \"Quot.sound\"},\n",
" \"apex_oracle_allowed\": {\n",
" \"sha2.Sha512\", \"verifying.sha512_new\",\n",
" \"verifying.sha512_update\", \"verifying.sha512_finalize_bytes\",\n",
" \"ed25519.Signature\", \"ed25519.Signature.to_bytes\",\n",
" \"signature.error.Error\", \"signature.error.Error.new\",\n",
" },\n",
"}\n",
"print(\"my requirements card:\", sum(len(v) for v in my_card.values()), \"named assumptions\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Wish meets supply \u2014 and it is no accident\n",
"\n",
"Now compare your self-written card against what the four forks\n",
"actually declare. They match. That convergence is *engineered*:\n",
"the supply was deliberately shrunk \u2014 every axiom made to justify\n",
"its existence \u2014 so that any reasonable person's independently\n",
"written card lands on the same minimal list. When the wish meets\n",
"the supply exactly, it is because the supplier spent months\n",
"making the supply as small as honesty allows.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pacta.profiles.ed25519 import APEX_BOUNDARIES\n",
"\n",
"# The supply: the apex-tier certificate's documented, allowed\n",
"# axiom cone for the upstream fork - the exact set the strongest\n",
"# theorem is permitted to rest on, nothing more, nothing less.\n",
"supply = set(APEX_BOUNDARIES[\"dalek-wrappers\"])\n",
"wish = my_card[\"foundational\"] | my_card[\"apex_oracle_allowed\"]\n",
"print(\"supply == my wish:\", supply == wish)\n",
"print(\"axioms in supply my card did not anticipate:\", (supply - wish) or \"none\")\n",
"print(\"axioms my card wanted that are absent:\", (wish - supply) or \"none\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The three roles, kept apart\n",
"\n",
"It is tempting to say \"the inclusion proof proves the Lean proof\n",
"is about my card.\" It does not, and the precision matters:\n",
"\n",
"| role | who | what it establishes |\n",
"|---|---|---|\n",
"| measurement | operator's kernel run | \"this theorem rests on exactly these named axioms\" (recorded verbatim in the attestation) |\n",
"| publication | the transparency log | \"this measurement is in the permanent record, shown identically to everyone\" (the inclusion proof + signed head) |\n",
"| judgment | **you** | \"these observed axioms are inside my allowed card\" (re-derived locally, every time) |\n",
"\n",
"Only judgment is yours, and it is the only step that involves an\n",
"opinion. The operator is trusted to *copy down what the kernel\n",
"printed* \u2014 never to interpret it.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## If you are happy \u2014 you are already finished\n",
"\n",
"If your card covers the observed cone, verification is: check\n",
"one signature, walk ~4 hashes to the signed root, compare cones\n",
"to your card. Milliseconds, standard library, no Lean. Done.\n",
"\n",
"## If you are NOT happy \u2014 there is nothing to negotiate\n",
"\n",
"Suppose your card is stricter: *\"I require SHA-512 itself\n",
"proven, not an oracle.\"* The system has no sales pitch for you.\n",
"Its entire answer is an **itemized** statement of the gap:\n",
"*not in supply; SHA-512-proven is the R5 frontier; here is the\n",
"exact list of what you would be accepting if you proceed\n",
"anyway.* Your decision is never \"lower my standards\" in the\n",
"abstract \u2014 it is a named line item: *SHA-512 as oracle: yes/no.*\n",
"Informed compromise, not diffuse trust.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# The honest gap, itemized. A stricter customer wants no hash oracle.\n",
"strict_card = my_card[\"foundational\"] # foundational ONLY, no oracle slots\n",
"gap = supply - strict_card\n",
"print(\"this customer must explicitly accept, or walk away:\")\n",
"for ax in sorted(gap):\n",
" print(\" -\", ax)\n",
"print()\n",
"print(\"verdict for the strict card:\", \"PASSES\" if supply <= strict_card else \"REFUSED (gap above)\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The third option: grow the supply\n",
"\n",
"The gap between anyone's dream card and today's supply is a\n",
"to-do list, not a wall. Prove SHA-512, certify the wire\n",
"parsers, and the new certificates enter the same log \u2014 and\n",
"stricter cards start passing. The log is additive in exactly\n",
"the way requirements are.\n",
"\n",
"**So: you hold the ruler. If your ruler is stricter than our\n",
"supply, your ruler is our roadmap.** That is the quiet\n",
"invitation built into the whole design.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Exercises\n",
"\n",
"- Write your own requirements card for the *foundational*\n",
" (non-signature) certificates from scratch. Those certificates\n",
" are expected to carry exactly Lean's three standard axioms\n",
" (`propext`, `Classical.choice`, `Quot.sound`) and nothing\n",
" else - did your card guess exactly those three?\n",
"- A vendor's attestation says `status: proven`. Explain in two\n",
" sentences why your tooling ignores that field entirely, and\n",
" what it looks at instead.\n",
"- You require reproducible builds. Is that a card you can write\n",
" today and have pass? Name the tier that gap belongs to, and\n",
" what would have to enter the log to close it.\n",
"- Argue both sides: is \"wish meets supply exactly\" a strength\n",
" (minimal, auditable) or a weakness (the same author wrote the\n",
" card and the supply)? What single action by a customer\n",
" settles the argument in their favor?\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Learning Objectives\n",
"\n",
"- Separate the three roles that a verification touches:\n",
" *measurement* (operator), *publication* (the log), and\n",
" *judgment* (you) — and see that only the last is yours.\n",
"- Understand the \"allowed axioms\" list as a **requirements\n",
" card** you own, can read, and could write yourself.\n",
"- Explain why a self-written card meeting the supply *exactly*\n",
" is engineered, not coincidental.\n",
"- State the three honest outcomes when your card is stricter\n",
" than the supply: relax (itemized), walk away, or grow the\n",
" supply.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The card is a requirements card — and ideally you write it\n",
"\n",
"The list of axioms a certificate is *allowed* to rest on is not\n",
"handed to you by the operator at verification time. It ships\n",
"inside your own tooling (the ed25519 profile), on your disk,\n",
"versioned by you. A maximally paranoid customer ignores the\n",
"shipped copy and writes the card from first principles:\n",
"\n",
"> *\"I accept Lean's three foundational axioms, because that is\n",
"> what the proof kernel's logic IS. For the signature-tier\n",
"> theorems I accept named placeholders for SHA-512 and the wire\n",
"> format, because RFC 8032 tells me those parts exist and I can\n",
"> see they are declared, not smuggled. Nothing else.\"*\n",
"\n",
"That is a wish-list: the assumptions you are willing to live\n",
"under. Call it your ruler.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys, pathlib\n",
"for parent in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents]:\n",
" if (parent / \"src\" / \"pacta\").exists():\n",
" sys.path.insert(0, str(parent / \"src\")); break\n",
"\n",
"# A customer writes their OWN card, from first principles - no\n",
"# peeking at the operator. Lean's three, plus named oracle slots.\n",
"my_card = {\n",
" \"foundational\": {\"propext\", \"Classical.choice\", \"Quot.sound\"},\n",
" \"apex_oracle_allowed\": {\n",
" \"sha2.Sha512\", \"verifying.sha512_new\",\n",
" \"verifying.sha512_update\", \"verifying.sha512_finalize_bytes\",\n",
" \"ed25519.Signature\", \"ed25519.Signature.to_bytes\",\n",
" \"signature.error.Error\", \"signature.error.Error.new\",\n",
" },\n",
"}\n",
"print(\"my requirements card:\", sum(len(v) for v in my_card.values()), \"named assumptions\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Wish meets supply — and it is no accident\n",
"\n",
"Now compare your self-written card against what the four forks\n",
"actually declare. They match. That convergence is *engineered*:\n",
"the supply was deliberately shrunk — every axiom made to justify\n",
"its existence — so that any reasonable person's independently\n",
"written card lands on the same minimal list. When the wish meets\n",
"the supply exactly, it is because the supplier spent months\n",
"making the supply as small as honesty allows.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pacta.profiles.ed25519 import APEX_BOUNDARIES\n",
"\n",
"# The supply: the apex-tier certificate's documented, allowed\n",
"# axiom cone for the upstream fork - the exact set the strongest\n",
"# theorem is permitted to rest on, nothing more, nothing less.\n",
"supply = set(APEX_BOUNDARIES[\"dalek-wrappers\"])\n",
"wish = my_card[\"foundational\"] | my_card[\"apex_oracle_allowed\"]\n",
"print(\"supply == my wish:\", supply == wish)\n",
"print(\"axioms in supply my card did not anticipate:\", (supply - wish) or \"none\")\n",
"print(\"axioms my card wanted that are absent:\", (wish - supply) or \"none\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The three roles, kept apart\n",
"\n",
"It is tempting to say \"the inclusion proof proves the Lean proof\n",
"is about my card.\" It does not, and the precision matters:\n",
"\n",
"| role | who | what it establishes |\n",
"|---|---|---|\n",
"| measurement | operator's kernel run | \"this theorem rests on exactly these named axioms\" (recorded verbatim in the attestation) |\n",
"| publication | the transparency log | \"this measurement is in the permanent record, shown identically to everyone\" (the inclusion proof + signed head) |\n",
"| judgment | **you** | \"these observed axioms are inside my allowed card\" (re-derived locally, every time) |\n",
"\n",
"Only judgment is yours, and it is the only step that involves an\n",
"opinion. The operator is trusted to *copy down what the kernel\n",
"printed* — never to interpret it.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## If you are happy — you are already finished\n",
"\n",
"If your card covers the observed cone, verification is: check\n",
"one signature, walk ~4 hashes to the signed root, compare cones\n",
"to your card. Milliseconds, standard library, no Lean. Done.\n",
"\n",
"## If you are NOT happy — there is nothing to negotiate\n",
"\n",
"Suppose your card is stricter: *\"I require SHA-512 itself\n",
"proven, not an oracle.\"* The system has no sales pitch for you.\n",
"Its entire answer is an **itemized** statement of the gap:\n",
"*not in supply; SHA-512-proven is the R5 frontier; here is the\n",
"exact list of what you would be accepting if you proceed\n",
"anyway.* Your decision is never \"lower my standards\" in the\n",
"abstract — it is a named line item: *SHA-512 as oracle: yes/no.*\n",
"Informed compromise, not diffuse trust.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# The honest gap, itemized. A stricter customer wants no hash oracle.\n",
"strict_card = my_card[\"foundational\"] # foundational ONLY, no oracle slots\n",
"gap = supply - strict_card\n",
"print(\"this customer must explicitly accept, or walk away:\")\n",
"for ax in sorted(gap):\n",
" print(\" -\", ax)\n",
"print()\n",
"print(\"verdict for the strict card:\", \"PASSES\" if supply <= strict_card else \"REFUSED (gap above)\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The third option: grow the supply\n",
"\n",
"The gap between anyone's dream card and today's supply is a\n",
"to-do list, not a wall. Prove SHA-512, certify the wire\n",
"parsers, and the new certificates enter the same log — and\n",
"stricter cards start passing. The log is additive in exactly\n",
"the way requirements are.\n",
"\n",
"**So: you hold the ruler. If your ruler is stricter than our\n",
"supply, your ruler is our roadmap.** That is the quiet\n",
"invitation built into the whole design.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Exercises\n",
"\n",
"- Write your own requirements card for the *foundational*\n",
" (non-signature) certificates from scratch. Those certificates\n",
" are expected to carry exactly Lean's three standard axioms\n",
" (`propext`, `Classical.choice`, `Quot.sound`) and nothing\n",
" else - did your card guess exactly those three?\n",
"- A vendor's attestation says `status: proven`. Explain in two\n",
" sentences why your tooling ignores that field entirely, and\n",
" what it looks at instead.\n",
"- You require reproducible builds. Is that a card you can write\n",
" today and have pass? Name the tier that gap belongs to, and\n",
" what would have to enter the log to close it.\n",
"- Argue both sides: is \"wish meets supply exactly\" a strength\n",
" (minimal, auditable) or a weakness (the same author wrote the\n",
" card and the supply)? What single action by a customer\n",
" settles the argument in their favor?\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Do it in the cockpit: the Inspect instrument\n",
"\n",
"The recomputation this notebook performs by hand is exactly what the cockpit's **Inspect**\n",
"instrument (`/inspect`) runs on paste — same deployed verifier, verbatim diagnostics —\n",
"and what the lab manual's Cryptographer session drills: verify the sample evidence, then\n",
"deliberately corrupt one character and learn the distinct failure surfaces.\n",
"`pacta wallet cockpit --demo`, then Inspect → “Load the sample evidence”."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
"nbformat": 4,
"nbformat_minor": 5
}

1
paper/.gitignore vendored
View file

@ -2,3 +2,4 @@
*.log
*.out
*.toc
rendered-pages/

31
paper/README.md Normal file
View file

@ -0,0 +1,31 @@
# Which file is current?
**`ltl.pdf` / `ltl.tex` — the current paper (v0.15, revised August 2026).**
The review process concluded in August 2026. v0.10 folded in the
corrections queued during the freeze (the closed consistency-verifier
divergence with its `sn = 0` root cause, replay-harness-integrity
limitation, claim-matrix row); v0.11 brought the paper up to the live
system: the nineteen-leaf dual-signed deployment, the SLH-DSA (FIPS 205)
verify-path instantiation, and its certificate appendix; v0.12 unified
entry numbering on 0-based leaf indices; v0.13 is the approachability
revision from an external-persona referee pass (house terms defined at
first use, theorem statements carry their own scoping, notation
collisions resolved, FIPS 205 reference added); v0.14 completes that pass after a
full-document eye inspection of the published PDF (notation-table rows
for C and b, HIST chain length renamed to avoid the split-point k); v0.15 names the
two-sided dilemma explicitly in the introduction and aligns the
introduction's question with the abstract (label, not verdict). The
version submitted for review (July 17, 2026, sha256 `7f140356…`) is
preserved unchanged in this repository's git history. The live copy at
<https://ltl.zkdefi.org/paper> serves the current revision; the v0.15
revision is archived with DOI
[10.5281/zenodo.22057482](https://doi.org/10.5281/zenodo.22057482)
(CC BY 4.0).
Superseded early drafts (v0.1, v0.2) were removed from the working tree
on 2026-08-15; like every prior state of this folder, they remain
retrievable from this repository's git history.
Revisions v0.3v0.9 were successive overwrites of `ltl.tex`; they live in
this repository's git history. v0.9 is the version that was submitted
for review; v0.10 through v0.15 supersede it (history above).

84
paper/check-paper.sh Executable file
View file

@ -0,0 +1,84 @@
#!/usr/bin/env bash
# check-paper.sh — the paper's form gate.
#
# Ports the book's check-book.sh lesson to the paper: the 2026-08-16
# socratic round found a ghost page (a fossil \clearpage) and a solid-set
# claim matrix whose badness-10000 warnings had printed in EVERY build,
# unread. This gate makes both classes of defect fail the build instead
# of shipping silently. It cannot replace the render-and-look eye pass —
# it renders the pages so the eye pass has no excuse.
#
# Usage: ./check-paper.sh build + all gates + render pages
# ./check-paper.sh --selftest exercise the gate parsers on
# known-bad and known-good log lines
set -euo pipefail
cd "$(dirname "$0")"
OVERFULL_LIMIT_PT=10
MIN_PAGE_CHARS=300 # calibrated 2026-08-16: real minimum was 922 (claim-matrix page)
PAGES_DIR=rendered-pages
fail() { echo "FAIL: $*" >&2; exit 1; }
# --- gate parsers (pure text -> verdict; selftestable) -------------------
overfull_violations() { # stdin: build log -> lines exceeding the limit
grep -i 'Overfull \\hbox' | grep -oP '\(\K[0-9.]+(?=pt too wide)' \
| awk -v lim="$OVERFULL_LIMIT_PT" '$1 > lim' || true
}
badness_violations() { # stdin: build log -> badness-10000 underfull lines
grep -i 'Underfull \\hbox (badness 10000)' || true
}
if [[ "${1:-}" == "--selftest" ]]; then
n=0
t() { n=$((n+1)); [[ "$2" == "$3" ]] && echo "selftest $n ok: $1" || fail "selftest $n: $1 (got '$3', want '$2')"; }
t "80pt overfull trips" "80.05" \
"$(echo 'warning: x.tex:1: Overfull \hbox (80.05pt too wide) in paragraph' | overfull_violations)"
t "3.4pt overfull passes" "" \
"$(echo 'warning: x.tex:1: Overfull \hbox (3.374pt too wide) in paragraph' | overfull_violations)"
t "badness 10000 trips" "1" \
"$(echo 'warning: x.tex:1: Underfull \hbox (badness 10000) in paragraph' | badness_violations | wc -l)"
t "badness 2913 passes" "0" \
"$(echo 'warning: x.tex:1: Underfull \hbox (badness 2913) in paragraph' | badness_violations | wc -l)"
echo "selftest: $n/$n ok"; exit 0
fi
# --- 1. build ------------------------------------------------------------
LOG=$(mktemp); trap 'rm -f "$LOG"' EXIT
tectonic ltl.tex 2>&1 | tee "$LOG" >/dev/null
grep -qi '^error' "$LOG" && fail "TeX errors in build log"
# --- 2. overfull gate ----------------------------------------------------
OV=$(overfull_violations <"$LOG")
[[ -z "$OV" ]] || fail "overfull hbox beyond ${OVERFULL_LIMIT_PT}pt: $OV"
# --- 3. loose-typesetting gate (the ignored-warnings class) --------------
BAD=$(badness_violations <"$LOG" | wc -l)
[[ "$BAD" -eq 0 ]] || fail "$BAD underfull badness-10000 lines (gappy table/paragraph)"
# --- 4. ghost-page gate (the fossil-clearpage class) ---------------------
NPAGES=$(pdfinfo ltl.pdf | awk '/^Pages:/{print $2}')
for p in $(seq 1 $((NPAGES-1))); do
chars=$(pdftotext -f "$p" -l "$p" ltl.pdf - 2>/dev/null | tr -d '[:space:]' | wc -c)
[[ "$chars" -ge "$MIN_PAGE_CHARS" ]] || fail "page $p is mostly blank ($chars chars) — ghost page"
done
# --- 5. content probes ---------------------------------------------------
VERSION=$(grep -oP '\\date\{[^}]*---\s*\Kv[0-9.]+' ltl.tex || true)
[[ -n "$VERSION" ]] || fail "cannot extract version from \\date{...} in ltl.tex"
pdftotext -f 1 -l 1 ltl.pdf - | grep -q "$VERSION" || fail "title page does not carry $VERSION"
! pdftotext ltl.pdf - | grep -q '??' || fail "unresolved ?? reference in PDF"
# --- 5b. site claim binding: the webdocs paper card's page count ---------
WEBDOCS=../provider/src/pacta_provider/webdocs.py
if [[ -f "$WEBDOCS" ]]; then
STATED=$(grep -oP '\(PDF, \K[0-9]+(?= pages)' "$WEBDOCS" || true)
[[ -n "$STATED" ]] || fail "webdocs paper card lost its '(PDF, N pages' claim"
[[ "$STATED" -eq "$NPAGES" ]] || fail "webdocs says $STATED pages, PDF has $NPAGES"
fi
# --- 6. render for the mandatory eye pass --------------------------------
rm -rf "$PAGES_DIR"; mkdir -p "$PAGES_DIR"
pdftoppm -png -r 110 ltl.pdf "$PAGES_DIR/p"
echo "OK: $VERSION, $NPAGES pages, no overfull>${OVERFULL_LIMIT_PT}pt, no badness-10000, no ghost pages, no ?? refs."
echo "NOW LOOK: the render-and-look law is not automated. Flip every page in $PAGES_DIR/."

Binary file not shown.

View file

@ -1,481 +0,0 @@
\documentclass[10pt,twocolumn]{article}
\usepackage[margin=1.7cm,bottom=1.95cm]{geometry}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\usepackage{microtype}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{booktabs}
\usepackage{tikz}
\usetikzlibrary{arrows.meta}
\usepackage[hidelinks]{hyperref}
\usepackage{xcolor}
\newcommand{\code}[1]{\texttt{\small #1}}
% poor-man's semantic brackets (stmaryrd unavailable in minimal TeX trees)
\newcommand{\den}[1]{[\![#1]\!]}
\setlength{\columnsep}{0.8cm}
\title{\textbf{LTL: Lean Transparency Log}\\[2pt]
\large Where Deductive and Cryptographic Proof Vouch for Each Other}
\author{Olaf Horvath\\ \small \texttt{Olaf.Horvath@zkdefi.org} --- \url{ltl.zkdefi.org} --- \href{https://orcid.org/0009-0004-8008-5805}{ORCID 0009-0004-8008-5805}}
\date{July 2026}
\begin{document}
\maketitle
\begin{abstract}
Interactive theorem provers can certify the functional correctness of
deployed cryptographic code, but the resulting assurance is expensive to
\emph{consume}: re-checking a realistic proof corpus takes a proof
toolchain and hours of kernel time, which excludes almost every
downstream user, including autonomous agents that must decide which
cryptographic library to trust. We describe the \emph{Lean Transparency
Log} (LTL)\footnote{Not to be confused with linear temporal
logic~\cite{pnueli77}; the collision is acknowledged and, in a paper
about verification, difficult to resist.}, a deployed RFC~9162-style
transparency log whose leaves are \emph{replay attestations}: signed
statements that the Lean~4 proofs of a specific Rust repository, at a
specific git commit, re-check with exactly their documented axiom sets.
Consumers verify one signature and a logarithmic inclusion proof in
milliseconds; the hours of kernel time are paid once, by the log
operator. Three design decisions distinguish the LTL from prior
attestation transparency systems: (i)~consumers re-derive verification
verdicts locally from the \emph{observed axiom cones} carried in each
attestation, so the operator is trusted for observations, never for
verdicts; (ii)~axiom cones are matched against per-theorem
\emph{documented boundaries} exactly, in both directions; and (iii)~the
log's tree heads are signed by a binary built from the very Ed25519
implementation whose correctness certificates are leaves of the log, and
each signature embeds the operator's own Merkle self-check of that leaf.
We report a small production deployment covering four verified
production Ed25519 implementations, describe what the accumulated
evidence does and does not establish, and outline the natural next step:
verifying the log's own proof-checking algorithms in Lean and entering
those certificates into the log they defend.
\end{abstract}
\section{Introduction}
\label{sec:intro}
Formal verification of deployed cryptographic code has matured from
research prototypes to substantial artifacts: verified-by-construction
libraries such as HACL*~\cite{hacl} and Fiat-Crypto~\cite{fiatcrypto}
ship in mainstream software, and post-hoc verification pipelines such as
Aeneas~\cite{aeneas} make it possible to state and prove theorems about
\emph{existing} production Rust code. The corpus underlying this paper
is of the latter kind: four production Ed25519
implementations---upstream \code{curve25519-dalek}/\code{ed25519-dalek}
and three deployed forks (Solana, RISC~Zero, Betrusted)---each carry
Lean~4~\cite{lean4} certificates, proven against that fork's own
extracted model, covering field arithmetic over $\mathbb{F}_{2^{255}-19}$,
the complete twisted Edwards laws~\cite{bernsteinlange,twisted}, scalar
arithmetic mod $\ell$, encoding/decoding with constructive point
decompression, and a four-tier characterization of signature
verification~\cite{ed25519,rfc8032} whose strongest tier states: the
extracted verifier accepts iff the signature's $R$ component decompresses
to a valid curve point equal to $[k](-A)+[s]B$.
\paragraph{What the certificates state.} Concretely, the corpus is a
stack of theorems about extracted code, each stated through a
\emph{denotation} from machine representation to mathematics. Field
elements are five 51-bit limbs denoting
$\den{(a_0,\dots,a_4)}=\sum_i a_i2^{51i}\bmod p$, and every
operation carries a two-clause specification---the value is right
\emph{and} the representation invariant is preserved, e.g.
\begin{align*}
\footnotesize
\forall a\,b.\ &\mathrm{bnd}\,a \Rightarrow \mathrm{bnd}\,b \Rightarrow{}\\[-2pt]
&\exists c.\ \code{mul}\,a\,b = \code{ok}\,c \,\wedge\, \mathrm{bnd}\,c
\,\wedge\, \den{c} = \den{a}\cdot\den{b}.
\end{align*}
Point operations are proven to implement the complete twisted Edwards
addition law on
$E : -x^2+y^2 = 1+d\,x^2y^2$ over $\mathbb{F}_p$,
\begin{equation*}
\small
(x_1,y_1)+(x_2,y_2)=
\Bigl(\tfrac{x_1y_2+x_2y_1}{1+d\,x_1x_2y_1y_2},\;
\tfrac{y_1y_2+x_1x_2}{1-d\,x_1x_2y_1y_2}\Bigr),
\end{equation*}
including the completeness fact that makes it branch-free ($a=-1$ is a
square and $d$ a non-square in $\mathbb{F}_p$, so the denominators never
vanish~\cite{bernsteinlange}).
At the apex, writing $\code{accept}(A,m,R,s)$ for ``the extracted
verifier returns \code{ok}'', with $k$ the scalar produced by the hash
oracle $H(R,A,m)$ and no properties assumed of $H$, the byte-level tier
states
\begin{equation*}
\footnotesize
\code{accept}(A,m,R,s) \;\Leftrightarrow\; \code{compress}([s]B-[k]A) = R,
\end{equation*}
and the strongest tier lifts byte equality to the group:
\begin{equation*}
\footnotesize
\code{accept}(A,m,R,s) \;\Leftrightarrow\; \code{decompress}(R) = [k](-A)+[s]B,
\end{equation*}
with decompression itself proven (exact byte parsing, the
$(p{+}3)/8$-power square root, sign-bit root selection). Each theorem's
axiom cone is pinned exactly---the standard three below the apex, the
enumerated oracle boundary at the apex tiers.
The economics of \emph{consuming} such evidence are poor: re-checking
one fork's certificates takes $\approx$30 minutes of Lean kernel time
and a pinned toolchain. A wallet, a package manager, or an autonomous
agent choosing a cryptographic backend cannot pay this per
decision---and need not: a deterministic re-check yields a fact that can
be attested once and distributed.
This is the classic transparency-log trade---Certificate
Transparency~\cite{rfc6962,rfc9162} for certificate issuance, Sigstore's
Rekor~\cite{sigstore} for signing events and supply-chain
attestations~\cite{intoto}, key transparency~\cite{coniks}, checksum
databases---applied to a payload with different trust semantics:
\emph{evidence of machine-checked mathematical truth, together with its
exact assumption set}. We claim no novelty for any component (the hash
structure and proof algorithms are RFC~9162 verbatim); the contribution
is the composition, its trust model, and a deployed, reproducible
instance. The LTL does not mechanize cryptographic security
proofs---that inter-disciplinary bridge is already being built by
EasyCrypt and its relatives~\cite{easycrypt}. It bridges the adjacent,
mostly empty gap: type-theory-certified artifacts have no distribution
infrastructure, and cryptographic transparency infrastructure has never
carried kernel-checked mathematics---each discipline solving the
other's problem.
\S\ref{sec:log} describes the log and its trust model,
\S\ref{sec:dogfood} the self-referential signing loop,
\S\ref{sec:deploy} the deployment and what its evidence establishes,
and \S\ref{sec:next} the verification agenda for the log itself.
\section{The Lean Transparency Log}
\label{sec:log}
\begin{figure*}[t]
\centering
\begin{tikzpicture}[every node/.style={font=\small}]
\tikzset{box/.style={draw, rounded corners=2pt, minimum width=1.6cm, minimum height=0.56cm}}
% leaves: 0-3 failed run (gray), 4-7 verified (black)
\foreach \i/\c in {0/gray, 1/gray, 2/gray, 3/gray, 4/black, 5/black, 6/black, 7/black}
\node[box, draw=\c, text=\c] (l\i) at (\i*2.05,0) {leaf \i};
\node[box, draw=black, very thick, fill=black!6] at (4*2.05,0) {\textbf{leaf 4}};
\node[text=gray, font=\footnotesize] at (3*2.05/2 - 1.02,-0.62) {failed-audit run --- retained forever};
\node[font=\footnotesize] at (5.5*2.05 + 0.35,-0.62) {verified attestations (16/16, boundary-exact)};
% internal nodes
\foreach \i in {0,...,3}
\node[box] (m\i) at (\i*4.1+1.025,1.25) {node};
\foreach \i in {0,1}
\node[box] (n\i) at (\i*8.2+3.075,2.5) {node};
\node[box, thick] (root) at (7.175,3.75) {root};
\foreach \a/\b in {l0/m0, l1/m0, l2/m1, l3/m1, l4/m2, l5/m2, l6/m3, l7/m3,
m0/n0, m1/n0, m2/n1, m3/n1, n0/root, n1/root}
\draw[gray!70] (\a) -- (\b);
% signature head
\node[box, thick, minimum width=5.2cm] (sig) at (7.175,4.95)
{signed tree head: $\mathrm{Ed25519}(\mathrm{root})$};
\draw[-{Stealth}] (root) -- (sig);
% the self-certification loop, routed through clear space on the right
\draw[-{Stealth}, thick, dashed]
(sig.east) .. controls (13.2,4.95) and (12.4,1.6) .. (l4.north east);
\node[align=left, font=\footnotesize, anchor=west] at (12.45,3.1)
{signed by the binary built\\ from the source attested at\\ leaf 4; the operator verifies\\ that leaf's inclusion first};
\end{tikzpicture}
\caption{The deployed 8-leaf log. Leaves 0--3 record an early replay whose
audit step failed (two tooling defects, since fixed); an append-only ledger
retains them. The tree head is signed by a binary built from the
implementation attested at leaf 4, and each signature embeds the operator's
own inclusion check of that leaf against the tree being signed.}
\label{fig:loop}
\end{figure*}
\paragraph{Roles.} The system has exactly two roles with deliberately
asymmetric costs and capabilities. The \emph{operator} (one per log)
owns a Lean toolchain, replays proof corpora, holds the log's signing
key, and bears append-only obligations. \emph{Consumers} (unbounded)
hold the operator's public key, receive small evidence files, and verify
with $\approx$25 lines of standard-library code. Nothing a consumer does
requires a theorem prover.
\paragraph{Leaves: replay attestations.} A leaf is the canonical JSON
serialization of an attestation recording: the subject repository URL
and \emph{git commit} (which cryptographically pins the entire source
tree); the toolchain versions; the resource-control regime under which
the replay ran; and, per certificate, its name, replay status, and the
\emph{observed axiom cone}---the exact output of Lean's
\code{\#print axioms} for that theorem. For the corpus above each
attestation carries 16 certificates.
\paragraph{Boundary-exact auditing.} Every certificate has a
\emph{documented} allowed axiom set. Foundational certificates must
carry exactly Lean's three standard axioms
(\code{propext}, \code{Classical.choice}, \code{Quot.sound}); the four
signature-tier certificates additionally carry a per-fork, explicitly
enumerated boundary (an opaque SHA-512 oracle and opaque wire-format
types---e.g., eleven axioms in total for the upstream fork). A cone is
\emph{clean} iff it equals its allowed set; deviation in either
direction---an unexpected axiom, or a missing boundary axiom---is
flagged. Each source repository enforces the same discipline in its own
check scripts; the log mirrors those sets and treats the repositories'
scripts as authoritative.
\paragraph{Observation, not verdict.} Attestations include the
operator's pass/fail judgment, but consumers ignore it: the consumer's
tooling re-derives every verdict locally by comparing the
\emph{observed} cone against the consumer's own copy of the allowed
sets. An operator that labels a dirty cone ``clean'' gains nothing; an
attestation that omits observed cones is treated as unverifiable. This
narrows the trust placed in the operator to: ``the reported
\code{\#print axioms} output is what the kernel printed for this commit.''
\paragraph{Tree, heads, receipts.} Leaves are accumulated in an
RFC~9162 Merkle tree ($\mathrm{SHA}$-256 with $0x00/0x01$
leaf/node domain separation). The operator signs tree heads; consumers
receive a \emph{receipt}: leaf index, sibling path, and the signed head.
Consumers additionally maintain a local pin store: same-size heads must
match the pinned root exactly (a mismatch is reported as equivocation
and is unrecoverable); growth requires a verified consistency proof from
the pinned size; shrinkage is rejected. A freshness policy bounds head
age. The full log is also published as a git repository (one file per
leaf, plus every head ever signed), so any cloner can recompute every
prefix root from the public leaves and audit the complete head history
without consistency proofs---a low-infrastructure witness
mechanism~\cite{rfc9162}; a standalone $\approx$150-line
standard-library verifier ships in the mirror.
\section{The self-certifying signature}
\label{sec:dogfood}
Tree heads are Ed25519 signatures, and this creates an opportunity for
coherence: the log \emph{contains} correctness certificates for an
Ed25519 implementation. The LTL's heads are therefore signed by a binary
built from the pinned source tree of exactly the implementation attested
in the log (serial backend pinned, matching the verified extraction),
and---before signing---the operator runs the same Merkle inclusion
verification a consumer runs, on the newest leaf attesting the signing
implementation, against the tree about to be signed. The verdict is
embedded in the signature block:
{\small
\begin{verbatim}
signing_backend: verified-dalek-serial
signing_library_source_commit: aa0f6ab...
signing_library_leaf_index: 4
signing_library_certificates_proven: 16/16
self_inclusion: verified
\end{verbatim}}
\noindent The signature vouches for the tree; the tree vouches for the
code that produced the signature; and the two vouchings are different
proof modalities (cryptographic and deductive), so the loop is
self-referential without being circular
(Fig.~\ref{fig:loop}). We state the honest extent of this claim
precisely: the Lean certificates cover the \emph{verification} path of
the library (the theorems' subject is the extraction image of that
path); the \emph{signing} path is not covered by any certificate and is
declared trusted base---the design merely ensures the trusted signing
code is the attested artifact rather than an unrelated third
implementation, and that consumers can check as much. Signature
\emph{verification} on consumer machines can optionally run through the
same certified-source binary, with the backend that actually ran
recorded in every result and a fail-closed policy flag available.
First-append bootstrapping is handled honestly: heads signed before the
signing library's attestation enters the log record
\code{self\_inclusion:\ library\_not\_in\_log}.
\section{Deployment and evidence}
\label{sec:deploy}
The LTL is deployed\footnote{Service:
\url{https://ltl.zkdefi.org} (read-only HTTP API and
documentation). Mirror: \url{https://github.com/saymrwulf/lean-transparency-log}.
Operator/consumer tooling and a twelve-lecture course:
\url{https://github.com/saymrwulf/proof-aware-crypto-tooling-agent}. The
underlying proof corpora are in the \code{saymrwulf/*-ed25519-verified}
repositories; every claim in this paper is re-checkable from these
artifacts.} with eight leaves: one attestation per fork from each of two
full replay runs (58--64 Lean files and $\approx$1{,}800\,s per
fork, under hard memory caps and core pinning). In the second run all
four forks reported 16/16 certificates proven with boundary-exact cones,
pinned to exact commits. The first run is deliberately still in the log: its audit step
failed due to two defects in the \emph{operator tooling} (a path issue
and a parser that mishandled Lean's line-wrapped axiom lists for the
eleven-axiom cones), and the operator signed attestations recording the
failure rather than suppressing the run. We consider the resulting
ledger---four failure leaves permanently beside four success
leaves---a feature of the trust model, and note that both defects were
fail-closed: valid proofs were rejected; invalid ones were never
accepted.
\paragraph{What a verified receipt establishes.} Under the assumptions
enumerated below, a consumer who verifies a receipt knows: \emph{the
operator whose key I pinned attests that the Lean certificates of
repository $X$ at commit $Y$ re-check, with per-certificate observed
axiom cones as included---and this statement is part of the log
presented to every other consumer}. Combined with local verdict
re-derivation, this yields source-level assurance for the pinned
commit. It deliberately does \emph{not} establish: correctness of any
binary (consumers build from the pinned source; compilers are trusted
base), correctness of SHA-512 (an opaque oracle in the theorems),
correctness of the wire-format parsers (their outcomes are hypotheses of
the signature tiers), signing-side correctness, or side-channel
properties. The assumption set, in full: the Lean kernel and its three
axioms plus mathlib; faithfulness of the Charon/Aeneas
extraction~\cite{aeneas}; each fork's documented oracle boundary;
operator key custody and the trust-on-first-use key distribution
(mitigated by publishing the key in two independent locations);
collision resistance of SHA-256 for the log; unforgeability of Ed25519
for the heads; and the consumer's own $\approx$25-line verifier.
\paragraph{An observational by-product: proof portability.} Because the
four corpora prove the same theorems against four independent
extractions, the diff between proof files measures how portable proofs
are across real forks. Pure-mathematics files (e.g., a carry-telescope
lemma file) are byte-identical across all four; extraction-facing proof
scripts diverge sharply where the forks' code or the extractor's naming
differs (e.g., 215 changed lines for the byte-parser proofs on the two
forks whose extraction produces a closure-based loader; 121 lines for
the signature-glue proofs on the same-crate fork; 27 lines---all
annotation---between the two structurally closest forks, documenting the
one fork's \code{black\_box} optimization barrier). Per-target
verification, in other words, is doing measurable work exactly where the
targets actually differ.
\section{Related work}
\label{sec:related}
Certificate Transparency~\cite{rfc6962,rfc9162} supplies the data
structure and proof algorithms, used here unchanged. Rekor within
Sigstore~\cite{sigstore} is the closest deployed system: a transparency
log over signing events and supply-chain attestations such as
in-toto~\cite{intoto} link metadata; its payloads attest \emph{process}
(who signed, how an artifact was built), whereas LTL leaves attest
kernel-checked \emph{mathematical statements together with their
assumption sets}, and the consumer re-derives verdicts rather than
trusting labels. Key transparency~\cite{coniks} and checksum databases
share the pattern with different payloads. Proof-carrying
code~\cite{pcc} ships proofs to consumers who check them; the LTL serves
consumers who cannot run any checker, replacing proof transport with
attestation, inclusion, and signature---at the cost of trusting the
operator's kernel run, a cost the design minimizes but does not
eliminate. Cheval, Moreira and Ryan formally verify transparency
protocols themselves~\cite{cheval}; our direction is the complement
(we log the verification), and \S\ref{sec:next} proposes meeting in the
middle. Verified Merkle tree \emph{implementations} exist, notably in
EverCrypt~\cite{evercrypt}; \S\ref{sec:next} builds on that precedent
rather than claiming it.
\section{Limitations and next steps}
\label{sec:next}
\paragraph{Limitations.} The deployment is small (one operator, eight
leaves, four subject repositories) and the operator is a single party;
split-view defense currently rests on consumer-side pinning plus the
public git mirror rather than an independent witness network. Key
distribution is trust-on-first-use. The signing path of the dogfood
binary is unverified (declared, not proven). The corpus itself stops at
source-level assurance---reproducible builds and side-channel evidence
remain open---and ML-DSA slots in the head format are deliberately
recorded as unavailable rather than backed by an unverified
implementation.
\paragraph{Verifying the accumulator itself.} The natural next step
applies the corpus's own discipline to the log's cryptographic half,
which is currently the \emph{unproven} half of the composition. All of
the following are tractable Lean targets: (i)~completeness of the
RFC~9162 inclusion verifier (honest proofs verify---no assumptions);
(ii)~\emph{soundness as an explicit reduction}: an accepting inclusion
proof for a leaf outside the tree yields a SHA-256 collision, making
collision resistance a documented boundary axiom audited exactly like
the SHA-512 oracle in the Ed25519 tiers; (iii)~the analogous statement
for the consistency verifier (acceptance implies prefix, modulo
collisions); (iv)~domain separation as a lemma; and (v)~total
correctness of the consumer's pin-store state machine (equivocation
detection, rollback rejection). Verified Merkle implementations in
F*~\cite{evercrypt} and algorithm verifications in other systems show
these proofs are well within reach; the LTL-specific closure is
\emph{where the certificates go}: into the log they defend, checked by
the certified checker they specify, alongside a consumer policy flag
requiring the certified verifier. At that point both proving traditions
in the composition run on certified code, and the remaining trusted base
is two hash assumptions, a compiler, an extraction pipeline, and one
key.
\paragraph*{Acknowledgments.} The proof corpora, tooling, deployment,
and this paper were produced with substantial assistance from Claude
(Anthropic). All claims are enforced by the referenced check scripts and
are independently re-checkable from the public artifacts.
\begin{thebibliography}{19}\scriptsize
\setlength{\itemsep}{1pt}
\bibitem{rfc6962} B.~Laurie, A.~Langley, E.~K\"asper.
\emph{Certificate Transparency}. RFC 6962, 2013.
\bibitem{rfc9162} B.~Laurie, E.~Messeri, R.~Stradling.
\emph{Certificate Transparency Version 2.0}. RFC 9162, 2021.
\bibitem{sigstore} Z.~Newman, J.~S.~Meyers, S.~Torres-Arias.
Sigstore: Software Signing for Everybody.
\emph{ACM CCS}, pp.~2353--2367, 2022.
\bibitem{intoto} S.~Torres-Arias, H.~Afzali, T.~K.~Kuppusamy,
R.~Curtmola, J.~Cappos. in-toto: Providing farm-to-table guarantees for
bits and bytes. \emph{USENIX Security}, 2019.
\bibitem{coniks} M.~S.~Melara, A.~Blankstein, J.~Bonneau,
E.~W.~Felten, M.~J.~Freedman. CONIKS: Bringing Key Transparency to End
Users. \emph{USENIX Security}, 2015.
\bibitem{pcc} G.~C.~Necula. Proof-Carrying Code.
\emph{ACM POPL}, pp.~106--119, 1997.
\bibitem{cheval} V.~Cheval, J.~Moreira, M.~Ryan.
Automatic verification of transparency protocols.
\emph{IEEE EuroS\&P}, 2023. arXiv:2303.04500.
\bibitem{easycrypt} G.~Barthe, B.~Gr\'egoire, S.~Heraud,
S.~Zanella~B\'eguelin. Computer-Aided Security Proofs for the Working
Cryptographer. \emph{CRYPTO}, LNCS 6841, pp.~71--90, 2011.
\bibitem{aeneas} S.~Ho, J.~Protzenko.
Aeneas: Rust verification by functional translation.
\emph{Proc.\ ACM Program.\ Lang.} 6 (ICFP): 711--741, 2022.
\bibitem{lean4} L.~de~Moura, S.~Ullrich.
The Lean 4 Theorem Prover and Programming Language.
\emph{CADE-28}, LNCS 12699, pp.~625--635, 2021.
\bibitem{hacl} J.-K.~Zinzindohou\'e, K.~Bhargavan, J.~Protzenko,
B.~Beurdouche. HACL*: A Verified Modern Cryptographic Library.
\emph{ACM CCS}, 2017.
\bibitem{evercrypt} J.~Protzenko et al.
EverCrypt: A Fast, Verified, Cross-Platform Cryptographic Provider.
\emph{IEEE S\&P}, 2020.
\bibitem{fiatcrypto} A.~Erbsen, J.~Philipoom, J.~Gross, R.~Sloan,
A.~Chlipala. Simple High-Level Code for Cryptographic Arithmetic---With
Proofs, Without Compromises. \emph{IEEE S\&P}, pp.~1202--1219, 2019.
\bibitem{ed25519} D.~J.~Bernstein, N.~Duif, T.~Lange, P.~Schwabe,
B.-Y.~Yang. High-speed high-security signatures.
\emph{J.\ Cryptographic Engineering} 2(2): 77--89, 2012.
\bibitem{rfc8032} S.~Josefsson, I.~Liusvaara.
\emph{Edwards-Curve Digital Signature Algorithm (EdDSA)}. RFC 8032, 2017.
\bibitem{bernsteinlange} D.~J.~Bernstein, T.~Lange.
Faster addition and doubling on elliptic curves.
\emph{ASIACRYPT}, LNCS 4833, pp.~29--50, 2007.
\bibitem{twisted} D.~J.~Bernstein, P.~Birkner, M.~Joye, T.~Lange,
C.~Peters. Twisted Edwards curves.
\emph{AFRICACRYPT}, LNCS 5023, pp.~389--405, 2008.
\bibitem{pnueli77} A.~Pnueli. The temporal logic of programs.
\emph{IEEE FOCS}, pp.~46--57, 1977.
\end{thebibliography}
\end{document}

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -1,5 +1,5 @@
% Reconstructed directly from the 1344-line rejected ltl.tex and its PDF,
% using reinvention-outline.md and the live 13-leaf deployment as factual patch.
% Reinvented August 2026 from the round-11 review of the prior draft;
% the full revision history lives in this folder's README.md.
\documentclass[11pt]{article}
\usepackage[a4paper,margin=1.02in]{geometry}
\usepackage{amsmath,amssymb,amsthm,mathtools}
@ -12,7 +12,7 @@
\usepackage{tikz}
\usetikzlibrary{arrows.meta,positioning,fit,decorations.pathreplacing,calc}
\usepackage[colorlinks=true,linkcolor=blue!55!black,citecolor=blue!55!black,urlcolor=blue!55!black]{hyperref}
\hypersetup{pdftitle={Accountable Distribution of Machine-Checked Correctness Evidence: A Transparency Model and the Lean Transparency Log},pdfauthor={Olaf Horvath},pdfsubject={Transparency distribution of formal-verification replay evidence},pdfkeywords={formal verification, transparency log, Lean, Ed25519, Merkle tree, attestation}}
\hypersetup{pdftitle={Accountable Distribution of Machine-Checked Correctness Evidence: A Transparency Model and the Lean Transparency Log},pdfauthor={Olaf Horvath},pdfsubject={Transparency distribution of formal-verification replay evidence},pdfkeywords={formal verification, transparency log, Lean, Ed25519, SLH-DSA, FIPS 205, Merkle tree, attestation}}
\lefthyphenmin=2 \righthyphenmin=3
\newtheorem{definition}{Definition}
@ -26,6 +26,7 @@
\newcommand{\Hh}{\mathsf{H}}
\newcommand{\hleaf}{\mathsf{h}_{\rm leaf}}
\newcommand{\hnode}{\mathsf{h}_{\rm node}}
\hyphenation{time-stamp time-stamps}
\newcommand{\MTH}{\mathsf{MTH}}
\newcommand{\Root}{\mathsf{Root}}
\newcommand{\Path}{\mathsf{Path}}
@ -47,7 +48,7 @@ showstringspaces=false,breaklines=true,xleftmargin=.5em,xrightmargin=.5em}
\large A Transparency Model and the Lean Transparency Log}
\author{Olaf Horvath\\
\small Olaf.Horvath@zkdefi.org \quad ORCID 0009-0004-8008-5805}
\date{July 2026}
\date{July 2026 \\ {\normalsize Revised: August 2026 --- v0.15}}
\begin{document}
\maketitle
@ -73,14 +74,20 @@ We instantiate the model as the Lean Transparency Log (LTL), using Lean~4 replay
attestations and an RFC~9162 Merkle tree. We give explicit
collision-extracting arguments for inclusion and consistency, lift them to
scheme-level accountability games with a composition theorem, and evaluate a
live deployment over four production Ed25519 codebases. The public log
contains thirteen leaves; the thirteenth attests a Lean mechanization of the
live deployment over four production Ed25519 codebases and the verification
path of SLH-DSA (FIPS~205), the hash-based post-quantum signature standard.
The public log contains nineteen leaves, including a Lean mechanization of the
accumulator's own security arguments (61 human-reviewed certificates with one
project-specific uninterpreted SHA-256 boundary axiom). The mechanization effort also exposed,
via differential testing, a nontrivial implementation boundary --- the
deployed iterative consistency verifier is not extensionally equal to the
recursive model on malformed size claims --- and the leaf records this
limitation explicitly. The contribution is a cryptographic distribution
project-specific uninterpreted SHA-256 boundary axiom) and, as its newest
entry, eleven certificates over the SLH-DSA-SHA2-128s verifier. Since tree
size 14 every signed head additionally carries a deterministic SLH-DSA
co-signature --- produced with the parameter set whose verification path the
log itself attests. The mechanization effort also exposed, via
differential testing, a nontrivial model/implementation divergence --- on
malformed size claims, the deployed iterative consistency verifier was not
extensionally equal to the recursive model proved in Lean (since closed;
this paper reports the pre-closure measurements) --- recorded explicitly
in the corresponding log entry. The contribution is a cryptographic distribution
model for machine-checked correctness evidence, with an end-to-end deployed
instantiation that carries scoped proofs about its own accountability
machinery.
@ -105,7 +112,7 @@ This creates a problem that is logically downstream of proof construction:
\begin{quote}
\emph{How can a consumer that cannot execute the prover obtain precise,
accountable evidence about a proof replay, without collapsing the result into
an opaque provider verdict?}
an opaque provider label?}
\end{quote}
A detached signature on the word ``verified'' authenticates an issuer but does
@ -116,7 +123,10 @@ portability objective. Committees distribute trust but do not themselves fix
the semantics of the attested result. Succinct proofs of verifier execution
would provide validity rather than mere accountability, but require a circuit or
verified-VM representation of the prover and are not yet the deployment
assumption of the artifacts studied here.
assumption of the artifacts studied here. Each alternative thus fails on
one of two sides: the cost of checking stays with the consumer, or it
disappears because belief in a label is demanded. The primitive studied
here occupies the point between.
We therefore study a narrower primitive: \emph{accountable delegation of
deterministic proof replay}. The operator still observes the replay. The
@ -130,7 +140,7 @@ The contribution is not a new Merkle tree and not a new theorem prover. It is a
trust decomposition for distributing machine-checked correctness evidence:
\begin{center}
\fbox{\parbox{0.91\linewidth}{
\fbox{\parbox{0.91\linewidth}{\raggedright\hyphenpenalty=10000\exhyphenpenalty=10000
\textbf{Expensive deterministic verification produces an observation.
Transparency makes that observation accountable. Consumer-local policy decides
whether the observation is acceptable.}}}
@ -140,8 +150,10 @@ The Lean Transparency Log (LTL)\footnote{The acronym collides with linear
temporal logic~\cite{pnueli}; we note the collision once and rely on context.}
is the complete instantiation evaluated in this paper. Its subjects are four
Rust Ed25519 codebases with Lean~4~\cite{lean4} certificates against extracted
models. Its thirteenth public leaf attests the Lean corpus
that mechanizes the log's own accumulator arguments. Thus the paper's central
models. Leaf 12 --- its thirteenth entry --- attests the Lean corpus
that mechanizes the security arguments of the log's own Merkle accumulator
(the tree of \S\ref{sec:construction} together with its inclusion and
consistency verifiers). The paper's central
claim survives replacement of Lean, Ed25519, or RFC~9162 by other components;
what is essential is the distribution and accountability model.
@ -163,18 +175,25 @@ those observations with their own policy; operator labels can veto but cannot
grant acceptance. We state clearly that axiom-name equality is not semantic
identity of theorem statements.
\item \textbf{A deployed cryptographic case study.}
The log contains twelve historical replay leaves for four verified Ed25519
codebases and a thirteenth leaf for the accumulator's own Lean corpus. The
entry-13 corpus carries an environment-derived audit inventory of 222 compiled
constants, 61 human-reviewed certificate cones, and a single uninterpreted
SHA-256 axiom.
The log contains nineteen leaves: three replay generations across the four
Ed25519 codebases (the newest at 44 certificates per fork; a \emph{certificate} throughout this paper is one theorem's kernel-checked proof together with its recorded axiom cone), two attestations
of the accumulator's own Lean corpus (leaf 12 carries an environment-derived
audit inventory of 222 compiled constants, 61 human-reviewed certificate
cones, and a single uninterpreted SHA-256 axiom; leaf 17 re-attests the
hardened state), and --- as leaf 18 --- the log's first post-quantum subject:
eleven certificates over the SLH-DSA-SHA2-128s verification path
(Appendix~\ref{app:slhtiers}).
\item \textbf{A negative deployment result.}
Differential testing found that the deployed iterative RFC-style consistency
verifier and the recursive model proved in Lean are not extensionally equal:
there are malformed size/root combinations accepted only by the deployed
verifier. We characterize 3,867 divergences in 73,573 pinned boundary tests
--- every one deployed-accepts-only --- and scope the public attestation
accordingly.
accordingly. (Subsequently closed: the divergence was traced to
the deployed verifier omitting RFC~9162 \S2.1.4.2 Step~7's terminal
$sn=0$ condition; restoring that one conjunct removes every divergence in the
pinned family, confirmed by a three-way regression against an independent
faithful RFC transliteration.)
\end{enumerate}
\paragraph{Non-claims.}
@ -184,16 +203,18 @@ binary correspondence, compiler correctness, extraction faithfulness,
side-channel resistance, SHA-512 correctness, or execution provenance of the
signing binary. The present leaf schema identifies theorem declarations by
repository commit and name, not by a canonical digest of their elaborated Lean
types. These are explicit boundaries, not hidden qualifications.
types. These are explicit boundaries, not hidden qualifications
(Appendix~\ref{app:matrix} tabulates every consumer-facing claim with its
establishing mechanism and remaining assumption).
\section{The distribution problem}\label{sec:problem}
\subsection{Three evidence modes}
Let a subject repository at commit $g$ contain theorem declarations
$T_1,\dots,T_q$. A deterministic verifier execution produces an observation
$O_g$ containing success/failure and the reported assumption cone of each
$T_i$ --- the set of axioms the checked proof of $T_i$ ultimately rests on. There are three natural ways to consume this result.
$\Theta_1,\dots,\Theta_q$. A deterministic verifier execution produces an observation
$O_g$ containing success/failure and the reported \emph{axiom cone} (synonymously, the observed axiom-name set) of each
$\Theta_i$ --- the set of axioms the checked proof of $\Theta_i$ ultimately rests on. There are three natural ways to consume this result.
\begin{description}[leftmargin=1.5em,itemsep=4pt]
\item[Direct replay.] The consumer reconstructs the verifier environment and
@ -280,10 +301,11 @@ A replay attestation $a$ contains at least
\]
where $N_i$ is a declaration name, $s_i$ is replay status, and $A_i$ is the
observed axiom-name set. The deployed schema additionally carries diagnostics,
resource controls, scope, and exclusions.
resource controls, a machine-readable \emph{scope block} (the deployed
leaf-12 instance is quoted verbatim in Appendix~\ref{app:entry13}), and exclusions.
\begin{definition}[Attestation-transparency scheme]
An attestation-transparency scheme is a tuple
An attestation-transparency scheme is a~\mbox{tuple}
\[
\Pi=(\mathsf{KeyGen},\mathsf{Append},\mathsf{ProveIncl},
\mathsf{VerifyIncl},\mathsf{ProveCons},\mathsf{VerifyCons},\mathsf{Verdict})
@ -291,7 +313,7 @@ An attestation-transparency scheme is a tuple
over a hash function and signature scheme. $\mathsf{Append}$ commits the
canonical serialization of an attestation as the next leaf and returns a signed
tree head. $\mathsf{Verdict}$ is parameterized by consumer-local policy and
does not consume an operator verdict as positive evidence.
does not consume an operator label as positive evidence.
\end{definition}
\begin{definition}[Accountable replay distribution]
@ -302,9 +324,10 @@ determined leaf value at its claimed position; (ii) a
consumer accepts a later view only as the same view or a verified extension;
(iii) two valid equal-size heads with unequal roots, in one log and protocol
context, form transferable evidence
that the key holder signed incompatible views; and (iv) positive acceptance of
that the key holder signed incompatible views, except under signature
forgery; and (iv) positive acceptance of
a theorem boundary is a function of recorded observations and consumer-local
policy, not of an operator verdict.
policy, not of an operator label.
\end{definition}
The definition is intentionally an accountability property, not a validity
@ -375,8 +398,9 @@ EUF-CMA security of the head-signature scheme, and correct initial acquisition
of the operator public key.
The model deliberately does not cryptographically exclude fabricated kernel
observations. That is a statement about a physical execution on the operator's
machine. The mechanism instead makes the claimed execution target precise
observations; the same holds when the operator's replay harness is defective
rather than dishonest. Whether the kernel actually ran as claimed is a
fact about a physical execution on the operator's machine. The mechanism instead makes the claimed execution target precise
enough for a third party to replay.
\subsection{Consumer goals}
@ -393,7 +417,7 @@ a later view only if it is the same view or a verified extension. Two valid
heads of equal size and unequal roots, in one log context, are transferable
evidence that the key holder signed incompatible views. Unequal-size forks
require retained history, gossip, or a witness. The transition discipline
itself is syntactic, enforced by the pin rule by construction; the semantic
itself is syntactic, enforced by the pin rule (\S4.3) by construction; the semantic
content --- an opened position cannot change value across accepted views ---
is a theorem (\S\ref{sec:games}).
\item[G3: Policy separation.] The operator's positive label cannot make a
@ -445,7 +469,22 @@ consistency proofs are the RFC~9162 algorithms~\cite{ct2}.
A tree head contains schema-version and type tags, a log identifier, tree
size, root hash, timestamp, and hash-algorithm identifier. The canonical JSON serialization of
those fields is signed with Ed25519. The log identifier and version tag prevent
cross-log and cross-protocol replay.
cross-log and cross-protocol replay. We call the leaf history a head
commits to a \emph{view}, and write \emph{signed view} for that history
as represented by its signed head.
Since tree size 14, every head additionally carries a \emph{deterministic}
SLH-DSA-SHA2-128s (FIPS~205) signature over the same payload. The
co-signature is additive: the Ed25519 signature remains the one every
consumer must verify, and heads published before size 14 carry no
post-quantum signature --- the standalone verifier reports the co-signature as
absent on such heads rather than rejecting them: an append-only log
necessarily preserves the history of its own signature-scheme upgrades. Determinism is chosen as an audit primitive: a
deterministic re-sign of the same payload is byte-comparable, so ``same
input, same signature'' becomes a diff rather than an assurance. The
co-signature closes a further loop: its parameter set is exactly the one
whose verification path is attested at leaf 18
(\S\ref{sec:slhdsa}, Appendix~\ref{app:slhtiers}).
The current implementation records signing-backend provenance alongside the
signature, but that provenance is not execution attestation: an Ed25519
@ -464,6 +503,9 @@ same-size fork evidence;
\item larger size: accept iff a consistency proof verifies, then update;
\item smaller size: reject as rollback.
\end{itemize}
We call this transition discipline the \emph{pin rule}, and the persisted
pair $(n_{\mathrm{pin}},r_{\mathrm{pin}})$ the \emph{pin-store}.
Freshness is an external availability policy. A persisted pin detects rollback
relative to local history; it does not prove that a client sees the globally
latest signed head.
@ -479,7 +521,8 @@ $D$, $d$, $m$, $n$ & leaf list; leaf bytes; leaf index; tree size \\
$\MTH(D)$;\ $k$ & Merkle root; split point (largest power of two below $n$) \\
$\Path(m,D)$;\ $\Root(v,m,n,P)$ & inclusion path (leaf to root); path refold \\
$\mathsf{Open}(d,m,n,P,r)$ & accepting opening: $m<n$ and $\Root(\hleaf(d),m,n,P)=r$ \\
$\ConsRec$;\ $\mathsf{Ext}$ & recursive consistency verifier; pin-rule transition \\
$\ConsRec$;\ $\mathsf{Ext}$ & recursive consistency verifier; pin-rule transition (\S\ref{sec:games}) \\
$C$;\ $b$ & consistency proof; flag: old root is the pinned $r_0$ ($\top$) vs read from $C$ \\
$\Obs_a(c)$;\ $\Policy(c)$ & axiom names recorded in leaf $a$; consumer's allowed set \\
$\chi_{\rm enc}$;\ $\chi=(\chi_{\rm enc},pk)$ & payload-encoded head context; full context with the key \\
$h=(n,r,t;\sigma)$;\ $\mathsf{Vf}_{pk}$ & signed head (size, root, timestamp); signature check \\
@ -496,9 +539,10 @@ This section states the consumer-facing arguments in the form used by the Lean
mechanization. The proofs are elementary but explicit: successful false
openings yield concrete SHA-256 collisions rather than appealing to an informal
``Merkle trees are secure'' statement. The explicitness is load-bearing: over a
fixed-width hash a bare ``some collision exists'' is vacuously true by
counting, so each soundness statement is about a named extractor function, and
the corpus pins a machine-checked non-vacuity guard for every extractor.
fixed-width hash, ``some collision exists'' is trivially true by counting;
each soundness statement therefore names an explicit extractor function,
and the corpus pins a machine-checked guard that each extractor's output
really is a collision (distinct preimages, equal digests).
\subsection{Inclusion}
@ -561,7 +605,9 @@ $|D_0|=n_0\le n_1=|D_1|$, $D_0\neq D_1[0{:}n_0]$, and
\ConsRec(n_0,n_1,C,\top,\MTH(D_0))
=(\MTH(D_0),\MTH(D_1)),
\]
$\mathcal{E}_{\rm cons}(D_0,D_1,C)$ returns a SHA-256 collision.
$\mathcal{E}_{\rm cons}(D_0,D_1,C)$ returns a SHA-256 collision. (The hypothesis supplies the honest
$\MTH(D_0)$ as the pinned value; \S5.3 measures the deployed flow, which
has no such mechanized supplier.)
\end{theorem}
\begin{proof}
The new-root component is a hash fold over the shape of the $n_1$ tree. Compare
@ -575,13 +621,16 @@ collision.
\end{proof}
The theorem's hypothesis pins the honest old root $\MTH(D_0)$. The deployed
flow has no mechanized supplier of that pin; the next subsection measures what
the iterative verifier does when size claims alone steer its walk.
flow contains nothing mechanized that guarantees the pinned value is the
honest old root; the next subsection measures the iterative verifier's
behavior when only the claimed sizes constrain its reconstruction.
\begin{proposition}[Pin-store safety]
Assume EUF-CMA security of the head signature and collision resistance of
SHA-256. A consumer following the pin transition accepts only a nondecreasing
sequence of sizes whose exhibited leaf lists are prefix-related. Two accepted
sequence of sizes; if leaf lists are exhibited for two accepted heads,
they are prefix-related except under collision (see the mapping paragraph
of \S\ref{sec:games}). Two accepted
heads under the same key, in one log context, with equal size and unequal
roots are transferable evidence that the key holder signed incompatible
views.
@ -589,7 +638,8 @@ views.
\begin{proof}
Rollback is rejected syntactically. At equal size the transition is accepted
only with equal roots; if the two exhibited equal-length leaf lists differed,
whole-tree binding would extract a SHA-256 collision, so under collision
whole-tree root binding (each root determines its committed leaf list up
to collision) would extract a SHA-256 collision, so under collision
resistance the lists are equal. A larger head is accepted only after a
consistency proof, so non-prefix acceptance yields a collision by the previous
theorem. Equal-size unequal roots in one log context, with valid signatures, are two
@ -603,7 +653,7 @@ certificate is a function only of $\Obs_a(c)$ and $\Policy(c)$. An operator
label cannot change a nonconforming observation into a conforming one.
\end{proposition}
\begin{proof}
The comparison is set equality and takes no positive operator verdict as input.
The comparison is set equality and takes no positive operator label as input.
A deployment may conservatively treat an operator failure label as a veto, but
a veto cannot grant acceptance.
\end{proof}
@ -621,6 +671,10 @@ reconstruction with the supplied old root and consults the size claims only as
bit-navigation state, so several distinct old-size claims navigate one proof
identically. In 73,573 lied-size boundary cases, 3,867 divergences were
observed; all were one-sided (deployed accepts, recursive model rejects).
The root cause was later identified and closed: the deployed loop omitted
RFC~9162 \S2.1.4.2 Step~7's terminal $sn=0$ condition; with the conjunct
restored the pinned family shows zero divergences (three-way regression:
deployed verifier, recursive model, independent RFC transliteration).
The intended consumer flow binds $(n_0,r_0)$ in local persistent state and
binds $(n_1,r_1)$ together in a signed head. The present corpus does not prove
@ -886,9 +940,9 @@ In the smallest growth case $2\to3$ --- the log's own transition in
old tree and $P_0$ is simply the opening's within-prefix tail.
\paragraph{Game $\mathsf{HIST}$ (local history binding).}
$\mathcal{A}$ outputs a chain of head values $h_0,\dots,h_k$, transition
proofs $C_1,\dots,C_k$ with $\mathsf{Ext}(n_{i-1},r_{i-1},n_i,r_i,C_i)=1$
for every $1\le i\le k$, indices $0\le a<b\le k$, an index $m<n_a$, and
$\mathcal{A}$ outputs a chain of head values $h_0,\dots,h_\ell$, transition
proofs $C_1,\dots,C_\ell$ with $\mathsf{Ext}(n_{i-1},r_{i-1},n_i,r_i,C_i)=1$
for every $1\le i\le \ell$, indices $0\le a<b\le \ell$, an index $m<n_a$, and
openings with
$\mathsf{Open}(d,m,n_a,P,r_a)=\mathsf{Open}(d',m,n_b,P',r_b)=1$ and
$d\neq d'$. $\mathcal{A}$ wins iff everything verifies. (The chain is the
@ -899,7 +953,7 @@ transcripts regardless of provenance.)
\begin{theorem}[History binding]\label{thm:hist}
There is an explicit algorithm $\mathcal{B}_{\rm hist}$ that, whenever
$\mathcal{A}$ wins $\mathsf{HIST}$, outputs a SHA-256 collision, using
$O(k\log n_k)$ hash evaluations.
$O(\ell\log n_\ell)$ hash evaluations.
\end{theorem}
\begin{proof}
By Lemma~\ref{lem:mono}, $m<n_a\le n_i$ for all $i\ge a$. Walk $t$ from $b$
@ -912,7 +966,7 @@ at $h_a$ yields two accepting openings of $d\neq d'$ at $m$ under
$(n_a,r_a)$, and Theorem~\ref{thm:pb} extracts the collision. Accepted
transcripts have their RFC-determined logarithmic length --- malformed
lengths reject --- so the walk costs at most the evaluations of replaying the
$k$ transition transcripts and the two openings.
$\ell$ transition transcripts and the two openings.
\end{proof}
\paragraph{Game $\mathsf{HEAD}$ (head authenticity).}
@ -958,7 +1012,7 @@ queried to the signing oracle; its valid signature is an existential forgery,
which $\mathcal{B}_{\rm fr}$ outputs.
\end{proof}
\begin{lemma}[Policy separation]\label{lem:policy}
\begin{lemma}[Policy separation --- Proposition~2 restated for the scheme package]\label{lem:policy}
For every leaf $a$ and certificate $c$, the verdict computed by
$\mathsf{Verdict}$ equals $[\Obs_a(c)=\Policy(c)]$; it reads no operator
label, and acceptance consults the operator's status only as a veto. This is
@ -977,11 +1031,11 @@ separation.
\end{definition}
\begin{theorem}[Collision-extractable accountability of the construction]\label{thm:main}
The LTL construction --- the RFC~9162 tree, the canonical signed heads of
\S4.2 in their fixed context $\chi$, the pin rule of \S4.3, and the policy
The LTL construction with the recursive verifiers of \S\ref{sec:security}
--- the RFC~9162 tree, the canonical signed heads of \S4.2 in their fixed context $\chi$, the pin rule of \S4.3, and the policy
verdict of \S\ref{sec:model} --- is collision-extractably accountable, with
$\mathcal{B}_{\rm pb}$ ($\le 2(\lceil\log_2 n\rceil{+}1)$ hash evaluations),
$\mathcal{B}_{\rm hist}$ ($O(k\log n_k)$), and the one-forgery reductions
$\mathcal{B}_{\rm hist}$ ($O(\ell\log n_\ell)$), and the one-forgery reductions
$\mathcal{B}_{\rm ha},\mathcal{B}_{\rm fr}$ of
Theorems~\ref{thm:pb}--\ref{thm:fork}.
\end{theorem}
@ -1020,7 +1074,7 @@ operator as first-class adversary, policy separation added.
\begin{remark}[What is mechanized, what is not]\label{rem:gamescope}
The games are stated for the scheme's specified verifiers --- the recursive
model whose honest-reference specializations are kernel-checked in entry~13
model whose honest-reference specializations are kernel-checked in leaf~12
(the named extractors and per-step pin safety). The two-transcript
comparisons and the transport induction are paper-level proofs in the same
discipline --- the induction reuses the corpus's mechanized
@ -1031,7 +1085,7 @@ verifier inherits the refinement boundary of the previous subsection
unchanged.
\end{remark}
\section{Lean and Ed25519 instantiation}\label{sec:instantiation}
\section{Lean instantiation: Ed25519 and SLH-DSA}\label{sec:instantiation}
\subsection{Proof corpus}
@ -1045,7 +1099,9 @@ for a large class of safe Rust programs~\cite{aeneas}. A recent independent
experience report likewise applies a Rust-to-Lean pipeline to cryptographic
code~\cite{klaus2026}.
Each fork's corpus contains sixteen reviewed certificates covering:
Each fork's corpus contained sixteen reviewed certificates at the
historical leaves studied here (the corpora have since grown to forty-four
per fork --- the log records both generations as separate leaves), covering:
\begin{itemize}[leftmargin=1.6em,itemsep=2pt]
\item five-limb field arithmetic over $\Fp$ with value and bound preservation;
\item complete twisted-Edwards group operations~\cite{edwards,twisted};
@ -1055,23 +1111,61 @@ Each fork's corpus contains sixteen reviewed certificates covering:
mathematical point equation.
\end{itemize}
The signature apex can be summarized as follows. Let $k$ be the challenge
scalar produced by an opaque SHA-512 boundary and let $r_1$ be the raw $R$ bytes
The signature apex is organized as four tiers T1--T4
(Appendix~\ref{app:tiers}). Let $c$ be the challenge scalar produced by an opaque SHA-512 boundary and let $\bar R$ be the raw $R$ bytes
from the signature. The corpus separates:
\begin{description}[leftmargin=1.5em,itemsep=2pt]
\item[T1:] acceptance iff the verifier's recomputed compressed bytes equal
$r_1$;
$\bar R$;
\item[T2:] those recomputed bytes are the canonical encoding of
$[k](-A)+[s]B$;
$[c](-A)+[s]B$;
\item[T3:] canonical encoding is injective on valid curve points;
\item[T4:] acceptance iff constructive decompression of $R$ yields
$[k](-A)+[s]B$.
$[c](-A)+[s]B$.
\end{description}
The separation keeps residual assumptions visible. SHA-512 and selected
wire-format interfaces are opaque boundaries at the apex; lower arithmetic and
group certificates use the foundational Lean axioms observed in the corpus.
\subsection{A second instantiation: the SLH-DSA verify path}\label{sec:slhdsa}
The second campaign extracts the verification path of SLH-DSA
(FIPS~205~\cite{fips205},
parameter set SHA2-128s) from a pinned pure-Rust implementation through the
same Charon/Aeneas route, starting from one monomorphic entry point with the
five hash primitives marked opaque at the extraction boundary. The corpus is
eleven certificates: ten \emph{loop-fidelity} theorems --- each stating
that an extracted loop computes the same value as a reference recursive
fold --- covering chain walking, WOTS recomputation and checksum, XMSS and
FORS Merkle ascent, hypertree layering, and digit/byte plumbing
(Appendix~\ref{app:slhtiers} lists each with its exact cone) and an acceptance characterization,
\code{slh_verify_128s_accepts_iff}: for every message digest, signature, and
public key at these parameters, the extracted verifier accepts exactly when
the recomputed hypertree root byte-equals the public key's root --- no other
acceptance path exists.
The terrain differs from Ed25519 in one structural way, and the leaf says so.
For Ed25519, each theorem relates extracted code to an independent
mathematical semantics (arithmetic over $\mathbb{Z}/p\mathbb{Z}$,
formalized with no reference to the extracted code); SLH-DSA verification
is hash chains and Merkle nodes all the way down, so its reference
specifications are folds over the same five uninterpreted hash oracles
(\code{h_msg}, \code{f}, \code{h}, \code{t_l}, \code{t_len}, modeling
the SHA-256 instantiations --- uninterpreted function symbols in the
logic, not random oracles) that the extracted loops call --- there is no
independent second semantics to land in. Each loop certificate therefore makes the extracted
control flow \emph{visible} --- small, sequential, checkable against the
standard's algorithms --- while the reading of fold against FIPS~205 remains
a declared human step. The audit enforces every certificate's axiom set
exactly in both directions, and the cone \emph{grows} up the pyramid ---
pure bit arithmetic rests on the kernel alone; the apex carries all five
oracles (Appendix~\ref{app:slhtiers}). Scope, stated in the leaf: the proved
subject is a monomorphic facade (the fixed-parameter entry point above)
whose bridge to the deployed generic verifier
is a 137-case differential test; one inner digit-extraction loop carries no
certificate; signing and key generation were never extracted.
\subsection{Replay attestation}
For every certificate the operator records:
@ -1108,18 +1202,25 @@ deployed verifier?
\subsection{Public state}
As of 16 July 2026, the public log contains thirteen leaves and current root
As of 15 August 2026, the public log contains nineteen leaves and current root
\begin{center}
\path{3488a2d0ff9f00415bb561d61b01a420e3ca2e0f7b29351ec9ebb3f57319da0d}.
\path{7ee239406890cf4ad59cc83ac3faa3d5cc48b29202159ee8c25bffd9737d32d8}.
\end{center}
Every signed head issued since public mirroring began is retained --- six
heads, at tree sizes 8 through 13 --- together with every leaf and receipt, in
an append-only Git mirror; a clone re-verifies the entire log offline with the
repository's standalone verifier.
The first twelve leaves are three four-fork replay generations. Leaves 0--3
Every signed head issued since public mirroring began is retained --- twelve
heads, at tree sizes 8 through 19, dual-signed from size 14 on --- together
with every leaf and receipt, in an append-only Git mirror; a clone
re-verifies the entire log offline with the repository's standalone verifier.
The first twelve leaves are three replay generations across the four
Ed25519 codebases. Leaves 0--3
record a failed audit run and remain permanently visible. Leaves 4--7 record a
clean replay. Leaves 8--11 re-attest rewritten repository histories rather
than replacing the old leaves. A leaf whose pinned commit ceases to be
than replacing the old leaves. Leaf 12 attests the accumulator's own Lean
corpus (\S\ref{sec:deployment}, E3); leaves 13--16 re-attest the four
Ed25519 corpora at 44 certificates each; leaf 17 re-attests the accumulator
corpus at its hardened state (the same corpus after closure of external
review findings); and leaf 18 attests the SLH-DSA-SHA2-128s
verification path --- the log's first post-quantum subject, and the scheme
that has co-signed every head since size 14. A leaf whose pinned commit ceases to be
distributed decays from a replayable claim to a historical record; consumers
act only on attestations whose subjects they can retrieve.
@ -1138,41 +1239,52 @@ boundary axiom, \code{LTLAcc.sha256}.
\centering
\begin{tikzpicture}[
>=Latex,
box/.style={draw,rounded corners=2pt,minimum width=1.03cm,minimum height=.52cm,font=\scriptsize,align=center},
box/.style={draw,rounded corners=2pt,minimum width=.78cm,minimum height=.5cm,font=\tiny,align=center,inner sep=1.5pt},
fail/.style={box,fill=black!6,draw=black!45,text=black!60},
pq/.style={box,fill=violet!8,draw=violet!60!black,text=violet!55!black},
ok/.style={box,fill=green!7!white,draw=deepgreen,text=deepgreen!80!black},
acc/.style={box,fill=blue!7!white,draw=deepblue,text=deepblue},
arrow/.style={->,draw=black!55}
]
\foreach \i in {0,...,3} {\node[fail] (l\i) at (1.08*\i,0) {\i\\failed};}
\foreach \i in {4,...,7} {\node[ok] (l\i) at (1.08*\i,0) {\i\\clean};}
\foreach \i in {8,...,11} {\node[ok] (l\i) at (1.08*\i,0) {\i\\clean};}
\node[acc] (l12) at (1.08*12,0) {12\\accum.};
\foreach \i in {0,...,3} {\node[fail] (l\i) at (0.82*\i,0) {\i\\failed};}
\foreach \i in {4,...,7} {\node[ok] (l\i) at (0.82*\i,0) {\i\\clean};}
\foreach \i in {8,...,11} {\node[ok] (l\i) at (0.82*\i,0) {\i\\clean};}
\node[acc] (l12) at (0.82*12,0) {12\\accum.};
\foreach \i in {13,...,16} {\node[ok] (l\i) at (0.82*\i,0) {\i\\re-att.};}
\node[acc] (l17) at (0.82*17,0) {17\\accum.};
\node[pq] (l18) at (0.82*18,0) {18\\slh-dsa};
\draw[decorate,decoration={brace,mirror,raise=5pt},black!45]
($(l0.south west)+(-.05,0)$)--($(l3.south east)+(.05,0)$)
($(l0.south west)+(.05,0)$)--($(l3.south east)+(-.05,0)$)
node[midway,below=11pt,font=\scriptsize]{run 1};
\draw[decorate,decoration={brace,mirror,raise=5pt},deepgreen]
($(l4.south west)+(-.05,0)$)--($(l7.south east)+(.05,0)$)
($(l4.south west)+(.05,0)$)--($(l7.south east)+(-.05,0)$)
node[midway,below=11pt,font=\scriptsize]{run 2};
\draw[decorate,decoration={brace,mirror,raise=5pt},deepgreen]
($(l8.south west)+(-.05,0)$)--($(l11.south east)+(.05,0)$)
($(l8.south west)+(.05,0)$)--($(l11.south east)+(-.05,0)$)
node[midway,below=11pt,font=\scriptsize]{run 3};
\node[draw,rounded corners,fill=softgray,minimum width=4.3cm,minimum height=.7cm,font=\small] (sth) at (6.7,1.65)
{signed head: size 13, root \code{3488a2d0...}};
\draw[arrow] (l12.north) -- (sth.south east);
\draw[decorate,decoration={brace,mirror,raise=5pt},deepblue]
($(l13.south west)+(.05,0)$)--($(l18.south east)+(-.05,0)$)
node[midway,below=11pt,font=\scriptsize]{August 2026};
\node[draw,rounded corners,fill=softgray,minimum width=5.9cm,minimum height=.85cm,align=center,font=\small] (sth) at (7.4,1.75)
{signed head: size 19, root \code{7ee23940...}\\dual-signed: Ed25519 $+$ SLH-DSA};
\draw[arrow] (l6.north) -- (sth.south west);
\draw[arrow] (l18.north) -- (sth.south east);
\end{tikzpicture}
\caption{The public 13-leaf deployment. Failure leaves are retained; entry 13
attests the accumulator corpus itself, scoped to the recursive model.}
\caption{The public nineteen-leaf deployment. Failure leaves are retained; leaf
12 (the thirteenth entry) attests the accumulator corpus itself, scoped to the
recursive model; leaves 13--16 re-attest the four forks at 44 certificates
each; leaf 17 the hardened accumulator corpus; leaf 18 the SLH-DSA verify
path. Heads are dual-signed from size 14 on.}
\label{fig:deployment}
\end{figure}
\subsection{Mechanization coverage}
Entry 13 is not a claim that the whole service is formally verified. The Lean
Leaf 12 is not a claim that the whole service is formally verified. The Lean
corpus covers the recursive Merkle model, inclusion completeness and
collision-extracting soundness, the consistency extractor, and the Merkle-layer
share of pin-store safety. The abstract root-binding lemma from the paper is
share of pin-store safety. The folklore whole-tree root-binding property (a root determines its
committed leaf list up to SHA-256 collision) is
mechanized through the specializations needed by the extractors rather than as
one quantified hash-fold theorem. Signature unforgeability, execution
provenance, the full signed-head state machine, asymptotic cost, and the
@ -1180,16 +1292,16 @@ refinement from the deployed iterative consistency verifier remain outside the
corpus.
\begin{center}\small
\begin{tabularx}{\textwidth}{@{}lXX@{}}
\begin{tabularx}{\textwidth}{@{}l>{\raggedright\arraybackslash}X>{\raggedright\arraybackslash}X@{}}
\toprule
Layer & Mechanized evidence & Explicit boundary \\
\midrule
Merkle definitions & MTH, Root, Path, recursive ConsRec & single SHA-256 boundary axiom \\
Merkle definitions & $\MTH$, $\Root$, $\Path$, recursive $\ConsRec$ & single SHA-256 boundary axiom \\
Inclusion & completeness and named collision extractor & collision resistance interpreted externally \\
Consistency & recursive-model soundness and extractor & no general consistency-completeness theorem \\
Pinning & per-step monotonicity and prefix correctness & signature layer and multi-step closure external \\
Deployment refinement & finite differential harness & no theorem for iterative verifier under authentic-pair invariant \\
Policy separation & deterministic tooling logic and regression tests & not mechanized in the entry-13 corpus \\
Policy separation & deterministic tooling logic and regression tests & not mechanized in the leaf-12 corpus \\
Scheme-level games (\S\ref{sec:games}) & paper-level explicit reductions & two-transcript comparisons and prefix transport not mechanized \\
\bottomrule
\end{tabularx}
@ -1198,9 +1310,10 @@ Scheme-level games (\S\ref{sec:games}) & paper-level explicit reductions & two-t
\subsection{Cost and reproducibility}
A replay of one Ed25519 fork requires approximately 30 minutes of end-to-end
guarded replay time under the pinned environment, a figure corroborated by the
resource-guarded (memory- and time-capped) replay time under the pinned environment, a figure corroborated by the
inter-leaf issuance spacing visible in the published log. Receipt verification requires one Ed25519
signature and a logarithmic number of SHA-256 node computations. The
signature and a logarithmic number of SHA-256 node computations (the
complete inclusion core is printed as Appendix~\ref{app:verifier}). The
accumulator corpus is independently reviewable with a pinned public Lean
release; an environment-derived inventory fails closed on added, removed, or
axiom-smuggling declarations.
@ -1224,7 +1337,9 @@ stable regression boundary.
\begin{remark}[Model/deployment seam]
For malformed size claims, the deployed iterative verifier and the recursive
model are not extensionally equal. In all 3,867 divergences observed across
model are not extensionally equal (figures are the pre-closure measurement;
the $sn=0$ restoration reduces the divergence count in this family to zero).
In all 3,867 divergences observed across
the pinned families the deployed verifier accepted and the model rejected; the
reverse direction was not observed, and no global inclusion relation between
the two acceptance sets is claimed.
@ -1261,7 +1376,7 @@ structure actually differs.
\paragraph{Transparency.}
Certificate Transparency introduced publicly auditable append-only logs for
certificate issuance~\cite{ct1,ct2}; Crosby and Wallach developed efficient
certificate issuance~\cite{ct1,ct2}; Crosby and Wallach built efficient
tamper-evident history trees~\cite{crosby}; Dowling et al. formalized security
notions for secure logging and CT~\cite{dghs} --- the games of
\S\ref{sec:games} adapt that two-transcript style to replay attestation, with
@ -1292,21 +1407,30 @@ about their replay.
\paragraph{Verification of transparency protocols.}
Cheval et al. mechanize transparency-protocol reasoning~\cite{cheval}.
The entry-13 corpus approaches the composition from the opposite direction: it
The leaf-12 corpus approaches the composition from the opposite direction: it
mechanizes accumulator arguments and then logs that replay result. The
remaining refinement from the deployed state machine to the recursive model is
explicitly open.
\section{Limitations and research agenda}\label{sec:limitations}
The subject corpus maintains a numbered ledger of fifteen known gaps together
with their closure options; this section groups the load-bearing ones.
The subject corpus maintains a numbered public file, \code{KNOWN-GAPS},
of fifteen gaps with their closure options (the scope block of
Appendix~\ref{app:entry13} cites its items 14 and 15); this section groups the load-bearing ones.
\paragraph{Operator observation trust.}
A malicious operator can fabricate a replay report. Signatures and Merkle
proofs make the lie attributable and persistent; they do not make it true.
Targeted independent replay is the corrective mechanism.
\paragraph{Replay-harness integrity.}
A wrong observation needs no malice: a defective replay harness --- a bug in
the audit driver, a fail-open guard, a truncated transcript --- produces the
same evidentiary damage as a dishonest operator, with the same accountability
answer (the record is attributable and persistent; independent replay corrects
it). The subject corpus's adversarial gate self-tests exist for exactly this
reason and reduce, but cannot eliminate, the exposure.
\paragraph{Theorem identity.}
Names and repository commits are not canonical semantic identifiers, and
commit identifiers are SHA-1-based --- a weaker binding than the log's own
@ -1326,7 +1450,7 @@ isolated clients to receive that view. Independent witnesses or gossip are the
natural next deployment step.
\paragraph{Consistency refinement.}
The recursive model is proved; the iterative deployment diverges from it on
The recursive model is proved; the iterative deployment diverged from it on
malformed inputs, every observed divergence being deployed-accepts-only. The strongest closure is either to deploy
$\ConsRec$-equivalent semantics or to mechanize the signed-head and pin-store
flow and prove the authentic-pair refinement theorem.
@ -1356,7 +1480,7 @@ become attributable when compared.
The Lean Transparency Log demonstrates the complete construction. It amortizes
expensive replay over lightweight consumers, retains failed and superseded
observations, and carries a scoped attestation of the accumulator's own Lean
corpus as entry 13. Just as importantly, the mechanization and differential
corpus as leaf 12. Just as importantly, the mechanization and differential
harness exposed a mismatch between the recursive model and the deployed
consistency verifier. Recording that mismatch in the public leaf is not a
failure of the method; it is evidence that the trust decomposition is doing
@ -1370,7 +1494,7 @@ replay attestation provides an immediate infrastructure layer while those
stronger validity mechanisms are developed.
\section*{Artifact availability}
The live service is \url{https://ltl.zkdefi.org}. Entry 13 has leaf hash
The live service is \url{https://ltl.zkdefi.org}. Leaf 12 (the log's thirteenth entry) has leaf hash
\begin{center}\small\ttfamily
8cb258d657f1fd00baaa9e0091e26c316cb69b591cb249a9543f51cade57c50a
\end{center}
@ -1396,7 +1520,7 @@ The author designed the system and is responsible for every claim. Claude
tooling, and manuscript review. Their output was not accepted as evidence;
claims were retained only after human review or reproducible artifact checks.
\begin{thebibliography}{22}
\begin{thebibliography}{23}
\itemsep2pt
\bibitem{ct1} B. Laurie, A. Langley, E. K\"asper. Certificate Transparency.
RFC 6962, 2013.
@ -1476,32 +1600,50 @@ Collision-Resistant Hashing without the Keys. VIETCRYPT, LNCS 4341, pp.
Verification Pipeline with AI Provers: An Experience Report. arXiv:2605.30106,
2026.
\bibitem{fips205} National Institute of Standards and Technology.
Stateless Hash-Based Digital Signature Standard. FIPS 205, August 2024.
\end{thebibliography}
% Appendix policy (declared 2026-08-16): the appendix block starts on a
% fresh page and then flows continuously -- no page breaks between
% individual appendices. The claim matrix is one unbreakable tabularx.
\clearpage
\appendix
\section{End-to-end claim matrix}\label{app:matrix}
\begin{center}\small
\begin{tabularx}{\textwidth}{@{}XXX@{}}
\begin{tabularx}{\textwidth}{@{}>{\raggedright\arraybackslash}X>{\raggedright\arraybackslash}X>{\raggedright\arraybackslash}X@{}}
\toprule
Consumer conclusion & Established by & Remaining assumption \\
\midrule
Leaf has an authentic opening with a position-bound leaf value at index $m$ under head $h$ & inclusion proof and signed head & SHA-256 collision resistance; correct public key; EUF-CMA of the head signature \\
Leaf has an authentic opening with a position-bound leaf value at index $m$ under head $h$ & inclusion proof and signed head & \mbox{SHA-256} collision resistance; correct public key; \mbox{EUF-CMA} of the head signature \\
\addlinespace[3pt]
Head root commits the published numbered leaf list & full-mirror recomputation (\code{verify.py --all}) & mirror availability and retention \\
Head was authorized by the log identity & Ed25519 verification & correct key acquisition; EUF-CMA \\
New pinned head extends old pinned head & consistency proof & SHA-256 collision resistance; recursive-model soundness; authentic size/root pairing for deployment \\
Equal-size unequal roots in one log context conflict & two valid signatures & correct public key; EUF-CMA; operationally, a retaining observer must compare the heads \\
\addlinespace[3pt]
Head was authorized by the log identity & Ed25519 verification & correct key acquisition; \mbox{EUF-CMA} \\
\addlinespace[3pt]
New pinned head extends old pinned head & consistency proof & \mbox{SHA-256} collision resistance; recursive-model soundness; authentic size/root pairing for deployment \\
\addlinespace[3pt]
Equal-size unequal roots in one log context conflict & two valid signatures & correct public key; \mbox{EUF-CMA}; operationally, a retaining observer must compare the heads \\
\addlinespace[3pt]
Observed cone matches local boundary policy & exact set equality & semantic identity of named declarations \\
Operator claims the kernel produced the observation & attestation signature and leaf inclusion & correct provider key; EUF-CMA \\
\addlinespace[3pt]
Operator claims the kernel produced the observation & attestation signature and leaf inclusion & correct provider key; \mbox{EUF-CMA} \\
\addlinespace[3pt]
Kernel actually produced the recorded observation & not cryptographically established; independently checkable by replay & operator and replay-pipeline honesty, or faithful independent replay \\
\addlinespace[3pt]
Recorded cone was produced by an audit that performed its checks & not established --- the audit driver is itself part of the replay pipeline & audit-gate integrity; adversarial gate self-tests reduce this exposure, they do not eliminate it \\
\addlinespace[3pt]
Source corresponds to deployed binary & not established & reproducible build and compiler assurance \\
\addlinespace[3pt]
Claimed signer implementation produced STH & not established & execution provenance \\
\bottomrule
\end{tabularx}
\end{center}
\section{Deployed entry-13 scope}\label{app:entry13}
The thirteenth public leaf contains the following deployment constraint,
\section{Deployed leaf-12 scope}\label{app:entry13}
Leaf 12 contains the following deployment constraint,
quoted verbatim, in its machine-readable scope block:
\begin{quote}\small
Attestation scope: this corpus kernel-checks the listed theorems about the
@ -1515,7 +1657,6 @@ additionally relies on an unmechanized authentic-size/root invariant
Its exclusions name SHA-256 collision resistance, deployed-verifier extensional
equality, the signature/STH layer, and asymptotic cost claims.
\clearpage
\section{Compact receipt-verification core}\label{app:verifier}
The following code is only the Merkle inclusion core. A complete receipt
verifier must additionally validate the signed tree head, log identifier,
@ -1578,4 +1719,48 @@ T4 & constructive decompression lift & \code{verify_accepts_iff_decompress} \\
\end{tabular}
\end{center}
All four tiers share one opaque boundary --- the SHA-512 challenge hash
and the selected wire-format interfaces (\S\ref{sec:instantiation});
the arithmetic and group certificates beneath them rest on Lean's
foundational axioms alone.
\section{SLH-DSA verification certificates and their cones}\label{app:slhtiers}
Eleven certificates over the extracted SLH-DSA-SHA2-128s verify path
(leaf 18). Beyond Lean's three foundational axioms, each certificate's
exact axiom set consists of the uninterpreted hash oracles listed ---
enforced by the audit as set equality in both directions, so the table is
machine-checked, not documentation. The cone grows with the layer: pure
digit/byte arithmetic rests on the kernel alone; the apex carries all
five oracles.
\begin{center}\small
\begin{tabular}{@{}lll@{}}
\toprule
Layer & Lean declaration(s) & Oracles in the cone \\
\midrule
Digit/byte plumbing & \code{to_int_loop_eq}, \code{to_byte_loop_eq} & --- \\
& \code{wots_csum_loop_eq}, \code{base2b_outer_loop_eq} & \\
Chain walk & \code{chain_free_loop_eq} & \code{f} \\
WOTS pk recomputation & \code{wots_loop1_eq} & \code{f} \\
XMSS Merkle ascent & \code{xmss_loop_eq} & \code{h} \\
FORS inner ascent & \code{fors_inner_loop_eq} & \code{h} \\
FORS outer loop & \code{fors_outer_loop_eq} & \code{f}, \code{h} \\
Hypertree walk & \code{ht_loop_eq} & \code{f}, \code{h}, \code{t_l} \\
Acceptance characterization & \code{slh_verify_128s_accepts_iff} & all five \\
\bottomrule
\end{tabular}
\end{center}
The oracles model the parameter set's SHA-256 hash-suite instantiations:
\code{h_msg} (message digest), \code{f} (chain step and FORS leaf),
\code{h} (Merkle node), \code{t_l} and \code{t_len} (the WOTS and FORS
compressors --- two axioms over what is one Rust primitive, deliberately
conservative, with the source's naming inversion against the standard's
$T_\ell$/$T_k$ documented at the declarations). The acceptance characterization is proved directly from the verifier's
structure, not by composing the ten loop theorems --- it would remain
provable if any of the ten were deleted. Conversely, each loop
certificate carries assurance only insofar as a human has checked its
reference fold against the corresponding FIPS~205 algorithm.
\end{document}

View file

@ -1,122 +0,0 @@
# Paper reinvention — working outline (post second ePrint rejection)
Status: approved direction (operator order 2026-07-16: "entry 13 and
paper reinvention"); this outline is the committed starting point for
the full rewrite. Diagnosis it responds to: both external analyses
(GPT-5.6 rejection post-mortem; round-6 reviewer aside) converge on
POSITIONING, not correctness — five papers compressed into one, the
novelty (trust decomposition) occupying less space than its supporting
machinery, and the honest "components unchanged" refrain reading as
"nothing new" in a thirty-second editorial skim.
## 0. The one sentence (everything else exists to support it)
> We define and instantiate a cryptographic distribution model for
> machine-checked correctness evidence, enabling lightweight consumers
> to obtain accountable assurance without executing the original
> verifier — and demonstrate it end to end with a deployed log that
> carries kernel-checked proofs of its own accountability machinery as
> an entry in itself.
The second clause is the only-this-paper claim; it becomes literally
true at entry 13 and the paper should be finalized AFTER that append so
the abstract can cite the live leaf.
## 1. Title candidates (working)
- "Accountable Distribution of Machine-Checked Correctness Evidence"
- "Transparency for Theorem Provers: Distributing Formal Verification
to Consumers Who Cannot Replay It"
- (subtitle either way): "…with a deployed log that notarizes the
proofs of its own machinery"
"The Lean Transparency Log" becomes the *system's* name, not the
paper's identity.
## 2. Inverted structure
1. **Problem.** Theorem provers solve the PRODUCTION of correctness
evidence; nothing standard solves scalable DISTRIBUTION and
CONSUMPTION by clients that cannot run the prover (no toolchain, no
hours of replay, no trust in the operator's word). State it as the
gap between "verified" and "verifiable by you."
2. **Model.** The trust decomposition, presented abstractly:
(a) producer performs expensive deterministic verification once;
(b) the result becomes a precise attestation object (subject commit,
toolchain, certificates with exact assumption cones, scope);
(c) a transparency structure supplies persistence, consistency, and
equivocation-evidence; (d) consumers apply policy locally without
executing the verifier; (e) the residual trust is enumerated, not
waved at. Define the accountability property: misbehavior
manufactures transferable evidence.
3. **Design space** (the section the old paper lacked). Position
against: reproducible replay services; succinct proofs of proof
checking (SNARKed kernels); checkpoint/cosigning committees;
witness cosigning; downloadable proof artifacts; proof-carrying
code; cached prover environments/containers. Axis of comparison:
consumer cost, operator trust, accountability (evidence on
misbehavior), incrementality, and what happens when the operator
disappears. The log wins on accountability-per-consumer-cost; be
honest where SNARKed checking would win (validity vs
accountability) and why it is not yet practical for a full kernel.
4. **Instantiation: the LTL.** Lean 4 + Aeneas corpora as producers;
Ed25519 heads signed by the dogfooded verified signer
(self_inclusion); RFC 9162 accumulator; pin-store consumers.
Compressed from old §45; the four ed25519 subject corpora become a
TABLE + pointer to project documentation, not sections.
5. **Security of the instantiation.** The mechanized model (61
certificates, single opaque-SHA-256 axiom, environment-derived audit
surface); Theorems 13 + Prop 1 in one page each of STATEMENT (the
proofs live in the repo; the paper sells the statements). Present
gap 14/15 as a RESULT, not a confession: the model/deployment seam
located, measured (3,867 one-sided divergences), fenced by pinned
tests, and bridged by an explicitly stated operational invariant —
this is what honest mechanization boundaries look like.
6. **Exhibit A: the log attests itself.** Entry 13 as the paper's
demonstration: the accountability machinery's own proofs, carried by
the machinery, verifiable end to end by one command from a stock
toolchain. Include the leaf hash + head root; a reader can check
inclusion in seconds.
7. **Deployment & consumers.** Live service; warden (quorum custody
wallet) as the internal consumer; Swiss Post e-voting PoC as the
prospective external consumer (dalek-family transport). Replay
economics (measured ~1,800 s/fork) as the cost the model amortizes.
8. **Related work.** CT lineage (RFC 6962/9162, CONIKS, sigstore);
optimistic-rollup analogy (accountability-over-validity; the
fraud-proof reading of Theorem 3 — from
ltl-accumulator-verified/docs/optimistic-accountability.md);
proof-carrying code; reproducible builds/SLSA; in-toto.
9. **Limitations** = the 15-gap ledger, grouped (assumption base /
scope lines / unmechanized bridge), each one sentence, pointer to
the repo ledger.
## 3. What moves OUT of the paper
- Ed25519 corpus construction details → project docs / possible
separate implementation report ("Paper C" in the GPT taxonomy).
- Curriculum, wallet internals, agent-native surfaces → out entirely.
- Appendix A/B verbatim-leaf material → shrink to one leaf excerpt
(entry 13 itself, naturally).
- The §8 operational-history narrative → compress to the
retrievability-decay paragraph + one table.
## 4. Sequencing & venues
1. Entry 13 lands (prerequisite for the abstract's second clause).
2. Rewrite per this outline (fresh tex, new title; old ltl.tex remains
hosted as the system report at /paper — versioned, not erased).
3. arXiv (cs.CR) immediately for timestamp + citability.
4. Venue: CPP/ITP (mechanization framing) or SCORED/SecDev
(supply-chain framing) or RWC talk (deployment framing) — decide
after the rewrite settles which face leads.
5. All queued wording fixes (Lemma-2 specializations, fidelity =
pinned families, §10(i)/(v), Remark 1, pinned-pair side condition)
are ABSORBED by the rewrite rather than patched into the old text.
## 5. Success criterion
A hurried editor can answer "what is the contribution?" from the title
+ first paragraph alone, and the thought-experiment inversion holds:
remove any single supporting component (Lean, Ed25519, RFC 9162) and
the CLAIM survives; remove the trust-decomposition model and nothing is
left. That is the opposite polarity of the rejected manuscript.

View file

@ -2,9 +2,9 @@
This nested project is a prototype third-party proof-checking service. It reuses host Lean/Aeneas infrastructure, runs portable PACTA replay/audit checks, and emits signed attestation certificates.
It does not modify anything outside this repository. It may read configured toolchains such as `/Users/oho/GitClone/ClaudeCodeProjects/your-lean-project/aeneas-toolchain/env.sh`.
It does not modify anything outside this repository. It may read configured toolchains such as `~/aeneas-toolchain/env.sh`.
It can also maintain a local transparency log. The log is an RFC 9162-style Merkle accumulator over signed attestations. It emits Signed Tree Heads with Ed25519 today and records an ML-DSA/FIPS 204 signature slot as `unavailable` unless a real backend is present. Agents that require both signatures must reject such receipts.
It can also maintain a local transparency log. The log is an RFC 9162-style Merkle accumulator over signed attestations. It emits Signed Tree Heads with Ed25519 today; heads are dual-sign capable with an additive SLH-DSA-SHA2-128s slot (proven verify path), and the ML-DSA/FIPS 204 slot stays honestly `not_configured`/`unavailable` — never silently filled. Agents that require both signatures must reject such receipts.
## Commands

View file

@ -22,7 +22,11 @@ files in this repository alone:
1. every entry's leaf hash,
2. every historical Signed Tree Head against the recomputed prefix root
(a split view or tampered entry fails here),
3. every STH Ed25519 signature,
3. every STH Ed25519 signature and, where a head carries the ADDITIVE
SLH-DSA-SHA2-128s signature (FIPS 205, heads from 2026-08 on), that
too: a present-but-wrong post-quantum signature FAILS the run, a head
without one is allowed, and an OpenSSL too old to check it (pre-3.5)
is reported loudly as a degradation, never counted as verified,
4. every published receipt under receipts/ (with --all), and any receipt
supplied via --receipt FILE, as a FULL transparency receipt: type tag,
STH signature, REQUIRED key fingerprint, log id, presence of its STH
@ -140,6 +144,57 @@ def check_sth_signature(head) -> str:
return "VALID" if result.returncode == 0 else "INVALID"
def check_slh_dsa_signature(head):
"""ADDITIVE post-quantum check (SLH-DSA-SHA2-128s, FIPS 205).
Returns (status, hard_failure). Ed25519 remains the REQUIRED signature;
this one is verified when the head carries it and the local OpenSSL
(>= 3.5) can check it. The distinctions matter:
ABSENT - head predates the second signature. Allowed: additive.
VALID - verified against provider.slhdsa.pub.
INVALID - present and WRONG. Hard failure - a bad signature is never
a degradation.
WRONG-KEY - the head names a different key than the mirror ships.
Hard failure.
NO-PUBKEY - the head claims the signature but the mirror ships no
provider.slhdsa.pub. Broken publication: hard failure.
TOOLING - this OpenSSL cannot read SLH-DSA keys (pre-3.5). Honest
degradation: reported loudly, never counted as verified,
never failed - the required Ed25519 check still gates.
"""
slh = (head.get("signatures") or {}).get("slh_dsa") or {}
if slh.get("status") != "signed":
return "ABSENT", False
key = HERE / "provider.slhdsa.pub"
openssl = shutil.which("openssl")
if not openssl:
return "TOOLING", False
if not key.exists():
return "NO-PUBKEY", True
fp = slh.get("public_key_fingerprint_sha256")
if fp and fp != hashlib.sha256(key.read_bytes()).hexdigest():
return "WRONG-KEY", True
probe = subprocess.run([openssl, "pkey", "-pubin", "-in", str(key), "-noout"],
capture_output=True)
if probe.returncode != 0:
return "TOOLING", False
payload = canonical_json({k: v for k, v in head.items() if k != "signatures"})
with tempfile.TemporaryDirectory() as tmp:
payload_path = Path(tmp) / "p"
signature_path = Path(tmp) / "s"
payload_path.write_bytes(payload)
try:
signature_path.write_bytes(base64.b64decode(slh.get("signature_base64", "")))
except Exception:
return "INVALID", True
result = subprocess.run(
[openssl, "pkeyutl", "-verify", "-pubin", "-inkey", str(key), "-rawin",
"-in", str(payload_path), "-sigfile", str(signature_path)],
capture_output=True,
)
return ("VALID", False) if result.returncode == 0 else ("INVALID", True)
RECEIPT_TYPE = "pacta.transparency.receipt.v1"
@ -245,6 +300,7 @@ def main() -> int:
# published latest-sth.json is exactly the final history head.
previous = -1
log_id = None
slh_tooling_seen = False
for position, head in enumerate(heads):
size = int(head["tree_size"])
if size > len(leaves):
@ -260,8 +316,20 @@ def main() -> int:
signature = check_sth_signature(head)
if signature == "INVALID" or (signature == "UNAVAILABLE" and not args.structural_only):
failures.append(f"STH #{position} signature {signature}")
print(f"STH #{position} size={size} root={head['root_hash'][:16]}… prefix-root:{structural} signature:{signature}")
if args.structural_only:
slh = "SKIPPED"
else:
slh, slh_hard = check_slh_dsa_signature(head)
if slh_hard:
failures.append(f"STH #{position} slh_dsa {slh}")
if slh == "TOOLING":
slh_tooling_seen = True
print(f"STH #{position} size={size} root={head['root_hash'][:16]}… prefix-root:{structural} signature:{signature} slh_dsa:{slh}")
previous = max(previous, size)
if slh_tooling_seen:
print("NOTE: this log carries an ADDITIVE SLH-DSA (FIPS 205) signature that "
"your OpenSSL cannot check (needs >= 3.5). The required Ed25519 checks "
"above still gate this result; the post-quantum signature was NOT verified.")
latest_path = HERE / "latest-sth.json"
if latest_path.exists() and heads:
latest = json.loads(latest_path.read_text())
@ -371,6 +439,40 @@ def main() -> int:
cases.append(("--structural-only is explicit, never claims full",
code == 0 and "REDUCED" in out and "[full]" not in out))
# The ADDITIVE post-quantum signature must fail closed when tampered.
# Applicable only to mirrors whose heads carry it; older mirrors record
# the case as not-applicable rather than silently passing.
latest = json.loads((HERE / "latest-sth.json").read_text())
slh = (latest.get("signatures") or {}).get("slh_dsa") or {}
if slh.get("status") == "signed":
import base64 as _b64
import shutil as _sh
with tempfile.TemporaryDirectory() as tmp:
mirror = Path(tmp) / "mirror"
_sh.copytree(HERE, mirror)
raw = bytearray(_b64.b64decode(slh["signature_base64"])); raw[0] ^= 1
bad = _b64.b64encode(bytes(raw)).decode()
for name in ("latest-sth.json", "sth-history.jsonl"):
path = mirror / name
text = path.read_text().replace(slh["signature_base64"], bad)
path.write_text(text)
result = subprocess.run([sys.executable, str(mirror / "verify.py"), "--all"],
capture_output=True, text=True)
cases.append(("corrupted slh_dsa signature REJECTED",
result.returncode == 1 and "slh_dsa:INVALID" in result.stdout))
# and the missing-pubkey path: a mirror claiming the signature but
# shipping no key is a broken publication, not a degradation.
(mirror / "provider.slhdsa.pub").unlink()
for name in ("latest-sth.json", "sth-history.jsonl"):
path = mirror / name
path.write_text(path.read_text().replace(bad, slh["signature_base64"]))
result = subprocess.run([sys.executable, str(mirror / "verify.py"), "--all"],
capture_output=True, text=True)
cases.append(("signed slh_dsa without published key REJECTED",
result.returncode == 1 and "NO-PUBKEY" in result.stdout))
else:
cases.append(("slh_dsa cases n/a (no signed slh_dsa block in this mirror)", True))
with tempfile.TemporaryDirectory() as tmp:
os.symlink(sys.executable, Path(tmp) / Path(sys.executable).name)
code, out = run("--all", env={"PATH": tmp})
@ -396,15 +498,19 @@ This repository is the **git-published face** of a transparency log of
formal-verification attestations: signed statements that the Lean 4 proofs
of specific software, at specific git commits, re-check with exactly their
documented assumptions. Its first twelve leaves attest four cryptographic
Rust libraries (Ed25519 implementations); as of **entry 13 (2026-07-16)**
Rust libraries (Ed25519 implementations); as of **its thirteenth entry (leaf index 12, 2026-07-16)**
the log also attests **its own accumulator machinery** a kernel-checked
mechanization of the log's security analysis, so the log carries
kernel-checked proofs *about the accumulator model* underlying its own
inclusion and consistency reasoning, as one of its own entries (subject
[`ltl-accumulator-verified`](https://github.com/saymrwulf/ltl-accumulator-verified);
scoped to the mechanized model it does not prove operator honesty,
signing, or execution provenance). Current head: tree size 13, root
`3488a2d0`.
signing, or execution provenance). As of **2026-08** the log also attests
the **SLH-DSA (FIPS 205) verify-path proofs** (leaf 18)
([`fips205-slhdsa-verified`](https://github.com/saymrwulf/fips205-slhdsa-verified))
and its heads carry a **second, post-quantum SLH-DSA-SHA2-128s signature**
beside the required Ed25519 one. The current head is `latest-sth.json`
this README deliberately names no tree size, so it cannot go stale.
Layout:
@ -415,7 +521,8 @@ Layout:
| `receipts/<component>.receipt.json` | inclusion proof binding that attestation to the latest signed head |
| `sth-history.jsonl` | **every** Signed Tree Head ever issued the witness channel: all cloners see the same heads |
| `latest-sth.json` | the current head |
| `provider.ed25519.pub` | the provider's public key — the sole cryptographic identity anchor; each statement's truth additionally rests on the assumptions stated in its leaf |
| `provider.ed25519.pub` | the provider's Ed25519 public key — the REQUIRED identity anchor; each statement's truth additionally rests on the assumptions stated in its leaf |
| `provider.slhdsa.pub` | the provider's SLH-DSA-SHA2-128s public key (FIPS 205) — checks the ADDITIVE post-quantum head signature; needs OpenSSL >= 3.5, and verify.py degrades honestly below that |
| `verify.py` | standalone verifier (Python stdlib + the `openssl` binary; fails closed without them; `--all` covers every published receipt) |
| `verify_selftest.py` | adversarial self-test: proves the verifier's fail-closed paths reject mutated receipts |
@ -429,6 +536,9 @@ python3 verify.py --receipt receipts/dalek-ed25519-verified.receipt.json
The online service (same data, live endpoints + customer documentation):
**https://ltl.zkdefi.org**
The design and its security analysis:
**https://ltl.zkdefi.org/paper** (DOI [10.5281/zenodo.22057482](https://doi.org/10.5281/zenodo.22057482))
The provider tooling, agent tooling, and course materials:
**https://github.com/saymrwulf/proof-aware-crypto-tooling-agent**
@ -439,5 +549,12 @@ declared trusted base). The log deliberately
retains early leaves recording a **failed** audit run: an append-only
trust ledger keeps its history. Tree heads are signed by the merkleized,
proof-attested Ed25519 library itself, and each signature embeds the
provider's own Merkle self-check of that library's leaf.
provider's own Merkle self-check of that library's leaf. Heads additionally
carry a **deterministic SLH-DSA-SHA2-128s signature** over the same payload:
strictly additional, so Ed25519 remains the signature a consumer must check, and honest
about scope the estate's certificates cover the *verification* path of both
algorithms; no signing operation is proven for either, and leaves themselves
are Ed25519-signed at issuance only. Heads published before 2026-08 have no
SLH-DSA signature and verify.py reports them as `slh_dsa:ABSENT`, which is
allowed an append-only log keeps its history.
'''

View file

@ -30,8 +30,26 @@ def build_attestation(
profile = get_profile(repo.kind, repo)
layout = discover_layout(path, repo.verification_dir)
lean_guard = resolve_lean_guard(repo.lean_guard, path)
# The replay compiles the certificate corpus, not the button's audit
# instruments (see RepoConfig.replay_exclude). Filter by the path relative
# to the verification dir; record what was actually excluded so the LEAF
# says it — a silent exclusion here would be the exact defect class the
# estate keeps finding.
import fnmatch as _fnmatch
verification_root = path / repo.verification_dir
compile_order = layout.compile_order
instruments_excluded: list[str] = []
if repo.replay_exclude:
kept = []
for f in compile_order:
rel = str(Path(f).resolve().relative_to(verification_root.resolve()))
if any(_fnmatch.fnmatch(rel, g) for g in repo.replay_exclude):
instruments_excluded.append(rel)
else:
kept.append(f)
compile_order = kept
check = lean_check_files(
layout.compile_order,
compile_order,
layout.verification_dir,
timeout=timeout,
log_dir=log_dir,
@ -103,6 +121,10 @@ def build_attestation(
"check_log_path": check.log_path,
"checked_files": len(check.checked_files),
"failed_files": check.failed_files,
# The button's audit instruments this replay did NOT compile —
# disclosed in the leaf rather than silently absent. Empty for
# repositories without a replay_exclude list.
"instruments_excluded": sorted(instruments_excluded),
"diagnostics": check.diagnostics,
"axiom_attempted": axiom.attempted if axiom else False,
"axiom_ok": axiom.ok if axiom else False,

View file

@ -96,6 +96,8 @@ class TransparencyLog:
private_key_path: str | Path,
public_key_path: str | Path,
timestamp: str | None = None,
slhdsa_private_key_path: str | Path | None = None,
slhdsa_public_key_path: str | Path | None = None,
) -> dict[str, Any]:
metadata = self.metadata()
entries = self.entries()
@ -108,6 +110,8 @@ class TransparencyLog:
private_key_path,
public_key_path,
signing_provenance=self.signing_provenance(entries),
slhdsa_private_key_path=slhdsa_private_key_path,
slhdsa_public_key_path=slhdsa_public_key_path,
)
dump_data(sth, self.sth_path)
self._record_sth(sth)
@ -119,6 +123,8 @@ class TransparencyLog:
private_key_path: str | Path,
public_key_path: str | Path,
receipt_out: str | Path | None = None,
slhdsa_private_key_path: str | Path | None = None,
slhdsa_public_key_path: str | Path | None = None,
) -> dict[str, Any]:
metadata = self.metadata()
attestation = load_data(attestation_path)
@ -151,6 +157,8 @@ class TransparencyLog:
private_key_path,
public_key_path,
signing_provenance=self.signing_provenance(entries),
slhdsa_private_key_path=slhdsa_private_key_path,
slhdsa_public_key_path=slhdsa_public_key_path,
)
dump_data(sth, self.sth_path)
self._record_sth(sth)
@ -302,6 +310,12 @@ class TransparencyLog:
(out / "README.md").write_text(README_MD, encoding="utf-8")
if public_key_path is not None:
(out / "provider.ed25519.pub").write_bytes(Path(public_key_path).read_bytes())
# The SLH-DSA verification key travels beside the Ed25519 one the
# moment it exists (operator decision 2026-08-06: additive second
# signature). Public key only — nothing secret lives near this path.
slh_pub = Path(public_key_path).parent / "provider.slhdsa.pub"
if slh_pub.exists():
(out / "provider.slhdsa.pub").write_bytes(slh_pub.read_bytes())
return {"entries": len(entries), "components": sorted(components), "out": str(out)}
def consistency_from(self, old_tree_size: int) -> dict[str, Any]:

View file

@ -20,6 +20,39 @@ from .transparency_log import TransparencyLog
API_VERSION = "v1"
def _openapi_document(base: str) -> dict:
"""The machine interface, described the industry-standard way (OpenAPI 3)
instead of a hand-written endpoint box on the human docs page."""
q = lambda name, desc, req=True: {"name": name, "in": "query", "required": req,
"description": desc, "schema": {"type": "string"}}
ok = {"200": {"description": "success"}}
return {
"openapi": "3.0.3",
"info": {"title": "Lean Transparency Log",
"description": "Read-only CT-style interface of the LTL. "
"Heads are signed offline; this service holds no key material.",
"version": API_VERSION},
"servers": [{"url": "https://ltl.zkdefi.org" + base}],
"paths": {
"/log-public-key": {"get": {"summary": "Required Ed25519 public key (PEM)", "responses": ok}},
"/log-slhdsa-public-key": {"get": {"summary": "Post-quantum SLH-DSA public key (PEM)", "responses": ok}},
"/healthz": {"get": {"summary": "Liveness and current tree size", "responses": ok}},
"/paper": {"get": {"summary": "The current paper (PDF)", "responses": ok}},
f"/{API_VERSION}/metadata": {"get": {"summary": "Log identity", "responses": ok}},
f"/{API_VERSION}/sth": {"get": {"summary": "Latest Signed Tree Head", "responses": ok}},
f"/{API_VERSION}/sth-history": {"get": {"summary": "Every Signed Tree Head ever issued (witness material)", "responses": ok}},
f"/{API_VERSION}/sth-consistency": {"get": {"summary": "Consistency proof from a pinned size",
"parameters": [q("first", "your pinned old tree size")], "responses": ok}},
f"/{API_VERSION}/proof": {"get": {"summary": "Inclusion proof (freshly issued receipt)",
"parameters": [q("component", "component name", False), q("leaf_hash", "leaf hash (hex)", False)], "responses": ok}},
f"/{API_VERSION}/attestation": {"get": {"summary": "Newest attestation for a component",
"parameters": [q("component", "component name")], "responses": ok}},
f"/{API_VERSION}/entries": {"get": {"summary": "Raw leaves in [start, end)",
"parameters": [q("start", "first index", False), q("end", "one past last index", False)], "responses": ok}},
},
}
def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdfs: dict[str, bytes] | None = None):
paper_pdfs = paper_pdfs or {}
@ -34,6 +67,16 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
except Exception as exc: # noqa: BLE001 - the service must not die on a bad request
self._send(500, {"error": f"internal error: {type(exc).__name__}"})
def do_HEAD(self) -> None: # noqa: N802 - link checkers and mail/chat
# unfurlers probe with HEAD; answer with the same headers as GET
# and no body (a 501 here makes every link look broken to them).
self._head_only = True
self.do_GET()
def _body(self, body: bytes) -> None:
if not getattr(self, "_head_only", False):
self.wfile.write(body)
def _route(self) -> None:
parsed = urlparse(self.path)
path = parsed.path.rstrip("/")
@ -45,17 +88,11 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
if route in ("/", "/docs"):
self._send_html(docs_html)
elif route in ("/paper", "/paper/ltl.pdf",
"/paper/v0.1", "/paper/v0.1/ltl.pdf",
"/paper/v0.2", "/paper/v0.2/ltl.pdf"):
# /paper is the current paper; /paper/v0.N are prior versions,
# preserved for citability, linked from the docs page.
variant = "current"
if route.startswith("/paper/v0.1"):
variant = "v0.1"
elif route.startswith("/paper/v0.2"):
variant = "v0.2"
body = paper_pdfs.get(variant)
elif route in ("/paper", "/paper/ltl.pdf"):
# /paper serves the current paper only (v0.15, revised
# August 2026). Superseded drafts were retired from the
# site 2026-08-15; git history retains them.
body = paper_pdfs.get("current")
if body is None:
self._send(404, {"error": f"paper ({variant}) not available on this deployment"})
return
@ -64,12 +101,17 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
self.send_header("Content-Disposition", 'inline; filename="ltl.pdf"')
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
elif route == "/log-public-key":
self._body(body)
elif route in ("/log-public-key", "/log-slhdsa-public-key"):
# TOFU mitigation depends on the key being published in two
# independent locations; this is the site's copy (the mirror
# carries the other). Serving only a fingerprint would not do.
key_path = Path(log.log_dir) / "provider.ed25519.pub"
# The SLH-DSA key (additive post-quantum signature, 2026-08)
# is published THE SAME WAY as the Ed25519 one — same route
# shape, same two-location rule.
fname = ("provider.ed25519.pub" if route == "/log-public-key"
else "provider.slhdsa.pub")
key_path = Path(log.log_dir) / fname
if not key_path.is_file():
self._send(404, {"error": "log public key not present in this log directory"})
return
@ -78,7 +120,9 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
self._body(body)
elif route == "/openapi.json":
self._send(200, _openapi_document(base))
elif route == "/healthz":
self._send(200, {"ok": True, "tree_size": len(log.entries())})
elif route == f"/{API_VERSION}/metadata":
@ -152,7 +196,9 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
f"{base}/docs",
f"{base}/paper",
f"{base}/log-public-key",
f"{base}/log-slhdsa-public-key",
f"{base}/healthz",
f"{base}/openapi.json",
f"{base}/{API_VERSION}/metadata",
f"{base}/{API_VERSION}/sth",
f"{base}/{API_VERSION}/sth-history",
@ -186,7 +232,7 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
self.send_header("Content-Length", str(len(body)))
self.send_header("X-Robots-Tag", "noindex, nofollow")
self.end_headers()
self.wfile.write(body)
self._body(body)
return True
def _send(self, code: int, payload: dict[str, Any], code_if_error: int | None = None) -> None:
@ -198,7 +244,7 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
self._body(body)
def _send_html(self, html: str) -> None:
body = html.encode("utf-8")
@ -206,7 +252,7 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
self._body(body)
def log_message(self, fmt: str, *args: Any) -> None: # quiet by default
pass
@ -229,9 +275,7 @@ def serve(
docs_html = render_docs(log, base_path)
paper_dir = Path(__file__).resolve().parents[3] / "paper"
variants = {
"current": paper_dir / "ltl.pdf", # v0.3 reinvention, the live one
"v0.1": paper_dir / "ltl-v0.1.pdf", # prior 4-page version
"v0.2": paper_dir / "ltl-v0.2.pdf", # prior 19-page system report
"current": paper_dir / "ltl.pdf", # v0.11, revised August 2026
}
paper_pdfs = {name: p.read_bytes() for name, p in variants.items() if p.is_file()}
handler = make_handler(log, base_path, docs_html, paper_pdfs)

View file

@ -21,7 +21,8 @@ _STYLE = """
h2{font-size:1.2rem;margin-top:2.6rem;border-bottom:2px solid var(--line);padding-bottom:.3rem}
.tagline{font-size:1.05rem;color:var(--ink2);max-width:46rem}
code,pre{font-family:ui-monospace,Menlo,Consolas,monospace;background:#eef0f3;border-radius:4px}
code{padding:.1rem .3rem;font-size:.9em} pre{padding:.9rem;overflow-x:auto;font-size:.85rem}
code{padding:.1rem .3rem;font-size:.9em;overflow-wrap:anywhere}
pre{padding:.9rem;overflow-x:auto;font-size:.85rem;max-width:100%}
table{border-collapse:collapse;width:100%;font-size:.93rem;background:#fff}
td,th{border:1px solid var(--line);padding:.5rem .7rem;text-align:left;vertical-align:top}
th{background:var(--accentbg)}
@ -41,7 +42,6 @@ _STYLE = """
.sw{display:inline-block;width:.8rem;height:.8rem;border-radius:3px;vertical-align:-1px;margin-right:.3rem}
"""
def _leaf_ok(entry: LogEntry) -> bool:
certificates = ((entry.leaf.get("attestation") or {}).get("certificates")) or []
return bool(certificates) and all(
@ -49,8 +49,13 @@ def _leaf_ok(entry: LogEntry) -> bool:
for certificate in certificates
)
def _leaf_short(component: str) -> str:
"""Compact display name for a leaf box at small spans."""
return (component.replace("-ed25519-verified", "")
.replace("ltl-accumulator-verified", "accum")
.replace("fips205-slhdsa-verified", "slh-dsa"))
def _svg_tree(entries: list[LogEntry], root_hex: str, signing_backend: str) -> str:
def _svg_tree(entries: list[LogEntry], root_hex: str, signing_backend: str, head_label: str = "Ed25519") -> str:
"""The accumulator, drawn from its real leaves."""
if not entries:
return "<p class='muted'>(log is empty)</p>"
@ -77,12 +82,22 @@ def _svg_tree(entries: list[LogEntry], root_hex: str, signing_backend: str) -> s
ok = _leaf_ok(entry)
component = (((entry.leaf.get("attestation") or {}).get("subject")) or {}).get("component", "?")
fill, stroke = ("#e2f2e9", "#1e7f4f") if ok else ("#f4f4f6", "#8a93a0")
out.append(f'<rect x="{x-56}" y="{y-22}" width="112" height="44" rx="5" fill="{fill}" stroke="{stroke}" stroke-width="1.4"/>')
out.append(f'<text x="{x}" y="{y-6}" text-anchor="middle" fill="#333">leaf {node_index}</text>')
short = escape(str(component).replace("-ed25519-verified", ""))
# Boxes must FIT the per-leaf span at any tree size (the
# 2026-08-16 lesson: fixed 112px boxes shingled at 19
# leaves). Rich boxes while they fit, compact ones after.
box_w = min(112.0, span * 0.94)
compact = box_w < 100
short = escape(_leaf_short(str(component)))
label = short if ok else f"{short}"
out.append(f'<text x="{x}" y="{y+8}" text-anchor="middle" fill="{stroke}">{label}</text>')
out.append(f'<text x="{x}" y="{y+19}" text-anchor="middle" fill="#999" font-size="9">{node.hex()[:10]}…</text>')
if compact:
out.append(f'<rect x="{x-box_w/2:.1f}" y="{y-18}" width="{box_w:.1f}" height="36" rx="4" fill="{fill}" stroke="{stroke}" stroke-width="1.2"/>')
out.append(f'<text x="{x}" y="{y-4}" text-anchor="middle" fill="#333" font-size="8">leaf {node_index}</text>')
out.append(f'<text x="{x}" y="{y+9}" text-anchor="middle" fill="{stroke}" font-size="7">{label}</text>')
else:
out.append(f'<rect x="{x-box_w/2:.1f}" y="{y-22}" width="{box_w:.1f}" height="44" rx="5" fill="{fill}" stroke="{stroke}" stroke-width="1.4"/>')
out.append(f'<text x="{x}" y="{y-6}" text-anchor="middle" fill="#333">leaf {node_index}</text>')
out.append(f'<text x="{x}" y="{y+8}" text-anchor="middle" fill="{stroke}">{label}</text>')
out.append(f'<text x="{x}" y="{y+19}" text-anchor="middle" fill="#999" font-size="9">{node.hex()[:10]}…</text>')
else:
is_root = level_index == len(levels) - 1
out.append(f'<rect x="{x-50}" y="{y-15}" width="100" height="30" rx="5" fill="{"#eef0f7" if is_root else "#fff"}" stroke="{"#3b4d8f" if is_root else "#bbb"}" stroke-width="{1.6 if is_root else 1}"/>')
@ -91,16 +106,23 @@ def _svg_tree(entries: list[LogEntry], root_hex: str, signing_backend: str) -> s
for child in (2 * node_index, 2 * node_index + 1):
if (level_index - 1, child) in positions:
cx, cy = positions[(level_index - 1, child)]
out.append(f'<line x1="{x}" y1="{y+15}" x2="{cx}" y2="{cy-22 if level_index==1 else cy-15}" stroke="#ccc"/>')
leaf_top = 18 if len(entries) > 9 else 22
out.append(f'<line x1="{x}" y1="{y+15}" x2="{cx}" y2="{cy-leaf_top if level_index==1 else cy-15}" stroke="#ccc"/>')
root_x, root_y = positions[(len(levels) - 1, 0)]
out.append(f'<rect x="{root_x-190}" y="{root_y-72}" width="380" height="34" rx="6" fill="#e2f2e9" stroke="#1e7f4f" stroke-width="1.6"/>')
out.append(f'<text x="{root_x}" y="{root_y-58}" text-anchor="middle" fill="#1e7f4f" font-weight="bold">Signed Tree Head — Ed25519({root_hex[:12]}…)</text>')
out.append(f'<text x="{root_x}" y="{root_y-46}" text-anchor="middle" fill="#1e7f4f" font-size="9">signed by: {escape(signing_backend)} (the proof-attested library itself)</text>')
# The head box sizes itself to its longest line (the 2026-08-16
# lesson: a fixed 380px box let a growing caption spill both sides).
title = f"Signed Tree Head — {head_label}({root_hex[:12]}…)"
line2 = f"signed by: {signing_backend}"
line3 = "(verify path attested; signing itself not proven)"
head_w = max(len(title) * 7.0, len(line2) * 5.3, len(line3) * 5.3) + 28
out.append(f'<rect x="{root_x-head_w/2:.1f}" y="{root_y-84}" width="{head_w:.1f}" height="46" rx="6" fill="#e2f2e9" stroke="#1e7f4f" stroke-width="1.6"/>')
out.append(f'<text x="{root_x}" y="{root_y-70}" text-anchor="middle" fill="#1e7f4f" font-weight="bold">{escape(title)}</text>')
out.append(f'<text x="{root_x}" y="{root_y-58}" text-anchor="middle" fill="#1e7f4f" font-size="9">{escape(line2)}</text>')
out.append(f'<text x="{root_x}" y="{root_y-47}" text-anchor="middle" fill="#1e7f4f" font-size="9">{escape(line3)}</text>')
out.append(f'<line x1="{root_x}" y1="{root_y-38}" x2="{root_x}" y2="{root_y-15}" stroke="#1e7f4f" stroke-width="1.4"/>')
out.append("</svg>")
return "".join(out)
def _trust_anchor_html(log: TransparencyLog, metadata: dict[str, Any], base: str, mirror: str) -> str:
"""The provider public key, displayed in full on the front page. The key
is the one thing a consumer takes on trust, once - hiding it behind a
@ -114,20 +136,41 @@ def _trust_anchor_html(log: TransparencyLog, metadata: dict[str, Any], base: str
f'<a href="{mirror}/blob/main/provider.ed25519.pub">mirror</a> instead.</div>'
)
pem = escape(key_path.read_text(encoding="utf-8").strip())
# The SLH-DSA verification key (additive post-quantum head signature,
# 2026-08) is published THE SAME WAY: full PEM on the page, raw endpoint,
# mirror comparison. Heads before tree 14 carry no SLH-DSA signature and
# verify.py reports them ABSENT — allowed; an append-only log keeps its
# history.
slh_path = log.log_dir / "provider.slhdsa.pub"
if slh_path.is_file():
import hashlib as _h
slh_pem = escape(slh_path.read_text(encoding="utf-8").strip())
slh_fp = _h.sha256(slh_path.read_bytes()).hexdigest()
slh_block = f"""<hr style="border:none;border-top:1px solid #ddd;margin:.8rem 0">
<p style="margin-top:0"><strong>Key&nbsp;2 SLH-DSA (FIPS&nbsp;205), post-quantum.</strong> Heads from tree size&nbsp;14 on carry a second signature from this key;
older heads legitimately have none an append-only log keeps its history. Check it where your
tooling allows (OpenSSL&nbsp;&nbsp;3.5). The kind of code that verifies such signatures is itself
a proof subject of this log (entry&nbsp;18).</p>
<pre style="margin-bottom:.4rem">{slh_pem}</pre>
<p class="muted" style="margin:.2rem 0 0">SHA-256 fingerprint <code>{slh_fp}</code>
&nbsp;·&nbsp; raw: <a href="{base}/log-slhdsa-public-key"><code>{base or ''}/log-slhdsa-public-key</code></a>
&nbsp;·&nbsp; mirror: <a href="{mirror}/blob/main/provider.slhdsa.pub">provider.slhdsa.pub</a></p>"""
else:
slh_block = ""
return f"""<div class="card">
<p style="margin-top:0">This key is the <strong>sole cryptographic identity anchor</strong>: it
authenticates that these statements were made by the operator. It does not, by itself, make
those statements true each attestation's truth additionally rests on the replay, theorem,
extraction and toolchain assumptions stated in that leaf. Every tree head and attestation is
signature-checked against this key.
Pin it, and compare this copy byte-for-byte with the independently hosted
<a href="{mirror}/blob/main/provider.ed25519.pub">mirror copy</a>; they must be identical.</p>
<p style="margin-top:0"><strong>Two keys sign everything in this log.</strong> Neither makes a
claim <em>true</em>; they prove a claim comes from this operator, unchanged. Save your own copy
of both that is called <em>pinning</em>: from then on you trust only what verifies against
your saved copies. Fetch each key from this page AND from the independently hosted mirror and
compare byte-for-byte; the copies must be identical. (The first fetch is trust-on-first-use;
comparing two independent hosts is what bounds it.)</p>
<p><strong>Key&nbsp;1 Ed25519, required.</strong> Every signed head and every attestation must
verify against it.</p>
<pre style="margin-bottom:.4rem">{pem}</pre>
<p class="muted" style="margin:.2rem 0 0">SHA-256 fingerprint <code>{escape(fingerprint)}</code>
&nbsp;·&nbsp; raw: <a href="{base}/log-public-key"><code>{base or ''}/log-public-key</code></a>
&nbsp;·&nbsp; <code>curl -s ltl.zkdefi.org/log-public-key</code></p>
</div>"""
&nbsp;·&nbsp; <code>curl -s https://ltl.zkdefi.org/log-public-key</code></p>
{slh_block}</div>"""
def render_docs(log: TransparencyLog, base_path: str) -> str:
base = "/" + base_path.strip("/") if base_path.strip("/") else ""
@ -161,7 +204,9 @@ def render_docs(log: TransparencyLog, base_path: str) -> str:
f"<td><span class='pill ok'>{escape(_counts(newest[c]))}</span></td></tr>"
for c in components
)
tree_svg = _svg_tree(entries, str(latest.get("root_hash", "")), signing_backend)
slh_signed = ((latest.get("signatures") or {}).get("slh_dsa") or {}).get("status") == "signed"
head_label = "Ed25519 + SLH-DSA" if slh_signed else "Ed25519"
tree_svg = _svg_tree(entries, str(latest.get("root_hash", "")), signing_backend, head_label)
return f"""<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
@ -172,24 +217,100 @@ def render_docs(log: TransparencyLog, base_path: str) -> str:
· <a href="https://zkdefi.org/saymrwulf">code</a>
· <a href="https://zkdefi.com/">cv</a></p>
<h1>LTL the Lean Transparency Log</h1>
<p class="tagline"><strong>One sentence:</strong> a public, append-only Merkle
accumulator of <em>signed statements that the Lean&nbsp;4 formal proofs of specific
cryptographic Rust libraries, at specific git commits, machine-re-check with exactly
their documented assumptions</em> so that you can trust a proof result by checking
<strong>one signature and ~{max(1,(latest.get('tree_size') or 1).bit_length())} hashes in
milliseconds</strong>, instead of running a theorem prover for hours.</p>
<p><strong>Every known way of consuming formal verification fails on one of two
sides: either the cost stays with you (re-running a theorem prover), or the cost
disappears because you are asked to believe a label. This site holds the point in
between it makes machine-checked proof results as cheap to check as one
signature and a handful of hashes, without turning them into trust. Every claim
the operator ever makes is recorded here permanently, bound to his key, and
scoped to its exact assumptions: he can never be believed, only held
accountable.</strong></p>
<h2>The trust anchor pin this key</h2>
<p>Concretely: a proof assistant <a href="https://lean-lang.org">Lean&nbsp;4</a>, a
program that checks mathematical proofs mechanically has verified precise
statements about the code that <em>checks signatures</em>: in four widely deployed
<strong>Ed25519</strong> libraries, in an implementation of <strong>SLH-DSA</strong>
(FIPS&nbsp;205, the hash-based post-quantum signature standard), and in <strong>the
Merkle-tree machinery of this log itself</strong>. Each completed proof check is one
signed, numbered entry that can never be altered or removed {len(entries)} entries
so far, drawn live further down this page. How much of our work you re-verify is
your choice, from milliseconds to everything:</p>
<h2>Choose where you stand the trust ladder</h2>
<div class="steps">
<div class="card"><strong>Hold a copy of the history.</strong> anyone; one minute.<br>
You still trust: the content of every claim. You defeat: silent rewriting.<br>
You need: Python&nbsp;3 and the <code>openssl</code> command (preinstalled on most Linux and macOS systems).
<pre>git clone https://github.com/saymrwulf/lean-transparency-log &amp;&amp; cd lean-transparency-log &amp;&amp; python3 verify.py --all</pre>
<span class="muted">This fetches the log&rsquo;s public mirror a git repository holding every entry
and every signed head ever issued (a head is the signed root fingerprint of the tree at a
given size) and re-computes every hash and signature in it. A green
result means the history you now hold is internally consistent and signed. Keep the folder:
if the operator ever shows a different history to anyone else, your copy proves it. A log
that shows different histories to different people (a &ldquo;split view&rdquo;) survives only until
two holders compare.</span></div>
<div class="card"><strong>Check that a claim is real and binding.</strong> milliseconds.<br>
You still trust: that the operator&rsquo;s recorded observation is honest.<br>
You need: four small files from the tables below the two public keys, plus one library&rsquo;s
claim file (&ldquo;attestation&rdquo;) and its proof of inclusion (&ldquo;receipt&rdquo;).
<pre>pacta receipt-verify --attestation --receipt --log-public-key provider.ed25519.pub --slhdsa-public-key provider.slhdsa.pub</pre>
<span class="muted">Your machine checks the required Ed25519 signature, the second (post-quantum) signature
(needs OpenSSL&nbsp;&nbsp;3.5; drop the second flag to skip it), and
~{max(1,(latest.get('tree_size') or 1).bit_length())} hashes no proof assistant involved.
The <code>pacta</code> tool ships in the
<a href="https://github.com/saymrwulf/proof-aware-crypto-tooling-agent">pacta repository</a>
(<code>pip install .</code> from a clone); about forty lines of ordinary Python do the same
check, and the mirror&rsquo;s <code>verify.py</code> contains exactly that core. Afterwards the
claim which repository, which exact source version, which theorems, which assumptions
is bound to the operator&rsquo;s key inside a history he can neither rewrite nor deny.</span></div>
<div class="card"><strong>Judge by your own standards.</strong> minutes.<br>
Every entry records more than pass/fail: it lists the exact assumptions each proof rests on
(its <em>axiom cone</em>). So you may ignore the operator&rsquo;s verdicts entirely: write down
which assumptions <em>you</em> accept, and have your tooling compare list against list, name
by name. <code>pacta</code> automates the comparison; lecture&nbsp;11 of the Jupyter course (same
repository) teaches it step by step.
<span class="muted">Afterwards every verdict is your verdict the operator&rsquo;s labels can at
most veto, never grant. The section &ldquo;You hold the ruler&rdquo; below is this rung in full.</span></div>
<div class="card"><strong>Re-run the proof check yourself.</strong> about 30&nbsp;minutes per library.<br>
You still trust: that the published Lean statements mean what they say.<br>
You need: a Lean&nbsp;4 toolchain (free).
<span class="muted">Every attested repository ships one script, <code>verification/check.sh</code>.
Clone the repository at the exact source version recorded in its log entry and run the script:
the proof assistant re-checks every theorem on your machine and prints every assumption list.
The operator is now out of the loop entirely.</span></div>
<div class="card"><strong>Rebuild everything from source.</strong> weeks.<br>
What remains trusted: Lean&rsquo;s proof kernel, the Rust-to-Lean translation tools (Charon and
Aeneas), and your compiler the floor, which we name rather than hide.
<span class="muted">Every repository documents its full path: the pinned upstream Rust code,
the extraction script that regenerates the Lean definitions byte-for-byte, and the theorem
statements to read against the standards themselves (FIPS&nbsp;205, RFC&nbsp;9162, the curve
equations). Reproduce any part; compare with what is published. After this rung you no longer
need this site that is the point. There is no rung&nbsp;6: even here you trust a kernel, a
compiler, and your hardware.</span></div>
</div>
<h2>Two keys sign this log pin these keys</h2>
{_trust_anchor_html(log, metadata, base, mirror)}
<h2>The accumulator, live</h2>
<p>The log is a <strong>Merkle tree</strong>: every entry (&ldquo;leaf&rdquo;) is hashed, hashes pair
up level by level, and a single 32-byte root fingerprints the entire history; the operator signs
that root. Changing any past entry would change the root that is the tamper evidence. Each leaf
records one proof run: a batch of <strong>certificates</strong> one machine-checked theorem
each, together with its exact assumption list. This
picture is computed from the live log at page render the leaf hashes, nodes, root, and
signature are the real ones:</p>
{tree_svg}
<p class="legend">
<span><span class="sw" style="background:#e2f2e9;border:1px solid #1e7f4f"></span>verified attestation (all certificates proven, axiom cones boundary-exact)</span>
<span><span class="sw" style="background:#f4f4f6;border:1px solid #8a93a0"></span>historical audit-failure attestation kept forever; an append-only ledger does not erase its bad day</span>
<span><span class="sw" style="background:#e2f2e9;border:1px solid #1e7f4f"></span>verified attestation every certificate proven, every assumption list exactly as declared</span>
<span><span class="sw" style="background:#f4f4f6;border:1px solid #8a93a0"></span>historical audit-failure attestation kept forever; an append-only ledger does not erase its bad day (leaves&nbsp;03: an early audit round that failed; leaves&nbsp;47 re-attest the same four libraries cleanly)</span>
</p>
<p class="muted">Every box above is computed from the live log at page render leaf hashes,
internal nodes, the root, and the signature are the real ones. Before signing this
<p class="muted">The library that signs the log is itself an entry in the log what that entry proves is its <em>verify</em> path (no signing code is proven, here or anywhere) and it checks its own entry before signing. In detail: before signing this
root, the provider Merkle-verified its own signing library's leaf
(index {provenance.get('signing_library_leaf_index','?')},
certificates {escape(str(provenance.get('signing_library_certificates_proven','?')))})
@ -201,26 +322,30 @@ log id <code>{escape(str(metadata.get('log_id',''))[:16])}…</code>.</p>
<h2>What do I download? the three artifacts, unambiguously</h2>
<p>To benefit from the accumulator you need <strong>exactly three files</strong> per
library, plus optionally the whole mirror. Nothing else.</p>
library, plus optionally the post-quantum key
(<code>provider.slhdsa.pub</code>) and the whole mirror. Nothing else.</p>
<table>
<tr><th>#</th><th>Artifact</th><th>What it is</th><th>Where</th></tr>
<tr><td><b>1</b></td><td><code>provider.ed25519.pub</code></td>
<td><strong>The identity anchor.</strong> The provider's public key — the sole cryptographic
<td><strong>The identity anchor.</strong> The provider's public key — the required cryptographic
identity you pin. It authenticates the operator's statements; their truth rests on each leaf's
stated assumptions. Fetch it from BOTH independent locations and compare; the copies must be
identical.</td>
<td><a href="{base}/log-public-key">this site</a> · <a href="{mirror}/blob/main/provider.ed25519.pub">mirror</a></td></tr>
<tr><td><b>2</b></td><td><code>&lt;library&gt;.attestation.json</code></td>
<td><strong>The claim.</strong> Which repo, which exact git commit, which theorems,
which observed axiom cones, what machine protection signed by the provider.</td>
<td>table above, or <a href="{mirror}">mirror</a> <code>entries/</code></td></tr>
which observed axiom cones (the exact set of assumptions each proof ultimately rests on), what machine protection signed by the provider.</td>
<td>table below, or <a href="{mirror}">mirror</a> <code>entries/</code></td></tr>
<tr><td><b>3</b></td><td><code>&lt;library&gt;.receipt.json</code></td>
<td><strong>The proof of inclusion.</strong> Binds artifact&nbsp;2 into the signed tree:
leaf index, sibling hashes, the Signed Tree Head. ~25 lines of stdlib Python verify it.</td>
<td>table above, or <a href="{mirror}">mirror</a> <code>receipts/</code></td></tr>
<td><strong>The proof of inclusion.</strong> Binds the claim into the signed tree:
leaf index, sibling hashes, the Signed Tree Head (STH). About forty lines of ordinary
Python verify it; the mirror&rsquo;s <code>verify.py</code> contains exactly that core, wrapped in
fail-closed safety checks (stdlib hashing; signature checks shell out to the <code>openssl</code> binary).</td>
<td>table below, or <a href="{mirror}">mirror</a> <code>receipts/</code></td></tr>
<tr><td>+</td><td>the full mirror clone</td>
<td><strong>Maximal benefit: become a witness.</strong> Every leaf + every signed head
ever issued + <code>verify.py</code> (stdlib-only). <code>python3 verify.py --all</code>
<td><strong>Maximal benefit: hold the complete history yourself (rung&nbsp;1 of the ladder).</strong> Every leaf + every signed head
ever issued + <code>verify.py</code> (Python stdlib + the <code>openssl</code> binary for
signatures; fails closed without them). <code>python3 verify.py --all</code>
recomputes the entire tree and every historical head you then hold a retained view that can
later EXPOSE a conflicting head shown to someone else. (A single clone cannot by itself prove the
log never split its view toward another consumer; that requires comparing heads across
@ -229,31 +354,8 @@ consumers.)</td>
</table>
<h2>Attested libraries</h2>
<table><tr><th>component</th><th>artifact 2</th><th>artifact 3</th><th>status</th></tr>{rows}</table>
<table><tr><th>library</th><th>claim (attestation)</th><th>proof of inclusion (receipt)</th><th>certificates proven</th></tr>{rows}</table>
<h2>Three ways to use it</h2>
<div class="steps">
<div class="card"><strong>Quick check</strong> (any machine, milliseconds): download
artifacts 13, then<br>
<code>pacta receipt-verify --attestation --receipt --log-public-key provider.ed25519.pub</code>
<br><span class="muted">No Lean, no Rust, no account. Add <code>--sth-store pins.json</code> for split-view defense.</span></div>
<div class="card"><strong>Zero-install audit</strong>: <code>git clone {mirror} &amp;&amp; python3 verify.py --all</code>
<br><span class="muted">Standard-library Python only. You become a witness of the whole history.</span></div>
<div class="card"><strong>Autonomous agent</strong>: the <a href="https://github.com/saymrwulf/proof-aware-crypto-tooling-agent">pacta</a>
tool adds STH pinning, freshness policy, online refresh from this service, risk scoring
(R0R5) with policy-gated consequences, and optionally verifies every signature through
the proof-attested Ed25519 code path itself (<code>--require-verified-verifier</code>).</div>
</div>
<h2>API</h2>
<pre>GET {base}/v1/sth latest Signed Tree Head
GET {base}/v1/sth-history the published head history (witness material)
GET {base}/v1/sth-consistency?first=N consistency proof from your pinned size
GET {base}/v1/proof?component=NAME inclusion proof (artifact 3, freshly issued)
GET {base}/v1/attestation?component=NAME the claim (artifact 2)
GET {base}/v1/entries?start=N&amp;end=M raw leaves
GET {base}/v1/metadata log identity
GET {base}/healthz</pre>
<h2>What a verified inclusion means and what it does not</h2>
<div class="card"><span class="pill ok">means</span> The provider whose key you hold
@ -264,19 +366,22 @@ view.</div>
<div class="card"><span class="pill warn">does not mean</span> A verified binary. The
proofs cover Rust <em>source</em>; clone the attested commit (the commit id identifies the
committed git tree not external dependencies, toolchain downloads, or generated artifacts) and
build it yourself compiler and build are declared trusted base
until the reproducible-builds program (R5) lands. Every attestation carries its full
residual-risk list. Honesty about the boundary is the product.</div>
build it yourself compiler and build are declared trusted base (assumed, not proven)
until the reproducible-builds program lands. Every attestation carries its full
residual-risk list the enumerated assumptions inside its <code>attestation.json</code>. Honesty about the boundary is the product.</div>
<h2>You hold the ruler</h2>
<div class="card">The list of assumptions a certificate is <em>allowed</em> to rest on
is not something this site hands you at verification time it is a
<strong>requirements card</strong> that lives in <em>your</em> tooling, on
<strong>requirements card</strong> a short plain-text file listing, by name, the
assumptions you accept that lives in <em>your</em> tooling, on
<em>your</em> disk, and that you can read in five minutes or rewrite from first
principles: Lean's three foundational axioms, plus — for the signature tiers only —
named placeholders for SHA-512 and the wire format. Your tooling ignores this
principles: Lean's three foundational axioms, plus — for the signature tiers only (the top proof layers, where full signature verification is proven) —
named placeholders for SHA-512 (the hash function Ed25519 uses internally
the proofs treat it as an assumption, not as proven) and the byte-level wire
format. Your tooling ignores this
operator's pass/fail labels entirely and re-derives every verdict by comparing the
attestation's <em>observed</em> axiom list against <em>your</em> card, name by name.
attestation's <em>observed</em> axiom list (its cone) against <em>your</em> card, name by name.
The operator is trusted to copy down what the proof kernel printed never to
interpret it.</div>
<div class="card">A card you write yourself will match this log's supply
@ -286,26 +391,28 @@ shrunk until every remaining axiom justifies its existence. If your card is
negotiate the gap is itemized, never blurred, and you have three honest options:
accept a <em>named</em> line item, walk away, or prove the missing piece and enter it
into this same log. <strong>If your ruler is stricter than our supply, your ruler is
our roadmap.</strong> (The full walk-through is lecture&nbsp;11 in the
<a href="https://github.com/saymrwulf/proof-aware-crypto-tooling-agent">course</a>.)</div>
our roadmap.</strong> (The full walk-through is lecture&nbsp;11 of the Jupyter course in the
<a href="https://github.com/saymrwulf/proof-aware-crypto-tooling-agent">pacta repo</a>.)</div>
<h2>The paper</h2>
<div class="card"><a href="{base}/paper"><strong>Accountable Distribution of Machine-Checked
Correctness Evidence: A Transparency Model and the Lean Transparency Log</strong></a>
(PDF, 23 pages, v0.9) the trust decomposition (expensive verification produces an
observation; transparency makes the observation accountable; consumer-local policy decides
acceptance), collision-extracting soundness for inclusion and consistency, scheme-level
accountability GAMES with an explicit composition theorem (head authenticity, position
binding, history binding with a fully proved prefix-transport induction, context-scoped
fork evidence all discharged by named reductions), the policy boundary where
operator labels can veto but never grant acceptance, the live thirteen-leaf deployment
whose entry 13 attests the accumulator's own mechanized model, and the measured
model/deployment divergence (3,867 lied-size cases, every one accepted only by the
deployed verifier) reported as a result rather than hidden.
<span class="muted">Previous versions: <a href="{base}/paper/v0.2">v0.2</a> (19 pages, the
system report) · <a href="{base}/paper/v0.1">v0.1</a> (4 pages).</span></div>
(PDF, 25 pages · v0.15, August&nbsp;2026 · DOI
<a href="https://doi.org/10.5281/zenodo.22057482">10.5281/zenodo.22057482</a>).
The full design and its security analysis: what the log guarantees, stated as
precise games with proofs and what it deliberately does not guarantee, with
the same honesty discipline as this page, at referee depth. It also tells the
project&rsquo;s best war story: the mechanized model caught our own deployed
verifier omitting a single condition of RFC&nbsp;9162 invisible to ordinary
testing, 3,867 wrong acceptances across 73,573 adversarial cases, zero after
the one-line fix. If you read one thing, read the claim matrix at the end:
every promise, what establishes it, and what remains assumed.</div>
<p class="muted">Log heads are signed offline; this service is read-only and holds no
key material. Provider tooling, agent tooling, and the full course (12 Jupyter
lectures) live in the <a href="https://github.com/saymrwulf/proof-aware-crypto-tooling-agent">pacta repository</a>.</p>
key material. Provider tooling, agent tooling, and the full Jupyter course live in the
<a href="https://github.com/saymrwulf/proof-aware-crypto-tooling-agent">pacta repository</a>.
Software integrators: the machine interface behind every link on this page is described by the
OpenAPI document at <a href="{base}/openapi.json"><code>/openapi.json</code></a> the
<code>pacta</code> tool builds on it (head pinning, freshness policy, risk scoring R0R5).</p>
</body></html>"""

View file

@ -82,7 +82,7 @@ COURSE = {
- Perform a proof hygiene scan and explain why `sorry`, local axioms, and trivial theorem targets are dangerous.
- Explain how a third-party proof-checking provider changes the trusted base.
- Implement and verify RFC 9162-style Merkle inclusion and consistency proofs.
- Explain why Signed Tree Heads need accountable signatures, why Ed25519 is useful here, and why ML-DSA requires a real backend.
- Explain why Signed Tree Heads need accountable signatures, why Ed25519 is useful here, why ML-DSA requires a real backend, and how the shipped SLH-DSA co-signature differs from both.
- Design policy gates that convert verification evidence into consequences.
- Read R4 four-tier apex evidence, name its residual blockers, and write a research plan toward R5 production assurance.
"""
@ -1029,6 +1029,7 @@ COURSE = {
- `require-signatures ed25519`: verify Ed25519 and allow ML-DSA to be unavailable.
- `require-signatures both`: require Ed25519 and ML-DSA verified. If ML-DSA is unavailable, fail closed.
- `--slhdsa-public-key <pem>`: additionally verify the second (post-quantum) SLH-DSA co-signature on the head; heads before tree size 14 report `absent` (allowed), a present-but-wrong signature fails closed.
"""
),
code(
@ -1050,6 +1051,53 @@ COURSE = {
ML-DSA adds post-quantum robustness for the accumulator signature layer. But it must be a real signature, not an aspirational label. If a host lacks ML-DSA, the correct result is an explicit blocker.
"""
),
md(
"""
## The second signature that actually shipped: SLH-DSA
Since tree size 14, every head of the LIVE log carries a second,
deterministic **SLH-DSA-SHA2-128s** (FIPS 205) signature beside the
required Ed25519 one. This is not the ML-DSA slot above - it is a
hash-based scheme, and it was chosen because the estate has PROVEN
its verify path (eleven certificates, log leaf 18): the log
co-signs with the parameter set whose verification path it itself
attests. Three design facts worth internalizing:
1. Heads published before size 14 carry no co-signature, and
verifiers report them `ABSENT` rather than failing them - an
append-only log keeps the history of its own signature-scheme
upgrades.
2. The co-signature is deterministic on purpose: re-signing the
same payload is byte-comparable, so "same input, same
signature" becomes a diff you can run, not an assurance you
must trust.
3. Signing is still never proven - here, as everywhere in this
estate, certificates cover the VERIFY path only.
"""
),
code(
"""
# Runnable where OpenSSL >= 3.5 is present; honest skip otherwise.
import tempfile
from pathlib import Path
from pacta import slhdsa
tmp = Path(tempfile.mkdtemp(prefix="nb06-slhdsa-"))
try:
slhdsa.generate_slhdsa_keypair(tmp / "slh.key", tmp / "slh.pub")
except Exception as exc:
print("SLH-DSA unavailable on this host (OpenSSL >= 3.5 needed):", exc)
else:
payload = b"canonical STH payload bytes"
block = slhdsa.slh_dsa_signature_block(payload, tmp / "slh.key", tmp / "slh.pub")
ok, err = slhdsa.verify_payload_slhdsa(payload, block["signature_base64"], tmp / "slh.pub")
print("co-signature verifies:", ok, err or "")
block2 = slhdsa.slh_dsa_signature_block(payload, tmp / "slh.key", tmp / "slh.pub")
print("deterministic (byte-equal re-sign):",
block["signature_base64"] == block2["signature_base64"])
"""
),
md(
"""
## Two domains, two notebooks - by design
@ -1749,6 +1797,7 @@ COURSE = {
- Modify a claim card to R2 and show that `build-library` is refused.
- Explain why a denial artifact is useful for auditability.
- Design a policy where an agent requires `both` Ed25519 and ML-DSA signatures for production deployment but allows Ed25519-only in a local lab.
- Extend it: when should the agent also require the SLH-DSA co-signature, given that heads before tree size 14 legitimately lack it?
- Write a downstream Rust pseudo-code snippet that imports the generated capsule before enabling a code path.
"""
),
@ -1770,7 +1819,7 @@ COURSE = {
- State precisely which parts of the dogfood verifier are certificate-covered and which are its trusted base.
- Extract a raw Ed25519 key from an OpenSSL PEM by hand (napkin) and mechanically (real).
- Demonstrate backend dispatch and the fail-closed `--require-verified-verifier` policy.
- Defend the hybrid post-quantum posture: one proven-classical signature plus one required-but-honest ML-DSA slot.
- Defend the three-legged post-quantum posture: proven-classical Ed25519, the shipped SLH-DSA co-signature with its attested verify path, and a required-but-honest ML-DSA slot.
"""
),
md(
@ -1858,7 +1907,8 @@ COURSE = {
leaf, against the very tree it is about to sign. The verdict is
embedded in the signature block (`signing_provenance`: backend,
library commit, leaf index, `self_inclusion: verified`,
certificates 16/16). Lectures 6a/6b walk both sides of this.
certificates 44/44 - the signer's source family was re-attested at 44
certificates as leaf 13). Lectures 6a/6b walk both sides of this.
Honesty note unchanged: the library's VERIFY path is
certificate-covered; the signing path is declared trusted base -
but it is the attested artifact, not an un-attested third
@ -1866,12 +1916,13 @@ COURSE = {
## The post-quantum line, held honestly
The dogfood loop deliberately does NOT extend to ML-DSA. There is no formally verified ML-DSA implementation in this corpus, and pretending otherwise would poison the whole posture. The hybrid strategy is therefore asymmetric on purpose:
The posture has three legs now, and each is exactly as strong as it claims:
- **Ed25519 (classical): proven path.** The signature everyone can check today runs on certificate-covered code.
- **ML-DSA-65 (post-quantum): required, honest, unavailable-until-real.** The tree-head slot exists in every signed structure; `--require-signatures both` fails CLOSED on hosts without a real FIPS 204 backend; and when a real backend lands, the policy flips on without a schema change.
- **Ed25519 (classical): proven verify path, dogfooded.** The signature everyone can check today runs on certificate-covered code.
- **SLH-DSA-SHA2-128s (post-quantum): shipped and attested.** Since tree size 14 every live head carries a second, deterministic SLH-DSA co-signature. The estate proved the VERIFY path of a pinned Rust FIPS 205 implementation (eleven certificates) and appended that attestation as leaf 18 - so the co-signature uses exactly the parameter set the log itself attests. Consumers check it with `pacta receipt-verify ... --slhdsa-public-key provider.slhdsa.pub` or the mirror's `verify.py`. Signing remains unproven - verify paths only, always.
- **ML-DSA-65 (lattice PQ): required, honest, unavailable-until-real.** The tree-head slot exists in every signed structure; `--require-signatures both` fails CLOSED on hosts without a real FIPS 204 backend; when a real backend lands, the policy flips on without a schema change.
A migration strategy that records "we cannot do this yet" as a deployment blocker is strictly stronger than one that ships a placeholder. Blockers get fixed; placeholders get trusted.
A migration strategy that records "we cannot do this yet" as a deployment blocker is strictly stronger than one that ships a placeholder. Blockers get fixed; placeholders get trusted. And the SLH-DSA leg shows the endgame: a slot stops being aspirational the day its verify path enters the log.
"""
),
code(
@ -2310,6 +2361,23 @@ COURSE = {
trusted for after you do.
"""
),
md(
"""
## The human surface: see this wallet through the cockpit
Everything this notebook built programmatically has a read-only human console:
```
pacta wallet cockpit --demo # throwaway demo wallet, zero setup
pacta wallet cockpit --wallet DIR # the wallet you just sealed here
```
Open `/deck` for all six role stations live in parallel (the quorum bench you built is the
indigo pane; the ledger you hash-chained is re-verified on every page load), and `/manual`
for the lab-manual sessions that teach each role Session 4's tamper drill breaks a *copy*
of a ledger exactly like this notebook's and watches two independent surfaces catch it.
"""
),
]
),
"11_the_customers_eye_view.ipynb": notebook(

View file

@ -137,6 +137,7 @@ def build_parser() -> argparse.ArgumentParser:
receipt_verify.add_argument("--receipt", required=True)
receipt_verify.add_argument("--log-public-key", required=True)
receipt_verify.add_argument("--require-signatures", choices=["ed25519", "both"], default="ed25519")
receipt_verify.add_argument("--slhdsa-public-key", help="Also verify the second (post-quantum, SLH-DSA) head signature against this public key (OpenSSL >= 3.5; heads before tree size 14 report absent, not failed).")
receipt_verify.add_argument("--sth-store", help="Path to the local STH pin store (split-view/rollback defense).")
receipt_verify.add_argument("--consistency-proof", help="File with a hex consistency proof from the pinned tree size (provider: log-consistency).")
receipt_verify.add_argument("--max-sth-age-seconds", type=int, help="Reject signed tree heads older than this (freshness policy).")
@ -476,7 +477,8 @@ def cmd_score(args: argparse.Namespace) -> int:
def cmd_receipt_verify(args: argparse.Namespace) -> int:
attestation = load_attestation(args.attestation)
receipt = load_receipt(args.receipt)
result = verify_receipt(attestation, receipt, args.log_public_key, require_signatures=args.require_signatures)
result = verify_receipt(attestation, receipt, args.log_public_key, require_signatures=args.require_signatures,
slhdsa_public_key_path=args.slhdsa_public_key)
accountability_diagnostics = _log_accountability_checks(
receipt,
sth_store=args.sth_store,

View file

@ -28,6 +28,17 @@ class RepoConfig:
env_script: str | None = None
lean_project_dir: str | None = None
lean_guard: str | None = None
# Files the provider REPLAY does not compile (globs relative to the
# verification dir). Exists because the hardened forks ship audit
# INSTRUMENTS — kernel-gate modules that read other modules' object files
# via readModuleData and only run inside the button's own environment
# (its cwd, LEAN_PATH and compile order). The replay's generic
# topological compile breaks on them (register:
# replay-pipeline-lags-hardened-forks). The replay checks the CERTIFICATES;
# the deeper self-auditing gates are the repository's own and run there.
# Every exclusion actually applied is recorded in the attestation's replay
# block, so the leaf discloses what its replay did not compile.
replay_exclude: list[str] = field(default_factory=list)
@classmethod
def from_dict(cls, raw: dict[str, Any]) -> "RepoConfig":
@ -53,6 +64,7 @@ class RepoConfig:
env_script=raw.get("env_script"),
lean_project_dir=raw.get("lean_project_dir"),
lean_guard=raw.get("lean_guard"),
replay_exclude=[str(g) for g in (raw.get("replay_exclude") or [])],
)

View file

@ -12,7 +12,27 @@ from pathlib import Path
from typing import Any
DOGFOOD_ENV = "PACTA_DOGFOOD_VERIFIER"
DEFAULT_STATE_DIR = Path("dogfood") / "state"
REQUIRE_VERIFIED_ENV = "PACTA_REQUIRE_VERIFIED_SIGNER"
# Anchored to the PACKAGE, not to the caller's working directory.
#
# This was `Path("dogfood") / "state"` — a relative path, so it resolved against
# whatever directory the process happened to start in. The consequence was not a
# crash but something quieter and worse: run the provider from the repository
# root and it signs with the attested dalek build; run it from anywhere else and
# locate_verifier() finds nothing, signing falls back to OpenSSL, and the head
# records `signing_backend: openssl`. WHICH IMPLEMENTATION SIGNS THE
# TRANSPARENCY LOG WAS AN ACCIDENT OF THE CURRENT DIRECTORY.
#
# Found 2026-08-04 by re-signing the published head 13 as a reproducibility
# check: the byte comparison passed, but the backend came back `openssl` while
# the published head says `verified-dalek-serial`. Both produced identical bytes
# — Ed25519 is deterministic, so that is expected and is itself useful evidence
# — which is exactly why the substitution was invisible. A silent backend swap
# that changes no output is one nobody notices until the outputs differ.
#
# __file__ is <repo>/src/pacta/dogfood.py, so parents[2] is the repo root.
DEFAULT_STATE_DIR = Path(__file__).resolve().parents[2] / "dogfood" / "state"
BACKEND_VERIFIED = "verified-dalek-serial"
BACKEND_OPENSSL = "openssl"
@ -112,6 +132,21 @@ def locate_verifier(state_dir: str | Path | None = None) -> Path | None:
return path if path.exists() else None
def require_verified_signer() -> bool:
"""Whether a downgrade to OpenSSL is forbidden for this process.
Recording a downgrade truthfully, which this code already does, tells you
afterwards which implementation signed. It does not let you DECIDE which
one will. For an operation as consequential as signing a transparency-log
head, the choice should be stated up front and enforced, not discovered in
the provenance field once the head exists.
Off by default: every existing caller keeps the fall-back-and-record
behaviour. Set PACTA_REQUIRE_VERIFIED_SIGNER=1 and signing raises instead
of quietly substituting OpenSSL."""
return os.environ.get(REQUIRE_VERIFIED_ENV, "").strip().lower() in {"1", "true", "yes", "on"}
def load_provenance(binary_path: str | Path) -> dict[str, Any]:
sidecar = Path(binary_path).with_suffix(".provenance.json")
if sidecar.exists():

View file

@ -173,7 +173,7 @@ ESTATE_HTML = r'''<title>LTL estate map — repos, services, loops</title>
<div class="node" id="pasta"><h3 class="mono">pasta-pallas-verified</h3><div class="role">field layer proven · curve layer pending</div>
<div class="chips"><span class="chip warn">not attested</span></div></div>
<div class="node" id="fips"><h3 class="mono">fips205-slhdsa-verified</h3><div class="role">SLH-DSA-SHA2-128s verify path · skeleton 0 certificates</div>
<div class="chips"><span class="chip warn">campaign in progress</span><span class="chip warn">not attested</span></div></div>
<div class="chips"><span class="chip warn">11 certs · reviewer attest-with-conditions</span><span class="chip warn">not attested</span></div></div>
<div class="node" id="corpus"><h3 class="mono">ltl-accumulator-verified</h3><div class="role">61 certs · proofs about the log's own accumulator model</div>
<div class="chips"><span class="chip ok">attested · entry 13</span><span class="chip frz">frozen 172a1d0</span><span class="chip l2">loop 2</span></div></div>
</div>
@ -249,7 +249,7 @@ const RUNTIME = {
dalek:"static repo — proofs replay on demand", anza:"static repo — proofs replay on demand",
risc0:"static repo — proofs replay on demand", bet:"static repo — proofs replay on demand",
pasta:"static repo — open work, run manually", corpus:"frozen repo — replay on demand",
fips:"static repo — no process; campaign sessions are episodic operator-machine runs under lean-guard; check.sh exits non-green by design",
fips:"static repo — no process; campaign sessions are episodic operator-machine runs under lean-guard; check.sh GREEN with an 18-attack self-test",
provider:"SPLIT: the write side (check/append/publish) runs ON DEMAND on the operator machine, only during a ceremony; the read-only web face runs ALWAYS ON in the droplet container",
signer:"on demand — invoked only while signing during a ceremony; key offline otherwise",
conslib:"library — runs inside whichever consumer invokes it",
@ -276,10 +276,10 @@ const DOSSIER = {
srcBet:{lane:"Upstream inputs",mut:"frozen",facts:["Pinned clone of the Betrusted dalek fork.","xous-core and litex-boards sit alongside as platform context.","Input to extraction; never modified."]},
srcPasta:{lane:"Upstream inputs",mut:"frozen",facts:["Pinned clone of the Pasta curves crate.","Feeds pasta-pallas-verified; never modified."]},
srcFips205:{lane:"Upstream inputs",mut:"frozen",facts:["Verbatim snapshot of integritychain/fips205 — pure-Rust FIPS 205 / SLH-DSA (zero unsafe, no_std, const-generic).","Pinned at upstream 30bac08 (2025-09-01); snapshot head 5dca0db — the single deviation from verbatim is stripping upstream CI workflows, documented in that commit.","Aeneas-compat patches land HERE as transparent, individually-justified commits; nothing is proposed upstream (no affiliation)."]},
dalek:{lane:"Verified subjects",mut:"frozen",facts:["16 reviewed certificates: field, group law, scalars, signature apex (T1T4).","Attested in all three log generations; current leaf 8.","LOOP 1 anchor: the dogfood signer binary is built from this source — the log's heads are signed by code whose proofs are inside the log.","Attestation pins a commit; the branch only moves for docs."]},
anza:{lane:"Verified subjects",mut:"frozen",facts:["16 reviewed certificates; current leaf 9.","Same proof pyramid as dalek, rebuilt for the fork's code structure."]},
risc0:{lane:"Verified subjects",mut:"frozen",facts:["16 reviewed certificates; current leaf 10.","Differs from Betrusted's corpus by 27 changed proof lines (the paper's portability datum)."]},
bet:{lane:"Verified subjects",mut:"frozen",facts:["16 reviewed certificates; current leaf 11."]},
dalek:{lane:"Verified subjects",mut:"frozen",facts:["31 bound certificates (field, group law, scalars, signature apex T1T4) and 3022 inventoried constants; 16 of the certificates are the curated attested subset in the log.","Attested in all three log generations; current leaf 8.","LOOP 1 anchor: the dogfood signer binary is built from this source — the log's heads are signed by code whose proofs are inside the log.","Attestation pins a commit; the branch only moves for docs."]},
anza:{lane:"Verified subjects",mut:"frozen",facts:["31 bound certificates, 3022 inventoried constants; attested subset is 16; current leaf 9.","Same proof pyramid as dalek, rebuilt for the fork's code structure."]},
risc0:{lane:"Verified subjects",mut:"frozen",facts:["31 bound certificates, 3022 inventoried constants; attested subset is 16; current leaf 10.","Differs from Betrusted's corpus by 27 changed proof lines (the paper's portability datum)."]},
bet:{lane:"Verified subjects",mut:"frozen",facts:["31 bound certificates, 3022 inventoried constants; attested subset is 16; current leaf 11."]},
pasta:{lane:"Verified subjects",mut:"free",facts:["Field layer proven from own extraction; curve layer (group law + scalar mul) remains open work.","NOT attested — the log carries only the four Ed25519 forks + the corpus."]},
fips:{lane:"Verified subjects",mut:"free",facts:["CAMPAIGN IN PROGRESS — ZERO certificates: verification/check.sh exits non-green and says so; that script is the only source of the word «proven» for this repo.","Scope: the FIPS 205 verify path only (slh_verify → fors / hypertree → xmss → wots → chain), parameter set SLH-DSA-SHA2-128s; keygen and signing are trusted base, exactly as ed25519 signing was.","The six SHA-2 hash oracles are opaque external models (TRUSTED-BASE.md), kept outside every future certificate's dependency cone.","Gate-0 (2026-07-22): Charon clean; Aeneas translated the whole cone with exactly one obstruction class (the Hashers fn-pointer struct) — campaign phase 1 is the compat patch in fips205-source.","NOT attested — the log carries nothing from this campaign yet."]},
corpus:{lane:"Verified subjects",mut:"frozen",facts:["61 certificates over one boundary axiom (LTLAcc.sha256); 222-constant environment inventory; 15-gap honest ledger.","Mechanizes the archived report's §6: extractors, consistency binding, per-step pin safety.","LOOP 2 anchor: attested INTO the log as entry 13 — the log carries kernel-checked proofs about its own accumulator model.","Frozen at 172a1d0 (the attested commit); doc-only commits may move the branch.","Docs carry numbering notes: paper references are v0.2 numbering."]},
@ -418,3 +418,135 @@ window.addEventListener("resize",()=>requestAnimationFrame(draw));
requestAnimationFrame(draw);setTimeout(draw,150);
</script>
'''
# ─────────────────────────────────────────────────────────────────────────────
# MEASURED PROGRESS PANEL
#
# Everything above this line is hand-written prose. That is why, between
# 2026-07-22 and 2026-07-30, this page told the operator that the SLH-DSA
# campaign was "in progress" with a button "non-green by design" while it had
# eleven proven certificates and a green button, and that the ed25519 forks had
# "16 reviewed certificates" while they had 31 bound and 3022 inventoried. A
# page that asserts cannot notice it has gone out of date; only a page that
# measures can.
#
# So this panel renders ONLY what formal-verification-control's
# tools/estate-progress.py derived from the repositories, and it states three
# things a reader would otherwise have to assume: when it was measured, whether
# the repositories have moved since, and which parts of this page are measured
# at all. If there is no snapshot it renders that fact loudly rather than
# quietly rendering nothing.
# ─────────────────────────────────────────────────────────────────────────────
import json as _json
import os as _os
import subprocess as _sp
PROGRESS_JSON = _os.environ.get(
"PACTA_PROGRESS_JSON",
"/home/oho/GitClone/FormalVerification/formal-verification-control/ESTATE-PROGRESS.json")
ESTATE_ROOT = _os.environ.get(
"ESTATE_ROOT", "/home/oho/GitClone/Claude/FormalVerification")
def _live_head(repo: str):
try:
r = _sp.run(["git", "-C", _os.path.join(ESTATE_ROOT, repo),
"rev-parse", "--short", "HEAD"],
capture_output=True, text=True, timeout=5)
return r.stdout.strip() or None
except Exception:
return None
def _panel(cls: str, title: str, body: str) -> str:
return (f'<section class="mprog {cls}"><h2>{title}</h2>{body}</section>')
def progress_panel() -> str:
"""The measured half of this page. Never falls back to prose."""
style = """
<style>
.mprog{max-width:62rem;margin:1.2rem 1.4rem;padding:1rem 1.2rem;
border:1px solid var(--line);border-radius:8px;background:var(--panel)}
.mprog h2{margin:.1rem 0 .5rem;font-size:1rem}
.mprog.warn{background:var(--warn);border-color:#e0b877}
.mprog.bad{background:#fdecea;border-color:#e3a7a0}
.mprog table{border-collapse:collapse;width:100%;font-size:.86rem}
.mprog th,.mprog td{text-align:left;padding:.28rem .5rem;border-bottom:1px solid var(--line)}
.mprog .num{text-align:right;font-variant-numeric:tabular-nums}
.mprog .note{color:var(--ink2);font-size:.78rem;margin-top:.6rem}
.bar{height:.5rem;background:#eef1f5;border-radius:3px;overflow:hidden;min-width:7rem}
.bar>i{display:block;height:100%;background:var(--sub)}
</style>"""
if not _os.path.exists(PROGRESS_JSON):
return style + _panel("bad", "Progress: NOT MEASURED", f"""
<p>No snapshot at <code>{PROGRESS_JSON}</code>, so this page is showing
you <em>nothing</em> rather than something stale. That is deliberate:
the previous version of this page displayed hand-typed claims that were
eight days out of date, and looked exactly as confident as this one.</p>
<p>To populate it:
<code>formal-verification-control/tools/estate-progress.py --json</code></p>""")
try:
d = _json.load(open(PROGRESS_JSON))
except Exception as e:
return style + _panel("bad", "Progress: SNAPSHOT UNREADABLE", f"<p>{e}</p>")
moved = []
for repo, recorded in (d.get("repo_heads") or {}).items():
live = _live_head(repo)
if live and recorded and live != recorded:
moved.append((repo, recorded, live))
rows = []
for c in d["campaigns"]:
ax = c["axes"]
for axis, label in (("proof", "act one · proof"), ("attestation", "act two · attestation")):
if axis not in ax:
continue
a = ax[axis]
unm = (f' <span title="only a human or an outside party can establish these">'
f'+{a["unmeasurable"]} unmeasurable</span>' if a["unmeasurable"] else "")
rows.append(
f'<tr><td>{c["title"]}</td><td>{label}</td>'
f'<td class="num">{a["earned"]} / {a["identified"]}</td>'
f'<td><div class="bar"><i style="width:{a["pct"]:.0f}%"></i></div></td>'
f'<td class="num">{a["pct"]}%{unm}</td>'
f'<td>{c["reproduction_note"]}</td></tr>')
t = d["totals"]
head = (f'<p><strong>act one — proof {t["proof"]["pct"]}%</strong> · '
f'<strong>act two — attestation {t["attestation"]["pct"]}%</strong> '
f'<span class="note">(two numbers, never one: a single figure is what let the '
f'old metric report 100% for work nobody had attacked)</span></p>')
table = ('<table><tr><th>campaign</th><th>axis</th><th class="num">band-points</th>'
'<th></th><th class="num">verified</th><th>reproduction</th></tr>'
+ "".join(rows) + "</table>")
contra = ""
if d.get("contradictions"):
items = "".join(f"<li><code>{c['id']}</code>: {c['detail']}</li>"
for c in d["contradictions"])
contra = (f'<p><strong>The ledger contradicts the repositories.</strong> '
f'These numbers are not trustworthy until this list is empty:</p><ul>{items}</ul>')
note = (f'<p class="note">Measured {d["generated_at"]} by <code>{d["generator"]}</code>, '
f'from the repositories as they were at that moment. Nothing here is cached or '
f'carried forward. <strong>Everything on this page ABOVE this panel is '
f'hand-written prose</strong> and can be out of date; only this panel is derived.</p>')
if moved:
rowsm = "".join(f"<li><code>{r}</code>: measured at <code>{a}</code>, "
f"now <code>{b}</code></li>" for r, a, b in moved)
return style + _panel(
"warn", "Progress: MEASURED, BUT THE REPOSITORIES HAVE MOVED SINCE",
f'<p>{len(moved)} repository(ies) changed after this snapshot, so the figures '
f'below describe an earlier state:</p><ul>{rowsm}</ul>{head}{table}{contra}{note}'
f'<p class="note">Re-run <code>tools/estate-progress.py --json</code> to refresh.</p>')
return style + _panel("", "Progress — measured, not asserted",
head + table + contra + note)

View file

@ -92,12 +92,20 @@ def sign_payload_ed25519_detailed(payload: bytes, private_key_path: str | Path)
library) and falling back to OpenSSL. Returns (base64 signature, the
backend that actually signed) - the backend is recorded next to every
signature so the provenance is never silent."""
from .dogfood import BACKEND_OPENSSL, BACKEND_VERIFIED, locate_verifier, sign_payload_dogfood
from .dogfood import (BACKEND_OPENSSL, BACKEND_VERIFIED, REQUIRE_VERIFIED_ENV,
default_binary_path, locate_verifier, require_verified_signer,
sign_payload_dogfood)
binary = locate_verifier()
if binary is not None:
signature_bytes = sign_payload_dogfood(payload, private_key_path, binary)
return base64.b64encode(signature_bytes).decode("ascii"), BACKEND_VERIFIED
if require_verified_signer():
raise SignatureError(
f"{REQUIRE_VERIFIED_ENV} is set, so falling back to OpenSSL is refused, but the "
f"attested signer was not found at {default_binary_path()}. "
f"Build it, or point {'PACTA_DOGFOOD_VERIFIER'} at it, or unset "
f"{REQUIRE_VERIFIED_ENV} to accept the recorded downgrade.")
return _sign_payload_openssl(payload, private_key_path), BACKEND_OPENSSL

232
src/pacta/slhdsa.py Normal file
View file

@ -0,0 +1,232 @@
"""SLH-DSA-SHA2-128s signing and verification for transparency-log heads.
This module is the post-quantum signing path that did NOT exist before
2026-08-06 (register: pq-slot-names-unproven-algorithm). Scope discipline,
stated up front because the estate has measured what silence costs:
* The parameter set is LOCKED to SLH-DSA-SHA2-128s the only set the
eleven fips205 certificates cover. Every entry point asserts the key's
algorithm and refuses anything else rather than producing a signature
outside every proof the estate holds.
* Signing is DETERMINISTIC (operator decision 2026-08-06): FIPS 205's
optional deterministic variant, selected via OpenSSL's
`-pkeyopt deterministic:1`. Chosen so the byte-level reproducibility
check that caught a real defect on the Ed25519 side survives for this
algorithm too. The trade is documented: fault-attack hardening from
hedged signing is forgone, for a key that signs a public log.
* NOTHING here is Lean-proven. The certificates cover the VERIFY path of
the extracted model; signing and key generation are outside every proof
(fips205 TRUSTED-BASE item 2). Verification below can be cross-checked
against the proven-source binary (pacta-verify-slhdsa); signing cannot
be cross-checked against anything proven, and no field this module
emits claims otherwise.
"""
from __future__ import annotations
import base64
import hashlib
import os
import subprocess
import tempfile
from pathlib import Path
from typing import Any
SLH_SCHEME = "openssl-slh-dsa-sha2-128s"
SLH_STANDARD = "FIPS 205"
SLH_PARAMETER_SET = "SLH-DSA-SHA2-128s"
SLH_SIGNATURE_BYTES = 7856
SLH_PUBLIC_KEY_BYTES = 32
# Package-anchored, NOT cwd-relative. The Ed25519 twin of this constant was a
# relative path and which implementation signed the log became an accident of
# the launch directory (register: signer-backend-depends-on-cwd). parents[2]
# of src/pacta/slhdsa.py is the repository root.
_REPO_ROOT = Path(__file__).resolve().parents[2]
PROVEN_VERIFIER = (_REPO_ROOT / "dogfood" / "quorum" / "verify-slhdsa"
/ "target" / "release" / "pacta-verify-slhdsa")
SLHDSA_VERIFIER_ENV = "PACTA_SLHDSA_VERIFIER"
class SlhDsaError(RuntimeError):
pass
def _openssl() -> str:
import shutil
exe = shutil.which("openssl")
if not exe:
raise SlhDsaError("openssl binary not found; SLH-DSA operations unavailable")
return exe
def _assert_128s_key(key_path: str | Path, public: bool) -> None:
"""Refuse any key that is not SLH-DSA-SHA2-128s.
The check is on the PROPERTY (the algorithm OpenSSL reports for the key),
not on a filename. A signature under any other parameter set would sit
outside all eleven certificates while looking exactly like dogfood.
"""
args = [_openssl(), "pkey", "-in", str(key_path), "-noout", "-text"]
if public:
args.insert(2, "-pubin")
result = subprocess.run(args, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
raise SlhDsaError(f"cannot read key {key_path}: {(result.stderr or '').strip()[:120]}")
if SLH_PARAMETER_SET not in result.stdout:
first = (result.stdout.strip().splitlines() or ["<empty>"])[0]
raise SlhDsaError(
f"key {key_path} is not {SLH_PARAMETER_SET} (openssl reports: {first!r}). "
f"The certificates cover {SLH_PARAMETER_SET} only; refusing.")
def generate_slhdsa_keypair(private_key_path: str | Path, public_key_path: str | Path) -> None:
"""Generate an SLH-DSA-SHA2-128s key pair. Private key mode 0600.
Key generation is NOT covered by any certificate; this is OpenSSL's
generator, trusted base, and recorded as such wherever the key is used.
"""
openssl = _openssl()
private_path = Path(private_key_path)
public_path = Path(public_key_path)
private_path.parent.mkdir(parents=True, exist_ok=True)
subprocess.run([openssl, "genpkey", "-algorithm", SLH_PARAMETER_SET,
"-out", str(private_path)], check=True, timeout=60)
os.chmod(private_path, 0o600)
subprocess.run([openssl, "pkey", "-in", str(private_path), "-pubout",
"-out", str(public_path)], check=True, timeout=30)
_assert_128s_key(private_path, public=False)
_assert_128s_key(public_path, public=True)
def sign_payload_slhdsa(payload: bytes, private_key_path: str | Path) -> str:
"""Deterministically sign; returns base64. Same payload + key => same bytes."""
_assert_128s_key(private_key_path, public=False)
openssl = _openssl()
with tempfile.TemporaryDirectory(prefix="pacta-slhdsa-sign-") as tmp:
payload_path = Path(tmp) / "payload.bin"
signature_path = Path(tmp) / "payload.sig"
payload_path.write_bytes(payload)
completed = subprocess.run(
[openssl, "pkeyutl", "-sign", "-inkey", str(private_key_path), "-rawin",
"-pkeyopt", "deterministic:1",
"-in", str(payload_path), "-out", str(signature_path)],
check=False, capture_output=True, text=True, timeout=120)
if completed.returncode != 0:
raise SlhDsaError((completed.stderr or "slh-dsa signing failed").strip())
signature = signature_path.read_bytes()
if len(signature) != SLH_SIGNATURE_BYTES:
raise SlhDsaError(
f"signature is {len(signature)} bytes, expected {SLH_SIGNATURE_BYTES} "
f"for {SLH_PARAMETER_SET} — wrong parameter set slipped through?")
return base64.b64encode(signature).decode("ascii")
def verify_payload_slhdsa(payload: bytes, signature_base64: str,
public_key_path: str | Path) -> tuple[bool, str | None]:
"""Verify with OpenSSL. For the proven-source cross-check, see
verify_payload_slhdsa_proven callers wanting both run both."""
_assert_128s_key(public_key_path, public=True)
try:
signature = base64.b64decode(signature_base64)
except Exception as exc:
return False, f"signature_base64 undecodable: {exc}"
if len(signature) != SLH_SIGNATURE_BYTES:
return False, f"signature is {len(signature)} bytes, expected {SLH_SIGNATURE_BYTES}"
openssl = _openssl()
with tempfile.TemporaryDirectory(prefix="pacta-slhdsa-verify-") as tmp:
payload_path = Path(tmp) / "payload.bin"
signature_path = Path(tmp) / "payload.sig"
payload_path.write_bytes(payload)
signature_path.write_bytes(signature)
completed = subprocess.run(
[openssl, "pkeyutl", "-verify", "-pubin", "-inkey", str(public_key_path),
"-rawin", "-in", str(payload_path), "-sigfile", str(signature_path)],
capture_output=True, timeout=120)
if completed.returncode == 0:
return True, None
return False, "OpenSSL rejected the SLH-DSA signature"
def locate_proven_verifier() -> Path | None:
env = os.environ.get(SLHDSA_VERIFIER_ENV)
if env:
path = Path(env)
return path if path.exists() else None
return PROVEN_VERIFIER if PROVEN_VERIFIER.exists() else None
def raw_public_key(public_key_path: str | Path) -> bytes:
der = subprocess.run([_openssl(), "pkey", "-pubin", "-in", str(public_key_path),
"-outform", "DER"], capture_output=True, timeout=30).stdout
if len(der) < SLH_PUBLIC_KEY_BYTES:
raise SlhDsaError(f"cannot extract raw public key from {public_key_path}")
return der[-SLH_PUBLIC_KEY_BYTES:]
def verify_payload_slhdsa_proven(payload: bytes, signature_base64: str,
public_key_path: str | Path) -> tuple[bool, str | None]:
"""Verify with pacta-verify-slhdsa, built from the PINNED proven source.
This is the one place in the estate where a log signature is checked by
the implementation whose verify path the certificates actually cover.
Honest residue: the binary also assembles M' and does IO, which no
certificate reaches; and it is a compiled binary, while the proofs are
about the extracted model (the estate's standing R5 gap).
"""
binary = locate_proven_verifier()
if binary is None:
return False, ("proven verifier not built (dogfood/quorum/build-verify-slhdsa.sh); "
"refusing to report a proven-path verdict without it")
try:
signature = base64.b64decode(signature_base64)
except Exception as exc:
return False, f"signature_base64 undecodable: {exc}"
if len(signature) != SLH_SIGNATURE_BYTES:
return False, f"signature is {len(signature)} bytes, expected {SLH_SIGNATURE_BYTES}"
with tempfile.TemporaryDirectory(prefix="pacta-slhdsa-proven-") as tmp:
payload_path = Path(tmp) / "payload.bin"
payload_path.write_bytes(payload)
completed = subprocess.run(
[str(binary), raw_public_key(public_key_path).hex(), signature.hex(),
str(payload_path)], capture_output=True, text=True, timeout=120)
if completed.returncode == 0:
return True, None
if completed.returncode == 1:
return False, "proven verifier rejected the signature"
return False, f"proven verifier input error: {(completed.stderr or '').strip()[:120]}"
def public_key_fingerprint(public_key_path: str | Path) -> str:
return hashlib.sha256(Path(public_key_path).read_bytes()).hexdigest()
def slh_dsa_signature_block(payload: bytes, private_key_path: str | Path,
public_key_path: str | Path) -> dict[str, Any]:
"""The `signatures.slh_dsa` block for a signed tree head.
A SEPARATE block by operator decision 2026-08-06: the ml_dsa slot keeps
saying, truthfully, that ML-DSA was never configured; no algorithm is
swapped inside a field that names a different one.
"""
signature_base64 = sign_payload_slhdsa(payload, private_key_path)
return {
"scheme": SLH_SCHEME,
"standard": SLH_STANDARD,
"parameter_set": SLH_PARAMETER_SET,
"mode": "deterministic",
"status": "signed",
"signing_backend": "openssl", # honest: no proven signer exists, for any algorithm
"payload_digest_sha256": hashlib.sha256(payload).hexdigest(),
"signature_base64": signature_base64,
"public_key_fingerprint_sha256": public_key_fingerprint(public_key_path),
}
def slh_dsa_not_configured_block() -> dict[str, Any]:
return {
"scheme": SLH_SCHEME,
"standard": SLH_STANDARD,
"parameter_set": SLH_PARAMETER_SET,
"status": "not_configured",
"reason": "No SLH-DSA signing key was configured for this log.",
}

View file

@ -121,7 +121,12 @@ def verify_consistency(
fn >>= 1
sn >>= 1
return old_hash == old_root_hash and new_hash == new_root_hash
# RFC 9162 2.1.4.2 Step 7 requires the new-size navigation counter to reach
# zero: the consumed proof length must match the claimed tree sizes. Without
# it, distinct (false) old-size claims can navigate one proof to the same
# reconstructed roots, so a valid proof for one transition verifies under a
# lied size. Reconstructing both roots is necessary but not sufficient.
return old_hash == old_root_hash and new_hash == new_root_hash and sn == 0
def attestation_leaf(attestation: dict[str, Any]) -> dict[str, Any]:
@ -153,6 +158,8 @@ def make_signed_tree_head(
private_key_path: str | Path,
public_key_path: str | Path,
signing_provenance: dict[str, Any] | None = None,
slhdsa_private_key_path: str | Path | None = None,
slhdsa_public_key_path: str | Path | None = None,
) -> dict[str, Any]:
sth: dict[str, Any] = {
"schema_version": 1,
@ -165,6 +172,17 @@ def make_signed_tree_head(
}
payload = signed_tree_head_payload(sth)
signature_base64, signing_backend = sign_payload_ed25519_detailed(payload, private_key_path)
# slh_dsa is a SEPARATE block (operator decision 2026-08-06): ml_dsa keeps
# its truthful not-configured disclosure; no algorithm is swapped inside a
# field that names a different one. ADDITIVE: ed25519 remains the signature
# consumers must check; a head without an SLH-DSA key carries the honest
# not-configured slot, exactly as ml_dsa always has.
from .slhdsa import slh_dsa_not_configured_block, slh_dsa_signature_block
if slhdsa_private_key_path and slhdsa_public_key_path:
slh_block = slh_dsa_signature_block(payload, slhdsa_private_key_path, slhdsa_public_key_path)
else:
slh_block = slh_dsa_not_configured_block()
sth["signatures"] = {
"ed25519": {
"scheme": "openssl-ed25519",
@ -176,6 +194,7 @@ def make_signed_tree_head(
**({"signing_provenance": signing_provenance} if signing_provenance else {}),
},
"ml_dsa": detect_ml_dsa().to_signature_slot(),
"slh_dsa": slh_block,
}
return sth
@ -237,6 +256,7 @@ def verify_receipt(
receipt: dict[str, Any],
log_public_key_path: str | Path,
require_signatures: str = "ed25519",
slhdsa_public_key_path: str | Path | None = None,
) -> ReceiptVerificationResult:
diagnostics: list[str] = []
if receipt.get("type") != RECEIPT_TYPE:
@ -247,6 +267,29 @@ def verify_receipt(
sth = receipt.get("sth") or {}
sth_ok, sth_diagnostics, statuses = verify_signed_tree_head(sth, log_public_key_path, require_signatures=require_signatures)
diagnostics.extend(sth_diagnostics)
if slhdsa_public_key_path is not None:
# The additive post-quantum co-signature (heads from tree size 14
# on). Absent on older heads is NOT a failure - an append-only log
# keeps its history; a present-but-bad signature fails closed.
from .slhdsa import verify_payload_slhdsa
slh = (sth.get("signatures") or {}).get("slh_dsa") or {}
if str(slh.get("status") or "absent") == "signed":
try:
slh_ok, slh_error = verify_payload_slhdsa(
signed_tree_head_payload(sth),
str(slh.get("signature_base64") or ""),
slhdsa_public_key_path,
)
except Exception as exc: # old OpenSSL, unreadable key: fail closed
slh_ok, slh_error = False, f"SLH-DSA verification unavailable: {exc}"
if slh_ok:
statuses["slh_dsa"] = "verified"
else:
statuses["slh_dsa"] = "failed"
diagnostics.append(f"SLH-DSA head co-signature did not verify: {slh_error}")
else:
statuses["slh_dsa"] = "absent"
try:
tree_size = int(receipt.get("tree_size"))
leaf_index = int(receipt.get("leaf_index"))

View file

@ -1070,8 +1070,11 @@ def make_handler(wallet_dir: Path):
elif route == "/manual":
page("lab manual", "/manual", render_manual())
elif route == "/estate":
from .estateview import ESTATE_HTML
self._send(ESTATE_HTML + _ESTATE_BACK_CHIP)
from .estateview import ESTATE_HTML, progress_panel
# The map is hand-written prose; the panel is derived from the
# repositories. Serving them together, in that order, is what
# stops a reader mistaking the first for the second.
self._send(ESTATE_HTML + progress_panel() + _ESTATE_BACK_CHIP)
elif route.startswith("/station/"):
station_id = route.removeprefix("/station/")
station = STATION_BY_ID.get(station_id)

View file

@ -1,5 +1,5 @@
"""Historical regression pin from the archived v0.2 system report
(hosted at /paper/v0.2), which cited these exact differential-testing
(retrievable from paper/ git history; retired from the site 2026-08-15), which cited these exact differential-testing
counts (164,479 inclusion; 164,224 consistency) for its recursive forms
against the deployed iterative RFC 9162 verifiers over these families.
The CURRENT paper makes no extensional-equality claim: it cites the
@ -141,3 +141,82 @@ def test_recursive_consistency_equals_deployed_exhaustive():
assert verify_consistency(m, n, r0, r1, P)
assert _paper_cons(m, n, r0, r1, P)
assert total == 164_224, total # the count cited in the paper
# --- independent faithful RFC 9162 2.1.4.2 verifier, incl. Step-7 sn==0 -----
# A THIRD oracle, structurally distinct from the recursive _paper_cons model,
# so the harness below is three-way (deployed / recursive model / RFC loop).
def _rfc_cons(first, second, fh, sh, path):
if first == 0:
return True
if first > second:
return False
if first == second:
return fh == sh and not path
if not path:
return False
p = ([fh] + list(path)) if (first & (first - 1)) == 0 else list(path)
fn, sn = first - 1, second - 1
while fn & 1:
fn >>= 1
sn >>= 1
fr = sr = p[0]
for c in p[1:]:
if sn == 0:
return False
if (fn & 1) or (fn == sn):
fr = _hnode(c, fr)
sr = _hnode(c, sr)
if not (fn & 1):
while (fn & 1) == 0 and fn != 0:
fn >>= 1
sn >>= 1
else:
sr = _hnode(sr, c)
fn >>= 1
sn >>= 1
return fr == fh and sr == sh and sn == 0
def test_consistency_lied_size_three_way_agreement():
"""Regression for the RFC 9162 Step-7 terminal check (sn==0).
The deployed iterative verify_consistency, the recursive ConsRec model
(_paper_cons), and an independent faithful RFC 9162 2.1.4.2 transliteration
(_rfc_cons) must agree on BOTH the honest family AND the lied-size family.
The lied-size dimension is the one the historical differential test above
never varied; it is exactly where the pre-fix verifier (which omitted RFC
Step 7's terminal sn==0) accepted semantically-false size claims. Flagship:
a valid 2->3 proof presented as 1->3 with the size-2 root. This test FAILS
against the pre-fix verifier and passes once sn==0 is restored.
"""
# Flagship named example: rejected by all three verifiers.
L = [f"leaf-{i}".encode() for i in range(3)]
P23 = consistency_proof(L, 2)
R2, R3 = merkle_root(L[:2]), merkle_root(L)
assert verify_consistency(1, 3, R2, R3, P23) is False
assert _paper_cons(1, 3, R2, R3, P23) is False
assert _rfc_cons(1, 3, R2, R3, P23) is False
N = 48
honest_total = lied_total = 0
for n in range(1, N + 1):
data = [f"leaf-{i}".encode() for i in range(n)]
rn = merkle_root(data)
for m in range(1, n + 1):
P = consistency_proof(data, m)
rm = merkle_root(data[:m])
assert (verify_consistency(m, n, rm, rn, P)
== _paper_cons(m, n, rm, rn, P)
== _rfc_cons(m, n, rm, rn, P) is True), ("honest", n, m)
honest_total += 1
for mlie in range(1, n):
if mlie == m:
continue
dep = verify_consistency(mlie, n, rm, rn, P)
mod = _paper_cons(mlie, n, rm, rn, P)
rfc = _rfc_cons(mlie, n, rm, rn, P)
assert dep == mod == rfc, ("lied", n, m, mlie, dep, mod, rfc)
lied_total += 1
assert honest_total == N * (N + 1) // 2
assert lied_total == sum((n - 1) ** 2 for n in range(1, N + 1))

126
tests/test_slhdsa.py Normal file
View file

@ -0,0 +1,126 @@
"""SLH-DSA signing path: deterministic, parameter-locked, two-verifier checked.
Every test uses THROWAWAY keys generated into tmp_path. No test touches the
provider state directory or any long-lived key.
"""
from __future__ import annotations
import base64
import json
def _keypair(tmp_path):
from pacta.slhdsa import generate_slhdsa_keypair
priv, pub = tmp_path / "t.key", tmp_path / "t.pub"
generate_slhdsa_keypair(priv, pub)
return priv, pub
def test_keygen_shape_and_permissions(tmp_path):
priv, pub = _keypair(tmp_path)
assert priv.exists() and pub.exists()
assert (priv.stat().st_mode & 0o777) == 0o600
def test_deterministic_signing_reproduces_bytes(tmp_path):
"""Operator decision 2026-08-06: same payload + key => identical bytes.
This is the property the Ed25519 reproducibility check relies on, and the
reason the deterministic variant was chosen over the FIPS 205 default."""
from pacta.slhdsa import sign_payload_slhdsa
priv, _pub = _keypair(tmp_path)
payload = b"the same head payload"
assert sign_payload_slhdsa(payload, priv) == sign_payload_slhdsa(payload, priv)
def test_sign_verify_roundtrip_both_verifiers(tmp_path):
from pacta.slhdsa import (locate_proven_verifier, sign_payload_slhdsa,
verify_payload_slhdsa, verify_payload_slhdsa_proven)
priv, pub = _keypair(tmp_path)
payload = b"a transparency log head payload"
sig = sign_payload_slhdsa(payload, priv)
ok, err = verify_payload_slhdsa(payload, sig, pub)
assert ok, err
if locate_proven_verifier() is None:
import pytest
pytest.skip("pacta-verify-slhdsa not built on this host")
ok, err = verify_payload_slhdsa_proven(payload, sig, pub)
assert ok, f"proven-source verifier disagrees with OpenSSL: {err}"
def test_corruption_rejected_by_both(tmp_path):
from pacta.slhdsa import (locate_proven_verifier, sign_payload_slhdsa,
verify_payload_slhdsa, verify_payload_slhdsa_proven)
priv, pub = _keypair(tmp_path)
payload = b"payload"
raw = bytearray(base64.b64decode(sign_payload_slhdsa(payload, priv)))
raw[0] ^= 1
bad = base64.b64encode(bytes(raw)).decode()
ok, _ = verify_payload_slhdsa(payload, bad, pub)
assert not ok
if locate_proven_verifier() is not None:
ok, _ = verify_payload_slhdsa_proven(payload, bad, pub)
assert not ok
def test_parameter_set_lock_refuses_foreign_key(tmp_path):
"""An Ed25519 key must be refused outright — a signature under any other
algorithm would look like dogfood while sitting outside every proof."""
import pytest
from pacta.signing import generate_ed25519_keypair
from pacta.slhdsa import SlhDsaError, sign_payload_slhdsa
priv, pub = tmp_path / "ed.key", tmp_path / "ed.pub"
generate_ed25519_keypair(priv, pub)
with pytest.raises(SlhDsaError):
sign_payload_slhdsa(b"x", priv)
def test_head_carries_separate_slh_dsa_block(tmp_path):
"""make_signed_tree_head with an SLH-DSA key: both signatures verify, the
ml_dsa slot is UNTOUCHED, and without a key the slot degrades honestly."""
from pacta.signing import generate_ed25519_keypair, verify_payload_ed25519_detailed
from pacta.slhdsa import verify_payload_slhdsa
from pacta.transparency import make_signed_tree_head, signed_tree_head_payload
ed_priv, ed_pub = tmp_path / "ed.key", tmp_path / "ed.pub"
generate_ed25519_keypair(ed_priv, ed_pub)
slh_priv, slh_pub = _keypair(tmp_path)
sth = make_signed_tree_head("00" * 32, 19, "11" * 32, "2026-08-06T00:00:00Z",
ed_priv, ed_pub,
slhdsa_private_key_path=slh_priv,
slhdsa_public_key_path=slh_pub)
payload = signed_tree_head_payload(sth)
ed = sth["signatures"]["ed25519"]
ok, err, _backend = verify_payload_ed25519_detailed(payload, ed["signature_base64"], ed_pub)
assert ok, err
slh = sth["signatures"]["slh_dsa"]
assert slh["status"] == "signed"
assert slh["parameter_set"] == "SLH-DSA-SHA2-128s"
assert slh["mode"] == "deterministic"
ok, err = verify_payload_slhdsa(payload, slh["signature_base64"], slh_pub)
assert ok, err
# ml_dsa stays exactly the honest disclosure it always was
assert sth["signatures"]["ml_dsa"]["status"] in {"not_configured", "unavailable"}
assert "signature_base64" not in sth["signatures"]["ml_dsa"]
# additive: no key => honest not-configured slot, never an error
bare = make_signed_tree_head("00" * 32, 19, "11" * 32, "2026-08-06T00:00:00Z",
ed_priv, ed_pub)
assert bare["signatures"]["slh_dsa"]["status"] == "not_configured"
# and the payload is unchanged by the slh_dsa presence: signatures are
# outside the signed bytes for BOTH algorithms
assert signed_tree_head_payload(bare) == payload
def test_block_is_json_serialisable(tmp_path):
from pacta.slhdsa import slh_dsa_signature_block
priv, pub = _keypair(tmp_path)
block = slh_dsa_signature_block(b"payload", priv, pub)
json.dumps(block)
assert set(block) >= {"scheme", "standard", "parameter_set", "mode", "status",
"payload_digest_sha256", "signature_base64",
"public_key_fingerprint_sha256"}

View file

@ -145,3 +145,42 @@ def test_requiring_both_signatures_fails_without_ml_dsa_backend(tmp_path):
assert not result.accepted
assert result.signatures["ed25519"] == "verified"
assert result.signatures["ml_dsa"] != "verified"
def test_receipt_verify_checks_slhdsa_cosignature(tmp_path):
# Rung 2 of the site promises both head signatures are checkable;
# this binds the promise to the tool (operator-caught 2026-08-16).
import pytest
from pacta import slhdsa
try:
slhdsa.generate_slhdsa_keypair(tmp_path / "slh.key", tmp_path / "slh.pub")
except Exception:
pytest.skip("OpenSSL without SLH-DSA support on this host")
attestation, private_key, public_key = _signed_attestation(tmp_path)
from pacta.yamlio import dump_data
dump_data(attestation, tmp_path / "attestation.yaml")
log = TransparencyLog(tmp_path / "log")
log.init("local-test-provider", public_key)
receipt = log.append_attestation(
tmp_path / "attestation.yaml", private_key, public_key,
receipt_out=tmp_path / "receipt.yaml",
slhdsa_private_key_path=tmp_path / "slh.key",
slhdsa_public_key_path=tmp_path / "slh.pub",
)
result = verify_receipt(attestation, receipt, public_key,
slhdsa_public_key_path=tmp_path / "slh.pub")
assert result.accepted, result.diagnostics
assert result.signatures["slh_dsa"] == "verified"
tampered = __import__("copy").deepcopy(receipt)
sig = tampered["sth"]["signatures"]["slh_dsa"]["signature_base64"]
import base64 as _b64
raw = bytearray(_b64.b64decode(sig)); raw[0] ^= 0xFF
tampered["sth"]["signatures"]["slh_dsa"]["signature_base64"] = _b64.b64encode(bytes(raw)).decode()
bad = verify_receipt(attestation, tampered, public_key,
slhdsa_public_key_path=tmp_path / "slh.pub")
assert not bad.accepted
assert bad.signatures["slh_dsa"] == "failed"

View file

@ -63,16 +63,16 @@ def test_web_endpoints_and_online_proof_roundtrip(tmp_path):
with urllib.request.urlopen(base + "/paper", timeout=10) as r:
assert r.headers["Content-Type"] == "application/pdf"
assert r.read(5) == b"%PDF-"
# the prior paper version stays reachable for citability
for path in ("/paper/ltl.pdf", "/paper/v0.1"):
with urllib.request.urlopen(base + path, timeout=10) as r:
assert r.read(5) == b"%PDF-", path
# the removed pseudonymous version must be gone entirely
try:
urllib.request.urlopen(base + "/paper/v0.0", timeout=10)
raise AssertionError("expected 404 for removed /paper/v0.0")
except urllib.error.HTTPError as exc:
assert exc.code == 404
with urllib.request.urlopen(base + "/paper/ltl.pdf", timeout=10) as r:
assert r.read(5) == b"%PDF-"
# superseded drafts were retired from the site 2026-08-15 (git
# history retains them); every old variant route must 404
for gone in ("/paper/v0.0", "/paper/v0.1", "/paper/v0.2"):
try:
urllib.request.urlopen(base + gone, timeout=10)
raise AssertionError(f"expected 404 for retired {gone}")
except urllib.error.HTTPError as exc:
assert exc.code == 404, gone
# the site's copy of the trust anchor (TOFU: two independent locations)
with urllib.request.urlopen(base + "/log-public-key", timeout=10) as r:
assert r.read() == (tmp_path / "k.pub").read_bytes()
@ -80,7 +80,7 @@ def test_web_endpoints_and_online_proof_roundtrip(tmp_path):
with urllib.request.urlopen(base + "/docs", timeout=10) as r:
page = r.read().decode()
assert "BEGIN PUBLIC KEY" in page
assert "pin this key" in page.lower()
assert "pin these keys" in page.lower()
# operator-dropped documents: served by bare name, absent from the
# endpoint index, traversal-safe
site = tmp_path / "log" / "site"
@ -159,3 +159,97 @@ def test_standalone_verify_py_runs(tmp_path):
assert result.returncode == 0, result.stdout + result.stderr
# hardened verifier: full mode (signatures verified) must report exactly this
assert "RESULT: OK [full]" in result.stdout
def test_webdocs_source_carries_no_stale_paper_claims():
# Regression for the 2026-08-16 operator finding: the paper card said
# "23 pages" and the July-snapshot card survived a silently failed
# replace (an invisible NBSP defeated the pattern). Guard the shipped
# STRINGS, not just version markers.
from pathlib import Path
source = Path(__file__).resolve().parents[1] / "provider" / "src" / "pacta_provider" / "webdocs.py"
text = source.read_text(encoding="utf-8")
for stale in ("snapshot", "thirteen leaves", "16 July", "16&nbsp;July",
"16\xa0July", "v0.9", "v0.10", "23 pages"):
assert stale not in text, f"stale marker {stale!r} in webdocs"
# printed commands must work as printed: curl needs the scheme
# (http->https redirect yields empty output), the clone one-liner
# needs the cd into the cloned directory
assert "curl -s ltl.zkdefi.org" not in text
assert "cd lean-transparency-log" in text
# first-use glosses the page promised: STH and axiom cones
assert "Signed Tree Head (STH)" in text
assert "axiom cones (the exact set of assumptions" in text
def test_svg_tree_boxes_never_overlap_or_spill():
# Regression for 2026-08-16: fixed-width leaf boxes shingled once the
# log outgrew the 8-leaf design, and a fixed head box let its caption
# spill. Render the tree at several sizes and assert geometry.
import re
from pacta_provider.webdocs import _svg_tree
class _E:
def __init__(self, i):
self.leaf_hash = f"{i:02x}" * 32
self.leaf = {"attestation": {"subject": {"component": "betrusted-ed25519-verified"},
"certificates": [{"status": "proven", "axiom_status": "clean"}]}}
for n in (8, 19, 33):
svg = _svg_tree([_E(i) for i in range(n)], "ab" * 32, "verified-dalek-serial")
rects = [(float(m.group(1)), float(m.group(2)), float(m.group(3)))
for m in re.finditer(r'<rect x="([-0-9.]+)" y="([0-9.]+)" width="([0-9.]+)"', svg)]
leaf_y = max(y for _, y, _ in rects)
leaves = sorted((x, w) for x, y, w in rects if y == leaf_y)
assert len(leaves) == n
for (x1, w1), (x2, _w2) in zip(leaves, leaves[1:]):
assert x1 + w1 <= x2 + 0.01, f"leaf boxes overlap at n={n}"
# head caption must fit its box: longest line estimated at 5.3px/char
head = re.search(r'<rect x="[-0-9.]+" y="[0-9.]+" width="([0-9.]+)" height="46"', svg)
title = re.search(r'font-weight="bold">([^<]+)</text>', svg).group(1)
assert len(title) * 7.0 <= float(head.group(1)), "head title spills"
def test_openapi_document_served_and_valid():
# The machine interface is published the industry-standard way
# (operator order 2026-08-16: no endpoint box on the human page).
import json as _json
from pacta_provider.web import _openapi_document
doc = _openapi_document("")
assert doc["openapi"].startswith("3.")
assert "/v1/sth" in doc["paths"] and "/log-public-key" in doc["paths"]
_json.dumps(doc) # serializable
def test_head_requests_answer_like_get_without_body(tmp_path):
# Link checkers and mail/chat unfurlers probe with HEAD; a 501 made
# /paper look broken to them (found 2026-08-22 while verifying what
# the paper link serves).
import http.client
import shutil
_make_log(tmp_path)
shutil.copy2(tmp_path / "k.pub", tmp_path / "log" / "provider.ed25519.pub")
server = serve(str(tmp_path / "log"), port=0)
port = server.server_address[1]
import threading
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
for route, ctype in [("/", "text/html"), ("/v1/sth", "application/json"),
("/log-public-key", "text/plain")]:
conn.request("HEAD", route)
r = conn.getresponse()
body = r.read()
assert r.status == 200, (route, r.status)
assert ctype in r.getheader("Content-Type", ""), route
assert body == b"", (route, len(body))
assert int(r.getheader("Content-Length", "0")) > 0, route
finally:
server.shutdown()