mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-03 19:53:43 +00:00
Three synchronized faces of one log - transport orthogonal to trust: - PUBLISHED GIT MIRROR: log-publish exports the public face (one file per leaf so git history mirrors log history; the FULL STH history as the witness channel; per-component attestations + receipts; the provider public key; a standalone stdlib-only verify.py and customer README). Live at github.com/saymrwulf/lean-transparency-log (genesis: 8 leaves incl. the honest failed-run entries, dogfood-signed head). - ONLINE SERVICE (pacta_provider serve): read-only, zero-dependency HTTP with CT-style endpoints under a base path for zkdefi.org/lean-transparency-log - /v1/sth, /v1/sth-history, /v1/sth-consistency?first=N, /v1/proof, /v1/attestation, /v1/entries, /v1/metadata, /healthz - plus self-contained customer documentation at /docs (current state, attested components, API, the verify- without-trusting-this-site path, and the means/does-NOT-mean boundary). The process never loads private keys: heads are signed offline; a compromised server can withhold or replay (pinning + freshness detect both) but never forge. STH history now recorded append-only by the provider (with a backfill head signed for the existing log). - AGENT ONLINE CLIENT: pacta log-fetch (download evidence; explicitly UNVERIFIED until receipt-verify runs - transport is not trust) and pacta sth-refresh (fetch head, verify signature, advance the pin via an online consistency proof from the pinned size; fail closed). - WITNESSES: pacta witness-audit over a clone of the published mirror recomputes every prefix root from the public leaves and checks every historical head + signature - no consistency proofs needed when the leaves are public. Tampering one published entry trips both the leaf-hash check and the prefix-root check (tested). verify.py gives customers the same audit with zero installation. - DEPLOY.md: the complete server-session checklist for zkdefi.org - reconstruct the servable log FROM the published mirror (the server stays in witness trust-position), hardened systemd unit, nginx/Caddy path routing, Forgejo mirror setup, the provider->world update cycle, and remote smoke tests. Validated end-to-end on the REAL log: all 10 endpoints, online-fetched proof re-verified locally through the dogfood verifier with pinning, online pin refresh, publish + witness audit green, tamper caught, standalone verify.py green in the published clone. 54/54 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
90 lines
3.6 KiB
Python
90 lines
3.6 KiB
Python
"""Witness audit over the PUBLISHED log repository.
|
|
|
|
The git-published log (entries/ + sth-history.jsonl + provider key) turns
|
|
every cloner into a witness: since all leaves are public, a witness does
|
|
not need consistency proofs at all - it recomputes the root of every
|
|
prefix directly and checks every historical Signed Tree Head against its
|
|
prefix root and its signature. Two different clones agreeing on the git
|
|
history and both passing this audit have PROOF the provider never
|
|
equivocated within it.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from .signing import canonical_json
|
|
from .transparency import leaf_hash, merkle_root, verify_signed_tree_head
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class WitnessReport:
|
|
ok: bool
|
|
tree_size: int
|
|
heads_checked: int
|
|
problems: list[str] = field(default_factory=list)
|
|
notes: list[str] = field(default_factory=list)
|
|
|
|
|
|
def audit_published_log(
|
|
published_dir: str | Path,
|
|
log_public_key_path: str | Path | None = None,
|
|
) -> WitnessReport:
|
|
root_dir = Path(published_dir)
|
|
problems: list[str] = []
|
|
notes: list[str] = []
|
|
|
|
entry_files = sorted((root_dir / "entries").glob("[0-9]*.json"))
|
|
leaves: list[bytes] = []
|
|
for position, path in enumerate(entry_files):
|
|
record = json.loads(path.read_text(encoding="utf-8"))
|
|
if int(record.get("index", -1)) != position:
|
|
problems.append(f"{path.name}: index {record.get('index')} at position {position} (gap or reorder).")
|
|
leaf_bytes = canonical_json(record["leaf"])
|
|
if leaf_hash(leaf_bytes).hex() != record.get("leaf_hash"):
|
|
problems.append(f"{path.name}: leaf_hash does not match the leaf content.")
|
|
leaves.append(leaf_bytes)
|
|
|
|
history_path = root_dir / "sth-history.jsonl"
|
|
heads = [
|
|
json.loads(line)
|
|
for line in history_path.read_text(encoding="utf-8").splitlines()
|
|
if line.strip()
|
|
] if history_path.exists() else []
|
|
if not heads:
|
|
notes.append("No sth-history.jsonl; only the latest head can be checked.")
|
|
latest_path = root_dir / "latest-sth.json"
|
|
if latest_path.exists():
|
|
heads = [json.loads(latest_path.read_text(encoding="utf-8"))]
|
|
|
|
previous_size = -1
|
|
for position, head in enumerate(heads):
|
|
size = int(head.get("tree_size", -1))
|
|
if size < previous_size:
|
|
problems.append(f"STH #{position}: tree_size {size} SHRANK from {previous_size} (rollback in history).")
|
|
previous_size = max(previous_size, size)
|
|
if size < 0 or size > len(leaves):
|
|
problems.append(f"STH #{position}: tree_size {size} outside published entries ({len(leaves)}).")
|
|
continue
|
|
expected_root = merkle_root(leaves[:size]).hex()
|
|
if head.get("root_hash") != expected_root:
|
|
problems.append(
|
|
f"STH #{position} (size {size}): signed root {str(head.get('root_hash'))[:16]}… does not match "
|
|
f"the recomputed prefix root {expected_root[:16]}… - EQUIVOCATION or tampered entries."
|
|
)
|
|
if log_public_key_path is not None:
|
|
ok, diagnostics, _statuses = verify_signed_tree_head(head, log_public_key_path)
|
|
if not ok:
|
|
problems.append(f"STH #{position} (size {size}): signature check failed: {'; '.join(diagnostics)}")
|
|
|
|
if log_public_key_path is None:
|
|
notes.append("No public key supplied; structural audit only (signatures unchecked).")
|
|
return WitnessReport(
|
|
ok=not problems,
|
|
tree_size=len(leaves),
|
|
heads_checked=len(heads),
|
|
problems=problems,
|
|
notes=notes,
|
|
)
|