verify.py: --all verifies every published receipt; binding fields required, not compare-if-present (round-12 GPT B4)
Round-12 review found two artifact-claim gaps: --all never touched
receipts, and receipt mode checked fingerprint/leaf_hash only when
present. Now:
- --all enumerates receipts/*.receipt.json and runs the full binding
list on each (type tag, STH signature, REQUIRED key fingerprint,
log_id vs log-metadata, STH membership in the published history,
REQUIRED leaf_hash vs the named entry, tree_size agreement, receipt
hash_algorithm + log_id consistency, inclusion proof).
- --receipt FILE routes through the same function.
- Malformed values (bad hex, missing sizes) are failures, not crashes.
- NEW verify_selftest.py: 11-case adversarial battery (mutated real
receipts must be REJECTED: no fingerprint, no leaf_hash, wrong type,
forged root, size mismatch, wrong log_id; honest controls pass;
structural-only never claims full; no-openssl exits 2). GREEN.
One bug caught by the honest controls during development: the new
hash-algorithm check assumed 'sha256' but deployed heads carry
'RFC9162_SHA256' - fixed against reality, plus receipt-level
hash_algorithm/log_id binding added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:15:19 +00:00
|
|
|
#!/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))
|
|
|
|
|
|
verify.py: --all verifies every published receipt; binding fields required, not compare-if-present (round-12 GPT B4)
Round-12 review found two artifact-claim gaps: --all never touched
receipts, and receipt mode checked fingerprint/leaf_hash only when
present. Now:
- --all enumerates receipts/*.receipt.json and runs the full binding
list on each (type tag, STH signature, REQUIRED key fingerprint,
log_id vs log-metadata, STH membership in the published history,
REQUIRED leaf_hash vs the named entry, tree_size agreement, receipt
hash_algorithm + log_id consistency, inclusion proof).
- --receipt FILE routes through the same function.
- Malformed values (bad hex, missing sizes) are failures, not crashes.
- NEW verify_selftest.py: 11-case adversarial battery (mutated real
receipts must be REJECTED: no fingerprint, no leaf_hash, wrong type,
forged root, size mismatch, wrong log_id; honest controls pass;
structural-only never claims full; no-openssl exits 2). GREEN.
One bug caught by the honest controls during development: the new
hash-algorithm check assumed 'sha256' but deployed heads carry
'RFC9162_SHA256' - fixed against reality, plus receipt-level
hash_algorithm/log_id binding added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:15:19 +00:00
|
|
|
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())
|