mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-03 19:53:43 +00:00
Compare commits
49 commits
vulnerable
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1086a3ba02 | |||
| 31a9dee10a | |||
| 7e9e557187 | |||
| 7cc5982af3 | |||
| 57ac2095c0 | |||
| 03de38eaac | |||
| 469a962825 | |||
| ca2a59f8a7 | |||
| 7a6499c452 | |||
| 3b13fdbfde | |||
| 8ab1f85556 | |||
| 4b45ece97b | |||
| 6fcaaaee42 | |||
| c5268ef46c | |||
| bcbf929045 | |||
| fdfe217d57 | |||
| 42c244374a | |||
| 6c65a53775 | |||
| 6b16eee940 | |||
| 43d7f00930 | |||
| 51a094182a | |||
| 2517d79f6e | |||
| 24809f8e4d | |||
| b497b00c73 | |||
| 5bea4c85cb | |||
| 4e1cb19e06 | |||
| f54e47778b | |||
| 810d6f47f1 | |||
| 46a3094216 | |||
| 4f9e750eeb | |||
| 0e700841c6 | |||
| 8af6667756 | |||
| 03c9dabf36 | |||
| 840e3fba2c | |||
| be282ab724 | |||
| 6c587b572b | |||
| 993cfb85d4 | |||
| 0d83dc3ec1 | |||
| b232265264 | |||
| 962c0fdd41 | |||
| d28a5c080e | |||
| ab629f34d8 | |||
| 3a7a56c4e5 | |||
| a03662438a | |||
| 16040b79f5 | |||
| 5e35a533e1 | |||
| cd3b1bc921 | |||
| f1a55721e5 | |||
| ddbb5a4fd8 |
50 changed files with 3335 additions and 3041 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
44
ESTATE.md
44
ESTATE.md
|
|
@ -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 · R0–R5"]
|
||||
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 8–11, generations at 0–7) | 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 13–16 (2026-08-08); earlier 16-cert generations at leaves 8–11, first generations at 0–7 | 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
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -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
|
||||
13–16 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
81
docs/security-2026-07-23-consistency-terminal-check.md
Normal file
81
docs/security-2026-07-23-consistency-terminal-check.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
102
dogfood/quorum/build-verify-slhdsa.sh
Executable file
102
dogfood/quorum/build-verify-slhdsa.sh
Executable 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"
|
||||
13
dogfood/quorum/verify-slhdsa/Cargo.toml.template
Normal file
13
dogfood/quorum/verify-slhdsa/Cargo.toml.template
Normal 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]
|
||||
49
dogfood/quorum/verify-slhdsa/expose-mono.patch
Normal file
49
dogfood/quorum/verify-slhdsa/expose-mono.patch
Normal 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)
|
||||
+}
|
||||
101
dogfood/quorum/verify-slhdsa/src/main.rs
Normal file
101
dogfood/quorum/verify-slhdsa/src/main.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
8
llms.txt
8
llms.txt
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 00–09 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
1
paper/.gitignore
vendored
|
|
@ -2,3 +2,4 @@
|
|||
*.log
|
||||
*.out
|
||||
*.toc
|
||||
rendered-pages/
|
||||
|
|
|
|||
31
paper/README.md
Normal file
31
paper/README.md
Normal 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.3–v0.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
84
paper/check-paper.sh
Executable 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.
|
|
@ -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.
1344
paper/ltl-v0.2.tex
1344
paper/ltl-v0.2.tex
File diff suppressed because it is too large
Load diff
BIN
paper/ltl.pdf
BIN
paper/ltl.pdf
Binary file not shown.
401
paper/ltl.tex
401
paper/ltl.tex
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 §4–5; 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 1–3 + 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.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 2 — SLH-DSA (FIPS 205), post-quantum.</strong> Heads from tree size 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 ≥ 3.5). The kind of code that verifies such signatures is itself
|
||||
a proof subject of this log (entry 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>
|
||||
· raw: <a href="{base}/log-slhdsa-public-key"><code>{base or ''}/log-slhdsa-public-key</code></a>
|
||||
· 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 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>
|
||||
· raw: <a href="{base}/log-public-key"><code>{base or ''}/log-public-key</code></a>
|
||||
· <code>curl -s ltl.zkdefi.org/log-public-key</code></p>
|
||||
</div>"""
|
||||
|
||||
· <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 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 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 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 3 and the <code>openssl</code> command (preinstalled on most Linux and macOS systems).
|
||||
<pre>git clone https://github.com/saymrwulf/lean-transparency-log && cd lean-transparency-log && python3 verify.py --all</pre>
|
||||
<span class="muted">This fetches the log’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 “split view”) 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’s recorded observation is honest.<br>
|
||||
You need: four small files from the tables below — the two public keys, plus one library’s
|
||||
claim file (“attestation”) and its proof of inclusion (“receipt”).
|
||||
<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 ≥ 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’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’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’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 11 of the Jupyter course (same
|
||||
repository) teaches it step by step.
|
||||
<span class="muted">Afterwards every verdict is your verdict — the operator’s labels can at
|
||||
most veto, never grant. The section “You hold the ruler” below is this rung in full.</span></div>
|
||||
|
||||
<div class="card"><strong>Re-run the proof check yourself.</strong> — about 30 minutes per library.<br>
|
||||
You still trust: that the published Lean statements mean what they say.<br>
|
||||
You need: a Lean 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’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 205, RFC 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 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 (“leaf”) 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 0–3: an early audit round that failed; leaves 4–7 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><library>.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><library>.receipt.json</code></td>
|
||||
<td><strong>The proof of inclusion.</strong> Binds artifact 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’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 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 1–3, 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} && 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
|
||||
(R0–R5) 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&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 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 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 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’s best war story: the mechanized model caught our own deployed
|
||||
verifier omitting a single condition of RFC 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 R0–R5).</p>
|
||||
</body></html>"""
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 [])],
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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 (T1–T4).","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 T1–T4) 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)
|
||||
|
|
|
|||
|
|
@ -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
232
src/pacta/slhdsa.py
Normal 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.",
|
||||
}
|
||||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
126
tests/test_slhdsa.py
Normal 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"}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue