lean-transparency-log/verify_selftest.py

131 lines
5.3 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Adversarial self-test for verify.py — proves the fail-closed paths fail.
Each case mutates a real published receipt (or the environment) and asserts
the verifier REJECTS it; plus the honest controls. Exit 0 only if every case
behaves. Run from a clone: python3 verify_selftest.py
"""
import copy
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
HERE = Path(__file__).resolve().parent
def run(*args, env=None):
result = subprocess.run(
[sys.executable, str(HERE / "verify.py"), *args],
capture_output=True, text=True, env=env,
)
return result.returncode, result.stdout
def base_receipt():
path = sorted((HERE / "receipts").glob("*.receipt.json"))[0]
return json.loads(path.read_text())
def mutated(**changes):
receipt = copy.deepcopy(base_receipt())
for dotted, value in changes.items():
target, keys = receipt, dotted.split(".")
for key in keys[:-1]:
target = target[key]
if value is None:
target.pop(keys[-1], None)
else:
target[keys[-1]] = value
return receipt
def check_receipt(receipt) -> int:
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as handle:
json.dump(receipt, handle)
path = handle.name
try:
code, _ = run("--receipt", path)
return code
finally:
os.unlink(path)
def main() -> int:
cases = []
code, out = run("--all")
cases.append(("honest --all passes (full)", code == 0 and "RESULT: OK [full]" in out))
cases.append(("--all covers every published receipt",
out.count("receipt ") == len(list((HERE / "receipts").glob("*.receipt.json")))))
cases.append(("honest receipt passes", check_receipt(base_receipt()) == 0))
cases.append(("missing key fingerprint REJECTED",
check_receipt(mutated(**{"sth.signatures.ed25519.public_key_fingerprint_sha256": None})) == 1))
cases.append(("missing leaf_hash REJECTED", check_receipt(mutated(leaf_hash=None)) == 1))
cases.append(("wrong receipt type REJECTED", check_receipt(mutated(type="forged.v0")) == 1))
cases.append(("forged (unsigned) root REJECTED",
check_receipt(mutated(**{"sth.root_hash": "ff" * 32})) == 1))
cases.append(("tree_size mismatch REJECTED",
check_receipt(mutated(tree_size=int(base_receipt()["tree_size"]) + 1)) == 1))
cases.append(("wrong log_id REJECTED",
check_receipt(mutated(**{"sth.log_id": "00" * 32})) == 1))
code, out = run("--all", "--structural-only")
cases.append(("--structural-only is explicit, never claims full",
code == 0 and "REDUCED" in out and "[full]" not in out))
entries 13-18: re-attest the estate at 44 certs/fork + first SLH-DSA leaf; heads now dual-signed Six new leaves (tree 13 -> 19, root 7ee23940…): [13-16] dalek/anza/risc0/betrusted-ed25519-verified — re-attested at 44 certificates each (27 main + 4 apex + 13 scalar; the leaf 8-11 generation recorded 16). The delta is the P0-P2 hardening campaign: scalar statements bound, kernel-side axiom gate, driver surface with cones, declaration coverage both directions, the accounting identity. [17] ltl-accumulator-verified — 61 certificates; the log again carries proofs of its own Merkle machinery, at the hardened state. [18] fips205-slhdsa-verified — FIRST post-quantum leaf: 11 certificates over the SLH-DSA-SHA2-128s verify path, apex fips205.slh_verify_128s_accepts_iff. Heads from tree 14 on carry a second, ADDITIVE signature: SLH-DSA-SHA2-128s (FIPS 205), deterministic, over the same payload as the Ed25519 signature. provider.slhdsa.pub ships beside the Ed25519 key. Ed25519 remains the signature consumers must check; verify.py now judges slh_dsa fail-closed where present (INVALID/WRONG-KEY/NO-PUBKEY fail; older heads report ABSENT, allowed; pre-3.5 OpenSSL degrades loudly, never silently). The ml_dsa slot stays not_configured — truthfully. Honesty, unchanged by any of it: the certificates cover VERIFICATION paths of the extracted Lean models; no signing operation is proven for any algorithm; leaves are Ed25519-signed at issuance only. Append-only law checked byte-for-byte before this commit: entries 000000-000012 identical, the six prior heads an exact prefix of the history. verify.py --all: RESULT OK [full]. verify_selftest.py: GREEN (13 cases). Quorum gate: 5-way Ed25519 + 2-way SLH-DSA (incl. the verifier built from the pinned proven source), all accept, all reject corruption. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 15:11:59 +00:00
# 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})
cases.append(("no openssl -> FAIL CLOSED (exit 2)", code == 2))
width = max(len(name) for name, _ in cases)
for name, ok in cases:
print(f"{'PASS' if ok else 'FAIL'} {name:<{width}}")
if all(ok for _, ok in cases):
print(f"SELFTEST GREEN ({len(cases)} cases)")
return 0
print("SELFTEST RED")
return 1
if __name__ == "__main__":
sys.exit(main())