verify.py fail-closed + full receipt bindings; README drops the 'honesty'/'first' overclaim (review round 10)

GPT-5.6 live-site review + Fable live verification:
- verify.py §4: was fail-OPEN — no openssl ⇒ signatures 'skipped' ⇒
  still 'OK'. Now fail-CLOSED (exit 2 unless --structural-only, which is
  explicitly labelled a reduced check). --receipt was Merkle-path-only;
  now verifies the receipt's STH signature, key fingerprint, log_id,
  tree_size agreement, leaf_hash-vs-entry, and history membership before
  the inclusion proof. --all now also checks latest-sth == final history
  head and size == leaf count, and constant log_id. Adversarially tested:
  fail-closed exit 2; forged unsigned-root receipt REJECTED (was the
  'inclusion VALID' hole); honest receipt + full run still exit 0.
- README §8: I had written 'the first deployed transparency log to carry
  proofs of its own honesty' this session — an overclaim (the corpus
  proves properties of the accumulator MODEL, not operator honesty).
  Now: 'kernel-checked proofs about the accumulator model underlying its
  inclusion/consistency reasoning', 'first' softened to 'unaware of a
  precedent'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrwulf 2026-07-16 23:46:28 +02:00
parent dea415a60c
commit 4e5c39b93c
2 changed files with 109 additions and 26 deletions

View file

@ -6,11 +6,12 @@ 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)**
the log also attests **its own accumulator machinery** — a kernel-checked
mechanization of the log's own security analysis, making this the first
deployed transparency log to carry proofs of its own honesty as one of
its own entries (subject
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). Current head: tree size 13, root
scoped to the mechanized model — it does not prove operator honesty,
signing, or execution provenance). Current head: tree size 13, root
`3488a2d0…`.
Layout:

126
verify.py
View file

@ -1,18 +1,29 @@
#!/usr/bin/env python3
"""Standalone verifier for the published Lean Transparency Log.
Python 3 standard library ONLY - no pacta, no pip. Verifies, from the
Pure Python 3 standard library for hashing and structure; Ed25519
signature checking shells out to the `openssl` binary. Verifies, from the
files in this repository alone:
1. every entry's leaf hash,
2. every historical Signed Tree Head against the recomputed prefix root
(this is the witness check: a split view or tampered entry fails here),
3. every STH Ed25519 signature (via the openssl binary, if available),
4. any receipt's inclusion proof (--receipt FILE).
(a split view or tampered entry fails here),
3. every STH Ed25519 signature,
4. a receipt as a FULL transparency receipt (--receipt FILE): its STH
signature, key fingerprint, log id, tree-size agreement, that its
leaf hash matches the named entry, that its STH is present in the
published history, and its inclusion proof.
FAIL-CLOSED: if signature checking is unavailable (no `openssl`, or the
public key is missing), the run FAILS signatures are load-bearing and a
"couldn't check" is not a pass. Use --structural-only to explicitly ask
for hashes/structure without signatures (it prints, and exits, as a
reduced check, never as full verification).
Usage:
python3 verify.py --all
python3 verify.py --receipt receipts/dalek-ed25519-verified.receipt.json
python3 verify.py --all --structural-only # explicit reduced check
"""
import argparse
import base64
@ -82,11 +93,21 @@ def load_leaves():
return leaves, problems
def signatures_available() -> bool:
return bool(shutil.which("openssl")) and (HERE / "provider.ed25519.pub").exists()
def key_fingerprint() -> str:
return hashlib.sha256((HERE / "provider.ed25519.pub").read_bytes()).hexdigest()
def check_sth_signature(head) -> str:
"""VALID / INVALID / UNAVAILABLE. UNAVAILABLE is a FAILURE at the
caller unless the run is explicitly --structural-only."""
openssl = shutil.which("openssl")
key = HERE / "provider.ed25519.pub"
if not openssl or not key.exists():
return "skipped (openssl or provider.ed25519.pub missing)"
return "UNAVAILABLE"
signatures = head.get("signatures") or {}
ed = signatures.get("ed25519") or {}
payload = canonical_json({k: v for k, v in head.items() if k != "signatures"})
@ -107,46 +128,107 @@ def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--all", action="store_true")
parser.add_argument("--receipt")
parser.add_argument("--structural-only", action="store_true",
help="skip Ed25519 signature checks explicitly; the run reports a "
"REDUCED check and can never print full verification.")
args = parser.parse_args()
sigs_ok = signatures_available()
if not args.structural_only and not sigs_ok:
# Fail closed: a verifier that cannot check signatures must not
# imply it did. Do not silently continue.
print("FATAL: signature checking unavailable (need the `openssl` binary and "
"provider.ed25519.pub). Install openssl / fetch the key, or pass "
"--structural-only to run an explicit hashes-and-structure check.")
return 2
leaves, problems = load_leaves()
print(f"entries: {len(leaves)}")
failures = list(problems)
for problem in problems:
print("PROBLEM:", problem)
history_path = HERE / "sth-history.jsonl"
heads = [json.loads(line) for line in history_path.read_text().splitlines() if line.strip()] if history_path.exists() else []
if args.all or not args.receipt:
history_path = HERE / "sth-history.jsonl"
heads = [json.loads(line) for line in history_path.read_text().splitlines() if line.strip()] if history_path.exists() else []
# log-wide checks (GPT §4.3): history internally consistent AND the
# published latest-sth.json is exactly the final history head.
previous = -1
log_id = None
for position, head in enumerate(heads):
size = int(head["tree_size"])
if size > len(leaves):
failures.append(f"STH #{position} claims size {size} > {len(leaves)} leaves")
expected = merkle_root(leaves[:size]).hex()
structural = "OK" if head["root_hash"] == expected and size >= previous else "MISMATCH"
if structural != "OK":
failures.append(f"STH #{position}")
failures.append(f"STH #{position} prefix-root/monotonicity")
if log_id is None:
log_id = head.get("log_id")
elif head.get("log_id") != log_id:
failures.append(f"STH #{position} log_id changed mid-history")
signature = check_sth_signature(head)
if signature == "INVALID":
failures.append(f"STH #{position} signature")
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}")
previous = max(previous, size)
latest_path = HERE / "latest-sth.json"
if latest_path.exists() and heads:
latest = json.loads(latest_path.read_text())
if canonical_json(latest) != canonical_json(heads[-1]):
failures.append("latest-sth.json is not the final sth-history head")
elif int(latest["tree_size"]) != len(leaves):
failures.append(f"latest-sth tree_size {latest['tree_size']} != {len(leaves)} leaves")
else:
print(f"latest-sth: size {latest['tree_size']} == leaf count, and == final history head OK")
if args.receipt:
receipt = json.loads(Path(args.receipt).read_text())
sth = receipt["sth"]
index = int(receipt["leaf_index"])
entry = json.loads((HERE / "entries" / f"{index:06d}.json").read_text())
ok = verify_inclusion(
canonical_json(entry["leaf"]),
index,
int(receipt["tree_size"]),
[bytes.fromhex(h) for h in receipt["inclusion_proof"]],
bytes.fromhex(receipt["sth"]["root_hash"]),
)
print(f"receipt leaf {index} of {receipt['tree_size']}: inclusion {'VALID' if ok else 'INVALID'}")
if not ok:
failures.append("receipt inclusion")
leaf_bytes = canonical_json(entry["leaf"])
print("RESULT:", "OK - the log is internally consistent" if not failures else f"FAILED ({len(failures)} problems)")
return 0 if not failures else 1
# (a) the receipt's STH must be signed by THIS log's key ...
rsig = check_sth_signature(sth)
if rsig == "INVALID" or (rsig == "UNAVAILABLE" and not args.structural_only):
failures.append(f"receipt STH signature {rsig}")
# (b) ... fingerprint the receipt names must be this key ...
fp = (sth.get("signatures", {}).get("ed25519", {}) or {}).get("public_key_fingerprint_sha256")
if sigs_ok and fp and fp != key_fingerprint():
failures.append("receipt STH signed by a different key than provider.ed25519.pub")
# (c) ... its log_id must match the log ...
meta_log_id = json.loads((HERE / "log-metadata.json").read_text()).get("log_id") if (HERE / "log-metadata.json").exists() else None
if meta_log_id and sth.get("log_id") not in (None, meta_log_id):
failures.append("receipt STH log_id does not match this log")
# (d) ... the receipt's STH must actually appear in the published history ...
if heads and canonical_json(sth) not in {canonical_json(h) for h in heads}:
failures.append("receipt STH is not present in sth-history.jsonl")
# (e) ... the receipt's leaf_hash must match the named entry ...
if receipt.get("leaf_hash") and receipt["leaf_hash"] != leaf_hash(leaf_bytes).hex():
failures.append("receipt leaf_hash does not match the named entry")
# (f) ... tree_size agreement ...
if int(receipt.get("tree_size", -1)) != int(sth.get("tree_size", -2)):
failures.append("receipt tree_size != its STH tree_size")
# (g) ... and finally the inclusion proof itself.
ok = verify_inclusion(
leaf_bytes, index, int(receipt["tree_size"]),
[bytes.fromhex(h) for h in receipt["inclusion_proof"]],
bytes.fromhex(sth["root_hash"]),
)
if not ok:
failures.append("receipt inclusion proof")
print(f"receipt leaf {index} of {receipt['tree_size']}: STH-sig:{rsig} bindings:"
f"{'OK' if not any('receipt' in f for f in failures) else 'FAIL'} inclusion:{'VALID' if ok else 'INVALID'}")
mode = "REDUCED (structural only, signatures NOT checked)" if args.structural_only else "full"
if failures:
print(f"RESULT: FAILED ({len(failures)} problems) [{mode}]")
return 1
print(f"RESULT: OK [{mode}]"
+ ("" if not args.structural_only else " — signatures were NOT verified; this is not full verification"))
return 0
if __name__ == "__main__":