mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-03 19:53:43 +00:00
verify.py template: check the additive SLH-DSA head signature, fail-closed where it counts
The consumer verifier now judges the slh_dsa block on every head. The status
taxonomy is the design decision, so it is spelled out and each arm tested:
ABSENT head predates the second signature — allowed (additive posture,
operator decision 2026-08-06); an append-only log keeps history.
VALID verified against provider.slhdsa.pub.
INVALID present and wrong — HARD FAILURE. A bad signature is never a
degradation.
WRONG-KEY head names a different key than the mirror ships — hard failure.
NO-PUBKEY head claims the signature, mirror ships no key — broken
publication, hard failure.
TOOLING OpenSSL < 3.5 cannot read SLH-DSA keys — honest degradation:
one loud NOTE, never counted as verified, never failed; the
required Ed25519 checks still gate the result.
verify_selftest.py gains two adversarial cases, both mutating a full mirror
copy: one flipped signature byte must fail naming slh_dsa:INVALID; deleting
provider.slhdsa.pub while heads claim the signature must fail NO-PUBKEY. On
mirrors without slh blocks the cases record as not-applicable rather than
silently passing. First selftest run caught my own marker bug — the case
grepped for the stored failure string, which is never printed, instead of
the printed status; the printed line is now the assertion target.
README template: SLH-DSA key row, honesty paragraph (verification proven for
both algorithms, signing for neither; leaves Ed25519-signed at issuance
only), and the hardcoded head size is GONE — the README now names no tree
size at all, so this staleness class dies with the instance (it said "tree
size 13" and would have shipped that under a 19-leaf head).
Template is deliberately ahead of the published mirror until the next
publication, which overwrites the mirror from here (the sync rule's
intended direction for pre-publication work). Proven against the scratch
19-leaf mirror: verify.py --all RESULT OK [full] with ABSENT on heads 8-13
and VALID on 14-19; selftest 13/13 GREEN. Backward compat proven against a
copy of the live 13-leaf mirror: RESULT OK [full], selftest GREEN with the
slh cases n/a. Suite 152/0/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d28a5c080e
commit
962c0fdd41
1 changed files with 120 additions and 6 deletions
|
|
@ -22,7 +22,11 @@ files in this repository alone:
|
||||||
1. every entry's leaf hash,
|
1. every entry's leaf hash,
|
||||||
2. every historical Signed Tree Head against the recomputed prefix root
|
2. every historical Signed Tree Head against the recomputed prefix root
|
||||||
(a split view or tampered entry fails here),
|
(a split view or tampered entry fails here),
|
||||||
3. every STH Ed25519 signature,
|
3. every STH Ed25519 signature — and, where a head carries the ADDITIVE
|
||||||
|
SLH-DSA-SHA2-128s signature (FIPS 205, heads from 2026-08 on), that
|
||||||
|
too: a present-but-wrong post-quantum signature FAILS the run, a head
|
||||||
|
without one is allowed, and an OpenSSL too old to check it (pre-3.5)
|
||||||
|
is reported loudly as a degradation, never counted as verified,
|
||||||
4. every published receipt under receipts/ (with --all), and any receipt
|
4. every published receipt under receipts/ (with --all), and any receipt
|
||||||
supplied via --receipt FILE, as a FULL transparency receipt: type tag,
|
supplied via --receipt FILE, as a FULL transparency receipt: type tag,
|
||||||
STH signature, REQUIRED key fingerprint, log id, presence of its STH
|
STH signature, REQUIRED key fingerprint, log id, presence of its STH
|
||||||
|
|
@ -140,6 +144,57 @@ def check_sth_signature(head) -> str:
|
||||||
return "VALID" if result.returncode == 0 else "INVALID"
|
return "VALID" if result.returncode == 0 else "INVALID"
|
||||||
|
|
||||||
|
|
||||||
|
def check_slh_dsa_signature(head):
|
||||||
|
"""ADDITIVE post-quantum check (SLH-DSA-SHA2-128s, FIPS 205).
|
||||||
|
|
||||||
|
Returns (status, hard_failure). Ed25519 remains the REQUIRED signature;
|
||||||
|
this one is verified when the head carries it and the local OpenSSL
|
||||||
|
(>= 3.5) can check it. The distinctions matter:
|
||||||
|
ABSENT - head predates the second signature. Allowed: additive.
|
||||||
|
VALID - verified against provider.slhdsa.pub.
|
||||||
|
INVALID - present and WRONG. Hard failure - a bad signature is never
|
||||||
|
a degradation.
|
||||||
|
WRONG-KEY - the head names a different key than the mirror ships.
|
||||||
|
Hard failure.
|
||||||
|
NO-PUBKEY - the head claims the signature but the mirror ships no
|
||||||
|
provider.slhdsa.pub. Broken publication: hard failure.
|
||||||
|
TOOLING - this OpenSSL cannot read SLH-DSA keys (pre-3.5). Honest
|
||||||
|
degradation: reported loudly, never counted as verified,
|
||||||
|
never failed - the required Ed25519 check still gates.
|
||||||
|
"""
|
||||||
|
slh = (head.get("signatures") or {}).get("slh_dsa") or {}
|
||||||
|
if slh.get("status") != "signed":
|
||||||
|
return "ABSENT", False
|
||||||
|
key = HERE / "provider.slhdsa.pub"
|
||||||
|
openssl = shutil.which("openssl")
|
||||||
|
if not openssl:
|
||||||
|
return "TOOLING", False
|
||||||
|
if not key.exists():
|
||||||
|
return "NO-PUBKEY", True
|
||||||
|
fp = slh.get("public_key_fingerprint_sha256")
|
||||||
|
if fp and fp != hashlib.sha256(key.read_bytes()).hexdigest():
|
||||||
|
return "WRONG-KEY", True
|
||||||
|
probe = subprocess.run([openssl, "pkey", "-pubin", "-in", str(key), "-noout"],
|
||||||
|
capture_output=True)
|
||||||
|
if probe.returncode != 0:
|
||||||
|
return "TOOLING", False
|
||||||
|
payload = canonical_json({k: v for k, v in head.items() if k != "signatures"})
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
payload_path = Path(tmp) / "p"
|
||||||
|
signature_path = Path(tmp) / "s"
|
||||||
|
payload_path.write_bytes(payload)
|
||||||
|
try:
|
||||||
|
signature_path.write_bytes(base64.b64decode(slh.get("signature_base64", "")))
|
||||||
|
except Exception:
|
||||||
|
return "INVALID", True
|
||||||
|
result = subprocess.run(
|
||||||
|
[openssl, "pkeyutl", "-verify", "-pubin", "-inkey", str(key), "-rawin",
|
||||||
|
"-in", str(payload_path), "-sigfile", str(signature_path)],
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
return ("VALID", False) if result.returncode == 0 else ("INVALID", True)
|
||||||
|
|
||||||
|
|
||||||
RECEIPT_TYPE = "pacta.transparency.receipt.v1"
|
RECEIPT_TYPE = "pacta.transparency.receipt.v1"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -245,6 +300,7 @@ def main() -> int:
|
||||||
# published latest-sth.json is exactly the final history head.
|
# published latest-sth.json is exactly the final history head.
|
||||||
previous = -1
|
previous = -1
|
||||||
log_id = None
|
log_id = None
|
||||||
|
slh_tooling_seen = False
|
||||||
for position, head in enumerate(heads):
|
for position, head in enumerate(heads):
|
||||||
size = int(head["tree_size"])
|
size = int(head["tree_size"])
|
||||||
if size > len(leaves):
|
if size > len(leaves):
|
||||||
|
|
@ -260,8 +316,20 @@ def main() -> int:
|
||||||
signature = check_sth_signature(head)
|
signature = check_sth_signature(head)
|
||||||
if signature == "INVALID" or (signature == "UNAVAILABLE" and not args.structural_only):
|
if signature == "INVALID" or (signature == "UNAVAILABLE" and not args.structural_only):
|
||||||
failures.append(f"STH #{position} signature {signature}")
|
failures.append(f"STH #{position} signature {signature}")
|
||||||
print(f"STH #{position} size={size} root={head['root_hash'][:16]}… prefix-root:{structural} signature:{signature}")
|
if args.structural_only:
|
||||||
|
slh = "SKIPPED"
|
||||||
|
else:
|
||||||
|
slh, slh_hard = check_slh_dsa_signature(head)
|
||||||
|
if slh_hard:
|
||||||
|
failures.append(f"STH #{position} slh_dsa {slh}")
|
||||||
|
if slh == "TOOLING":
|
||||||
|
slh_tooling_seen = True
|
||||||
|
print(f"STH #{position} size={size} root={head['root_hash'][:16]}… prefix-root:{structural} signature:{signature} slh_dsa:{slh}")
|
||||||
previous = max(previous, size)
|
previous = max(previous, size)
|
||||||
|
if slh_tooling_seen:
|
||||||
|
print("NOTE: this log carries an ADDITIVE SLH-DSA (FIPS 205) signature that "
|
||||||
|
"your OpenSSL cannot check (needs >= 3.5). The required Ed25519 checks "
|
||||||
|
"above still gate this result; the post-quantum signature was NOT verified.")
|
||||||
latest_path = HERE / "latest-sth.json"
|
latest_path = HERE / "latest-sth.json"
|
||||||
if latest_path.exists() and heads:
|
if latest_path.exists() and heads:
|
||||||
latest = json.loads(latest_path.read_text())
|
latest = json.loads(latest_path.read_text())
|
||||||
|
|
@ -371,6 +439,40 @@ def main() -> int:
|
||||||
cases.append(("--structural-only is explicit, never claims full",
|
cases.append(("--structural-only is explicit, never claims full",
|
||||||
code == 0 and "REDUCED" in out and "[full]" not in out))
|
code == 0 and "REDUCED" in out and "[full]" not in out))
|
||||||
|
|
||||||
|
# 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:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
os.symlink(sys.executable, Path(tmp) / Path(sys.executable).name)
|
os.symlink(sys.executable, Path(tmp) / Path(sys.executable).name)
|
||||||
code, out = run("--all", env={"PATH": tmp})
|
code, out = run("--all", env={"PATH": tmp})
|
||||||
|
|
@ -403,8 +505,12 @@ kernel-checked proofs *about the accumulator model* underlying its own
|
||||||
inclusion and consistency reasoning, as one of its own entries (subject
|
inclusion and consistency reasoning, as one of its own entries (subject
|
||||||
[`ltl-accumulator-verified`](https://github.com/saymrwulf/ltl-accumulator-verified);
|
[`ltl-accumulator-verified`](https://github.com/saymrwulf/ltl-accumulator-verified);
|
||||||
scoped to the mechanized model — it does not prove operator honesty,
|
scoped to the mechanized model — it does not prove operator honesty,
|
||||||
signing, or execution provenance). Current head: tree size 13, root
|
signing, or execution provenance). As of **2026-08** the log also attests
|
||||||
`3488a2d0…`.
|
the **SLH-DSA (FIPS 205) verify-path proofs**
|
||||||
|
([`fips205-slhdsa-verified`](https://github.com/saymrwulf/fips205-slhdsa-verified))
|
||||||
|
and its heads carry an **additive post-quantum SLH-DSA-SHA2-128s signature**
|
||||||
|
beside the required Ed25519 one. The current head is `latest-sth.json` —
|
||||||
|
this README deliberately names no tree size, so it cannot go stale.
|
||||||
|
|
||||||
Layout:
|
Layout:
|
||||||
|
|
||||||
|
|
@ -415,7 +521,8 @@ Layout:
|
||||||
| `receipts/<component>.receipt.json` | inclusion proof binding that attestation to the latest signed head |
|
| `receipts/<component>.receipt.json` | inclusion proof binding that attestation to the latest signed head |
|
||||||
| `sth-history.jsonl` | **every** Signed Tree Head ever issued — the witness channel: all cloners see the same heads |
|
| `sth-history.jsonl` | **every** Signed Tree Head ever issued — the witness channel: all cloners see the same heads |
|
||||||
| `latest-sth.json` | the current head |
|
| `latest-sth.json` | the current head |
|
||||||
| `provider.ed25519.pub` | the provider's public key — the sole cryptographic identity anchor; each statement's truth additionally rests on the assumptions stated in its leaf |
|
| `provider.ed25519.pub` | the provider's Ed25519 public key — the REQUIRED identity anchor; each statement's truth additionally rests on the assumptions stated in its leaf |
|
||||||
|
| `provider.slhdsa.pub` | the provider's SLH-DSA-SHA2-128s public key (FIPS 205) — checks the ADDITIVE post-quantum head signature; needs OpenSSL >= 3.5, and verify.py degrades honestly below that |
|
||||||
| `verify.py` | standalone verifier (Python stdlib + the `openssl` binary; fails closed without them; `--all` covers every published receipt) |
|
| `verify.py` | standalone verifier (Python stdlib + the `openssl` binary; fails closed without them; `--all` covers every published receipt) |
|
||||||
| `verify_selftest.py` | adversarial self-test: proves the verifier's fail-closed paths reject mutated receipts |
|
| `verify_selftest.py` | adversarial self-test: proves the verifier's fail-closed paths reject mutated receipts |
|
||||||
|
|
||||||
|
|
@ -439,5 +546,12 @@ declared trusted base). The log deliberately
|
||||||
retains early leaves recording a **failed** audit run: an append-only
|
retains early leaves recording a **failed** audit run: an append-only
|
||||||
trust ledger keeps its history. Tree heads are signed by the merkleized,
|
trust ledger keeps its history. Tree heads are signed by the merkleized,
|
||||||
proof-attested Ed25519 library itself, and each signature embeds the
|
proof-attested Ed25519 library itself, and each signature embeds the
|
||||||
provider's own Merkle self-check of that library's leaf.
|
provider's own Merkle self-check of that library's leaf. Heads additionally
|
||||||
|
carry a **deterministic SLH-DSA-SHA2-128s signature** over the same payload:
|
||||||
|
additive, so Ed25519 remains the signature a consumer must check, and honest
|
||||||
|
about scope — the estate's certificates cover the *verification* path of both
|
||||||
|
algorithms; no signing operation is proven for either, and leaves themselves
|
||||||
|
are Ed25519-signed at issuance only. Heads published before 2026-08 have no
|
||||||
|
SLH-DSA signature and verify.py reports them as `slh_dsa:ABSENT`, which is
|
||||||
|
allowed — an append-only log keeps its history.
|
||||||
'''
|
'''
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue