mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-03 19:53:43 +00:00
receipt-verify checks the post-quantum co-signature — the tool now does what rung 2 promises
Operator-caught: rung 2 demanded both public keys while the shown command consumed only the Ed25519 one — and the gap was real: pacta had NO SLH-DSA head-signature check (only the mirror's verify.py had one; '--require-signatures both' refers to the empty ML-DSA slot). New: verify_receipt(slhdsa_public_key_path=...) verifies the additive co-signature fail-closed (absent on pre-14 heads reports absent, not failed; unavailable OpenSSL fails closed), CLI grows --slhdsa-public-key, rung 2's command carries the flag and its muted text explains both checks. Proven against the LIVE log: accepted:true, slh_dsa:verified, ed25519 on the dogfood backend. New tamper test flips a signature byte and must be rejected. Suite 156 green.
This commit is contained in:
parent
fdfe217d57
commit
bcbf929045
4 changed files with 69 additions and 3 deletions
|
|
@ -255,8 +255,9 @@ two holders compare.</span></div>
|
||||||
You still trust: that the operator’s recorded observation is honest.<br>
|
You still trust: that the operator’s recorded observation is honest.<br>
|
||||||
You need: four small files from the tables below — the two public keys, plus one library’s
|
You need: four small files from the tables below — the two public keys, plus one library’s
|
||||||
claim file (“attestation”) and its proof of inclusion (“receipt”).
|
claim file (“attestation”) and its proof of inclusion (“receipt”).
|
||||||
<pre>pacta receipt-verify --attestation … --receipt … --log-public-key provider.ed25519.pub</pre>
|
<pre>pacta receipt-verify --attestation … --receipt … --log-public-key provider.ed25519.pub --slhdsa-public-key provider.slhdsa.pub</pre>
|
||||||
<span class="muted">Your machine checks one Ed25519 signature and
|
<span class="muted">Your machine checks the required Ed25519 signature, the additive
|
||||||
|
post-quantum co-signature (needs OpenSSL ≥ 3.5; drop the second flag to skip it), and
|
||||||
~{max(1,(latest.get('tree_size') or 1).bit_length())} hashes — no proof assistant involved.
|
~{max(1,(latest.get('tree_size') or 1).bit_length())} hashes — no proof assistant involved.
|
||||||
The <code>pacta</code> tool ships in the
|
The <code>pacta</code> tool ships in the
|
||||||
<a href="https://github.com/saymrwulf/proof-aware-crypto-tooling-agent">pacta repository</a>
|
<a href="https://github.com/saymrwulf/proof-aware-crypto-tooling-agent">pacta repository</a>
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
receipt_verify.add_argument("--receipt", required=True)
|
receipt_verify.add_argument("--receipt", required=True)
|
||||||
receipt_verify.add_argument("--log-public-key", required=True)
|
receipt_verify.add_argument("--log-public-key", required=True)
|
||||||
receipt_verify.add_argument("--require-signatures", choices=["ed25519", "both"], default="ed25519")
|
receipt_verify.add_argument("--require-signatures", choices=["ed25519", "both"], default="ed25519")
|
||||||
|
receipt_verify.add_argument("--slhdsa-public-key", help="Also verify the additive SLH-DSA head co-signature against this public key (OpenSSL >= 3.5; heads before tree size 14 report absent, not failed).")
|
||||||
receipt_verify.add_argument("--sth-store", help="Path to the local STH pin store (split-view/rollback defense).")
|
receipt_verify.add_argument("--sth-store", help="Path to the local STH pin store (split-view/rollback defense).")
|
||||||
receipt_verify.add_argument("--consistency-proof", help="File with a hex consistency proof from the pinned tree size (provider: log-consistency).")
|
receipt_verify.add_argument("--consistency-proof", help="File with a hex consistency proof from the pinned tree size (provider: log-consistency).")
|
||||||
receipt_verify.add_argument("--max-sth-age-seconds", type=int, help="Reject signed tree heads older than this (freshness policy).")
|
receipt_verify.add_argument("--max-sth-age-seconds", type=int, help="Reject signed tree heads older than this (freshness policy).")
|
||||||
|
|
@ -476,7 +477,8 @@ def cmd_score(args: argparse.Namespace) -> int:
|
||||||
def cmd_receipt_verify(args: argparse.Namespace) -> int:
|
def cmd_receipt_verify(args: argparse.Namespace) -> int:
|
||||||
attestation = load_attestation(args.attestation)
|
attestation = load_attestation(args.attestation)
|
||||||
receipt = load_receipt(args.receipt)
|
receipt = load_receipt(args.receipt)
|
||||||
result = verify_receipt(attestation, receipt, args.log_public_key, require_signatures=args.require_signatures)
|
result = verify_receipt(attestation, receipt, args.log_public_key, require_signatures=args.require_signatures,
|
||||||
|
slhdsa_public_key_path=args.slhdsa_public_key)
|
||||||
accountability_diagnostics = _log_accountability_checks(
|
accountability_diagnostics = _log_accountability_checks(
|
||||||
receipt,
|
receipt,
|
||||||
sth_store=args.sth_store,
|
sth_store=args.sth_store,
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,7 @@ def verify_receipt(
|
||||||
receipt: dict[str, Any],
|
receipt: dict[str, Any],
|
||||||
log_public_key_path: str | Path,
|
log_public_key_path: str | Path,
|
||||||
require_signatures: str = "ed25519",
|
require_signatures: str = "ed25519",
|
||||||
|
slhdsa_public_key_path: str | Path | None = None,
|
||||||
) -> ReceiptVerificationResult:
|
) -> ReceiptVerificationResult:
|
||||||
diagnostics: list[str] = []
|
diagnostics: list[str] = []
|
||||||
if receipt.get("type") != RECEIPT_TYPE:
|
if receipt.get("type") != RECEIPT_TYPE:
|
||||||
|
|
@ -266,6 +267,29 @@ def verify_receipt(
|
||||||
sth = receipt.get("sth") or {}
|
sth = receipt.get("sth") or {}
|
||||||
sth_ok, sth_diagnostics, statuses = verify_signed_tree_head(sth, log_public_key_path, require_signatures=require_signatures)
|
sth_ok, sth_diagnostics, statuses = verify_signed_tree_head(sth, log_public_key_path, require_signatures=require_signatures)
|
||||||
diagnostics.extend(sth_diagnostics)
|
diagnostics.extend(sth_diagnostics)
|
||||||
|
if slhdsa_public_key_path is not None:
|
||||||
|
# The additive post-quantum co-signature (heads from tree size 14
|
||||||
|
# on). Absent on older heads is NOT a failure - an append-only log
|
||||||
|
# keeps its history; a present-but-bad signature fails closed.
|
||||||
|
from .slhdsa import verify_payload_slhdsa
|
||||||
|
|
||||||
|
slh = (sth.get("signatures") or {}).get("slh_dsa") or {}
|
||||||
|
if str(slh.get("status") or "absent") == "signed":
|
||||||
|
try:
|
||||||
|
slh_ok, slh_error = verify_payload_slhdsa(
|
||||||
|
signed_tree_head_payload(sth),
|
||||||
|
str(slh.get("signature_base64") or ""),
|
||||||
|
slhdsa_public_key_path,
|
||||||
|
)
|
||||||
|
except Exception as exc: # old OpenSSL, unreadable key: fail closed
|
||||||
|
slh_ok, slh_error = False, f"SLH-DSA verification unavailable: {exc}"
|
||||||
|
if slh_ok:
|
||||||
|
statuses["slh_dsa"] = "verified"
|
||||||
|
else:
|
||||||
|
statuses["slh_dsa"] = "failed"
|
||||||
|
diagnostics.append(f"SLH-DSA head co-signature did not verify: {slh_error}")
|
||||||
|
else:
|
||||||
|
statuses["slh_dsa"] = "absent"
|
||||||
try:
|
try:
|
||||||
tree_size = int(receipt.get("tree_size"))
|
tree_size = int(receipt.get("tree_size"))
|
||||||
leaf_index = int(receipt.get("leaf_index"))
|
leaf_index = int(receipt.get("leaf_index"))
|
||||||
|
|
|
||||||
|
|
@ -145,3 +145,42 @@ def test_requiring_both_signatures_fails_without_ml_dsa_backend(tmp_path):
|
||||||
assert not result.accepted
|
assert not result.accepted
|
||||||
assert result.signatures["ed25519"] == "verified"
|
assert result.signatures["ed25519"] == "verified"
|
||||||
assert result.signatures["ml_dsa"] != "verified"
|
assert result.signatures["ml_dsa"] != "verified"
|
||||||
|
|
||||||
|
|
||||||
|
def test_receipt_verify_checks_slhdsa_cosignature(tmp_path):
|
||||||
|
# Rung 2 of the site promises both head signatures are checkable;
|
||||||
|
# this binds the promise to the tool (operator-caught 2026-08-16).
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pacta import slhdsa
|
||||||
|
|
||||||
|
try:
|
||||||
|
slhdsa.generate_slhdsa_keypair(tmp_path / "slh.key", tmp_path / "slh.pub")
|
||||||
|
except Exception:
|
||||||
|
pytest.skip("OpenSSL without SLH-DSA support on this host")
|
||||||
|
attestation, private_key, public_key = _signed_attestation(tmp_path)
|
||||||
|
from pacta.yamlio import dump_data
|
||||||
|
|
||||||
|
dump_data(attestation, tmp_path / "attestation.yaml")
|
||||||
|
log = TransparencyLog(tmp_path / "log")
|
||||||
|
log.init("local-test-provider", public_key)
|
||||||
|
receipt = log.append_attestation(
|
||||||
|
tmp_path / "attestation.yaml", private_key, public_key,
|
||||||
|
receipt_out=tmp_path / "receipt.yaml",
|
||||||
|
slhdsa_private_key_path=tmp_path / "slh.key",
|
||||||
|
slhdsa_public_key_path=tmp_path / "slh.pub",
|
||||||
|
)
|
||||||
|
result = verify_receipt(attestation, receipt, public_key,
|
||||||
|
slhdsa_public_key_path=tmp_path / "slh.pub")
|
||||||
|
assert result.accepted, result.diagnostics
|
||||||
|
assert result.signatures["slh_dsa"] == "verified"
|
||||||
|
|
||||||
|
tampered = __import__("copy").deepcopy(receipt)
|
||||||
|
sig = tampered["sth"]["signatures"]["slh_dsa"]["signature_base64"]
|
||||||
|
import base64 as _b64
|
||||||
|
raw = bytearray(_b64.b64decode(sig)); raw[0] ^= 0xFF
|
||||||
|
tampered["sth"]["signatures"]["slh_dsa"]["signature_base64"] = _b64.b64encode(bytes(raw)).decode()
|
||||||
|
bad = verify_receipt(attestation, tampered, public_key,
|
||||||
|
slhdsa_public_key_path=tmp_path / "slh.pub")
|
||||||
|
assert not bad.accepted
|
||||||
|
assert bad.signatures["slh_dsa"] == "failed"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue