diff --git a/provider/src/pacta_provider/web.py b/provider/src/pacta_provider/web.py index 1c388c6..b1b2266 100644 --- a/provider/src/pacta_provider/web.py +++ b/provider/src/pacta_provider/web.py @@ -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": "Additive 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 {} @@ -78,6 +111,8 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(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": @@ -153,6 +188,7 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf 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", diff --git a/provider/src/pacta_provider/webdocs.py b/provider/src/pacta_provider/webdocs.py index 38c7ec4..df6ac4a 100644 --- a/provider/src/pacta_provider/webdocs.py +++ b/provider/src/pacta_provider/webdocs.py @@ -150,11 +150,11 @@ def _trust_anchor_html(log: TransparencyLog, metadata: dict[str, Any], base: str slh_pem = escape(slh_path.read_text(encoding="utf-8").strip()) slh_fp = _h.sha256(slh_path.read_bytes()).hexdigest() slh_block = f"""
-

Second, additive anchor — post-quantum. Heads from -tree 14 on additionally carry a deterministic SLH-DSA-SHA2-128s (FIPS 205) -signature over the same payload. The Ed25519 signature above remains the one every consumer must -check; this one is checked where tooling allows (OpenSSL ≥ 3.5). Its verify path is the -proof subject of leaf 18.

+

Key 2 — SLH-DSA-SHA2-128s (FIPS 205), additive +post-quantum. 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 (leaf 18).

{slh_pem}

SHA-256 fingerprint {slh_fp}  ·  raw: {base or ''}/log-slhdsa-public-key @@ -162,13 +162,14 @@ proof subject of leaf 18.

else: slh_block = "" return f"""
-

This key is the required cryptographic identity anchor — the one every consumer must check: it -authenticates that these statements were made by the operator (the same party the artifacts call “the provider”). 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 (one signed entry of the tree below). Every tree head and attestation is -signature-checked against this key. -Pin it (save your own copy; from then on trust only what checks against that copy), and compare this copy byte-for-byte with the independently hosted -mirror copy; they must be identical. The first fetch is trust-on-first-use; the two-host byte-comparison is what bounds it.

+

Two keys sign everything in this log. Neither makes a +claim true; they prove a claim comes from this operator, unchanged. Save your own copy +of both — that is called pinning: 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.)

+

Key 1 — Ed25519, required. Every signed head and every attestation must +verify against it.

{pem}

SHA-256 fingerprint {escape(fingerprint)}  ·  raw: {base or ''}/log-public-key @@ -221,93 +222,91 @@ def render_docs(log: TransparencyLog, base_path: str) -> str: · code · cv

LTL — the Lean Transparency Log

-

What happened here, in plain terms: we took real cryptographic -code — four production Ed25519 signature libraries and the verification path of -SLH-DSA (FIPS 205), the post-quantum signature standard — and machine-checked -mathematical proofs about it with the Lean 4 -proof assistant. Re-checking those proofs yourself takes a toolchain and about half -an hour of compute per library. This site is the shortcut that does not ask for -blind trust: a public, tamper-evident ledger of signed statements about every proof -check we ran — so you decide how much of our work you re-verify, from a millisecond -signature check to redoing everything.

+

This site is a public notary for machine-checked proofs about cryptographic +software. A proof assistant — Lean 4, a program +that checks mathematical proofs mechanically — has verified precise statements about the code +that checks signatures: in four widely deployed Ed25519 libraries, in an +implementation of SLH-DSA (FIPS 205, the hash-based post-quantum signature +standard), and in the Merkle-tree machinery of this log itself. Every completed +proof check is recorded here as a signed, numbered entry that can never be altered or removed — +{len(entries)} entries so far, drawn live further down this page.

-

The same thing, in one precise sentence: a public, append-only Merkle -accumulator (a hash tree that only ever grows) of signed statements that the Lean 4 formal proofs of specific -cryptographic Rust libraries, at specific git commits, re-check by machine with exactly -their documented assumptions — so that you can trust a proof result by checking -one required signature (Ed25519) and ~{max(1,(latest.get('tree_size') or 1).bit_length())} hashes in -milliseconds, instead of running a theorem prover for hours.

+

Re-checking such proofs yourself takes a toolchain and real compute time. This log gives you +cheaper positions to stand on: in milliseconds you can verify that the operator is permanently +bound to every claim he ever made — and you can escalate, step by step, up to redoing +everything yourself. The ladder below lists every position, cheapest first.

Choose where you stand — the trust ladder

-

Every rung below is a legitimate place to stand. Each states what you still take -on trust, what you do, what it costs, and what you know afterwards. Climb one rung -at a time — the whole service is built so that you can.

-
“I just want the history held honest.” — anyone, one minute.
-Still trusted: everything — but lying becomes attributable. +
Hold a copy of the history. — anyone; one minute.
+You still trust: the content of every claim. You defeat: silent rewriting.
+You need: Python 3 and the openssl command (preinstalled on most Linux and macOS systems).
git clone https://github.com/saymrwulf/lean-transparency-log && cd lean-transparency-log && python3 verify.py --all
-Python plus the system openssl binary; fails closed without it. -Afterwards you hold every leaf and every Signed Tree Head (STH) ever issued. If the -operator ever shows anyone a conflicting history, your copy exposes it — you are a -witness. A split view (the operator showing different histories to different -consumers) survives only until two witnesses compare.
+This fetches the log’s public mirror — a git repository holding every entry +and every signed head ever issued — 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.
-
“I trust the operator’s reports; bind him to them.” — milliseconds.
-Still trusted: that the recorded observations are honest. -Download the three artifacts (key, claim, inclusion proof — table below), then: +
Check that a claim is real and binding. — milliseconds.
+You still trust: that the operator’s recorded observation is honest.
+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”).
pacta receipt-verify --attestation … --receipt … --log-public-key provider.ed25519.pub
-The pacta CLI ships in the +Your machine checks one Ed25519 signature and +~{max(1,(latest.get('tree_size') or 1).bit_length())} hashes — no proof assistant involved. +The pacta tool ships in the pacta repository -(pip install . from a clone); a one-page Python core (the paper’s -Appendix C) does the same check without it. Add --sth-store pins.json to -remember every head you accept. Afterwards the exact claim — repository, commit, -theorems, assumptions — is cryptographically pinned to the operator’s key inside an -append-only history: he can never rewrite or deny it. What he observed, you -have not yet checked.
+(pip install . from a clone); about forty lines of ordinary Python do the same +check, and the mirror’s verify.py 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.
-
“I accept his observations — not his judgment.” — minutes; the rung most people miss.
-Still trusted: the recorded axiom lists; not the operator’s pass/fail labels. -Compare each attestation’s recorded assumption cones against a requirements card -you write yourself — pacta automates the comparison, and lecture 11 of the -Jupyter course -walks through it. -Afterwards every verdict is your verdict, re-derived from your -own ruler; operator labels can veto but never grant acceptance (details in -“You hold the ruler” below).
+
Judge by your own standards. — minutes.
+Every entry records more than pass/fail: it lists the exact assumptions each proof rests on +(its axiom cone). So you may ignore the operator’s verdicts entirely: write down +which assumptions you accept, and have your tooling compare list against list, name +by name. pacta automates the comparison; lecture 11 of the Jupyter course (same +repository) teaches it step by step. +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.
-
“I don’t trust his observations — I’ll run the proofs myself.” — about 30 minutes per library.
-Still trusted: the published Lean sources and the extraction that produced them; -not the operator’s execution. Clone the attested repository at its pinned -commit and press its check button (verification/check.sh) with a Lean 4 -toolchain: the kernel re-checks every certificate on your machine and the axiom -audit prints the exact assumption cones. -Afterwards the theorem prover accepted on your hardware — -the operator is out of the loop entirely.
+
Re-run the proof check yourself. — about 30 minutes per library.
+You still trust: that the published Lean statements mean what they say.
+You need: a Lean 4 toolchain (free). +Every attested repository ships one script, verification/check.sh. +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.
-
“I trust none of it — I’ll rebuild the whole path.” — weeks.
-Still trusted: Lean’s kernel, the extraction tools, and your compiler — the floor, -which we name rather than hide. Pin the upstream Rust source yourself, extract it to -Lean with Charon/Aeneas (every repository ships its extract.sh, pinned -toolchain versions, and byte-pinned generated models for comparison), re-read the -theorem statements against FIPS 205 / RFC 9162 / the curve equations, and re-prove -or audit each certificate. -Afterwards you have reproduced the estate and no longer need us — -which is the point. There is no rung above this one: even here you trust a kernel, a -compiler, and your silicon. Anyone offering zero trust is selling something.
+
Rebuild everything from source. — weeks.
+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. +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.
-

The trust anchors — pin these keys (one required, one additive)

+

Two keys sign this log — pin these keys

{_trust_anchor_html(log, metadata, base, mirror)}

The accumulator, live

+

The log is a Merkle tree: 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. This +picture is computed from the live log at page render — the leaf hashes, nodes, root, and +signature are the real ones:

{tree_svg}

verified attestation (all certificates proven, axiom cones boundary-exact) 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)

-

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. The library that signs the log is itself an entry in the log — what that entry proves is its verify path (no signing code is proven, here or anywhere) — and it checks its own entry before signing. In detail: before signing this +

The library that signs the log is itself an entry in the log — what that entry proves is its verify 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','?')))}) @@ -335,7 +334,9 @@ which observed axiom cones (the exact set of assumptions each proof ultimately r table below, or mirror entries/ 3<library>.receipt.json The proof of inclusion. Binds artifact 2 into the signed tree: -leaf index, sibling hashes, the Signed Tree Head (STH). A one-page Python core verifies it — printed as Appendix C of the paper; the shipped verify.py wraps that core with full fail-closed binding checks (stdlib hashing; signature checks shell out to the openssl binary). +leaf index, sibling hashes, the Signed Tree Head (STH). About forty lines of ordinary +Python verify it; the mirror’s verify.py contains exactly that core, wrapped in +fail-closed safety checks (stdlib hashing; signature checks shell out to the openssl binary). table below, or mirror receipts/ +the full mirror clone Maximal benefit: become a witness. Every leaf + every signed head @@ -387,24 +388,6 @@ into this same log. If your ruler is stricter than our supply, your rule our roadmap. (The full walk-through is lecture 11 of the Jupyter course in the pacta repo.)

-

For your tooling — the raw API

-

Humans never need these directly; every link on this page already uses them. They -exist so that your software — a CI job, an autonomous agent, a package -resolver — can consume the log without scraping HTML. The pacta CLI -builds on them: STH pinning, freshness policy, risk scoring (R0–R5, six named -residual-risk classes) with policy-gated consequences, and optionally ---require-verified-verifier, which checks every signature through the -proof-attested Ed25519 code path itself.

-
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
- -

The paper

Accountable Distribution of Machine-Checked Correctness Evidence: A Transparency Model and the Lean Transparency Log @@ -428,9 +411,13 @@ Ed25519 tiers.
describes this deployment as it runs — nineteen leaves, dual-signed heads, the post-quantum verify path as leaf 18 with its own certificate appendix. The log is append-only and keeps growing past any paper revision; every number the paper states -stays checkable against the retained history: python3 verify.py --all -re-verifies all of it, paper-era and after, from a clone of the mirror. +stays checkable against the retained history: the mirror clone from rung 1 of the +ladder re-verifies all of it, paper-era and after.

Log heads are signed offline; this service is read-only and holds no -key material. Provider tooling, agent tooling, and the full Jupyter course live in the pacta repository.

+key material. Provider tooling, agent tooling, and the full Jupyter course live in the +pacta repository. +Software integrators: the machine interface behind every link on this page is described by the +OpenAPI document at /openapi.json — the +pacta tool builds on it (head pinning, freshness policy, risk scoring R0–R5).

""" diff --git a/tests/test_web_and_witness.py b/tests/test_web_and_witness.py index 34ea920..5426467 100644 --- a/tests/test_web_and_witness.py +++ b/tests/test_web_and_witness.py @@ -210,3 +210,16 @@ def test_svg_tree_boxes_never_overlap_or_spill(): head = re.search(r'([^<]+)', 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