Serve the paper at /paper; witness mirror must be independently operated

- web.py: /paper (and /paper/ltl.pdf) serve the committed PDF, loaded
  once at startup from the repo checkout; listed in the 404 endpoint
  index; covered by the web roundtrip test
- docs page: 'The paper' card linking the PDF
- DEPLOY.md: the second witness mirror belongs on a host the operator
  does NOT control (a self-hosted mirror adds no equivocation defense)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrwulf 2026-07-06 19:44:06 +02:00
parent 47dfb61a7f
commit 9acb078844
4 changed files with 32 additions and 7 deletions

View file

@ -168,13 +168,15 @@ server {
Check: `https://ltl.zkdefi.org/docs` renders the customer
documentation; `/v1/sth` returns the dogfood-signed head.
## 4. Second mirror (any Forgejo/Gitea/GitLab you operate)
## 4. Second mirror (an independently-operated host — not yours)
Create a periodic pull-mirror of
`https://github.com/saymrwulf/lean-transparency-log` on a second,
independently-operated git host. The published repo is the witness
channel; two independent mirrors mean split-view lies must fool two
infrastructures at once — exactly the point.
`https://github.com/saymrwulf/lean-transparency-log` on a second git
host **operated by someone else** (e.g. Codeberg). The published repo is
the witness channel; two independent mirrors mean split-view lies must
fool two infrastructures at once — exactly the point. A mirror on
infrastructure the log operator also controls adds convenience, not
witness value: the operator could equivocate consistently on both.
## 4b. Key hygiene (non-negotiable)

View file

@ -12,6 +12,7 @@ from __future__ import annotations
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from urllib.parse import parse_qs, urlparse
@ -20,7 +21,7 @@ from .transparency_log import TransparencyLog
API_VERSION = "v1"
def make_handler(log: TransparencyLog, base_path: str, docs_html: str):
def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf: bytes | None = None):
base = "/" + base_path.strip("/") if base_path.strip("/") else ""
class Handler(BaseHTTPRequestHandler):
@ -43,6 +44,16 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str):
if route in ("/", "/docs"):
self._send_html(docs_html)
elif route in ("/paper", "/paper/ltl.pdf"):
if paper_pdf is None:
self._send(404, {"error": "paper not available on this deployment"})
return
self.send_response(200)
self.send_header("Content-Type", "application/pdf")
self.send_header("Content-Disposition", 'inline; filename="ltl.pdf"')
self.send_header("Content-Length", str(len(paper_pdf)))
self.end_headers()
self.wfile.write(paper_pdf)
elif route == "/healthz":
self._send(200, {"ok": True, "tree_size": len(log.entries())})
elif route == f"/{API_VERSION}/metadata":
@ -112,6 +123,7 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str):
"error": "unknown endpoint",
"endpoints": [
f"{base}/docs",
f"{base}/paper",
f"{base}/healthz",
f"{base}/{API_VERSION}/metadata",
f"{base}/{API_VERSION}/sth",
@ -161,5 +173,7 @@ def serve(
from .webdocs import render_docs
docs_html = render_docs(log, base_path)
handler = make_handler(log, base_path, docs_html)
paper_path = Path(__file__).resolve().parents[3] / "paper" / "ltl.pdf"
paper_pdf = paper_path.read_bytes() if paper_path.is_file() else None
handler = make_handler(log, base_path, docs_html, paper_pdf)
return ThreadingHTTPServer((host, port), handler)

View file

@ -218,6 +218,12 @@ the content hash) and build it yourself — compiler and build are declared trus
until the reproducible-builds program (R5) lands. Every attestation carries its full
residual-risk list. Honesty about the boundary is the product.</div>
<h2>The paper</h2>
<div class="card"><a href="{base}/paper"><strong>LTL: the Lean Transparency Log</strong></a>
(PDF, 4 pages) the design in full: the trust model (observations, never verdicts),
the self-certifying signature, the deployment with its retained failure leaves, and an
exact account of what a verified receipt does and does not establish.</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>

View file

@ -56,6 +56,9 @@ def test_web_endpoints_and_online_proof_roundtrip(tmp_path):
assert consistency["from_tree_size"] == 2 and consistency["proof"]
history = get("/v1/sth-history")["sth_history"]
assert len(history) == 3 # one head per append
with urllib.request.urlopen(base + "/paper", timeout=10) as r:
assert r.headers["Content-Type"] == "application/pdf"
assert r.read(5) == b"%PDF-"
finally:
server.shutdown()