mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-04 20:03:40 +00:00
The dogfood principle now runs in BOTH directions. Agents already
verified signatures through the proven dalek path; now the provider
SIGNS with it too, and proves to itself that the signing code is in its
own log before every signature:
- dogfood binary gains a `sign` mode (seed over stdin, never argv;
ed25519_dalek::SigningKey from the same pinned merkleized workspace).
Honesty ledger unchanged: the library's VERIFY path is
certificate-covered; its signing path is declared trusted base - but
it is the ATTESTED artifact, not an un-attested third implementation.
- sign_payload_ed25519_detailed: signing dispatch mirroring the verify
dispatch; the backend that actually signed is recorded in every
attestation signature block and STH.
- THE SELF-REFERENTIAL CHECK: before signing any tree head, the
provider runs the SAME Merkle inclusion verification an agent runs -
against the very tree it is about to sign - for the newest leaf
attesting the signing library itself, and embeds the result in the
signature block:
signing_provenance:
signing_backend: verified-dalek-serial
signing_library_component: dalek-ed25519-verified
signing_library_source_commit: aa0f6ab...
self_inclusion: verified
signing_library_leaf_index: 4
signing_library_certificates_proven: 16/16
A root signature that names the leaf vouching for the code that
produced it. First-append chicken-and-egg is handled honestly
(self_inclusion: library_not_in_log).
- Evidence refreshed: all four receipts re-issued under dogfood-signed
STHs; the full agent verify loop re-run green.
50/50 tests (new signing roundtrip test, skip-safe where unbuilt).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
345 lines
13 KiB
Python
345 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from .postquantum import detect_ml_dsa
|
|
from .signing import canonical_json, public_key_fingerprint, sign_payload_ed25519_detailed, verify_payload_ed25519_detailed
|
|
from .yamlio import load_data
|
|
|
|
HASH_ALGORITHM = "RFC9162_SHA256"
|
|
LEAF_TYPE = "pacta.transparency.attestation_leaf.v1"
|
|
RECEIPT_TYPE = "pacta.transparency.receipt.v1"
|
|
STH_TYPE = "pacta.transparency.signed_tree_head.v1"
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ReceiptVerificationResult:
|
|
accepted: bool
|
|
diagnostics: list[str] = field(default_factory=list)
|
|
log_id: str | None = None
|
|
tree_size: int | None = None
|
|
root_hash: str | None = None
|
|
leaf_hash: str | None = None
|
|
signatures: dict[str, str] = field(default_factory=dict)
|
|
|
|
def evidence(self) -> dict[str, Any]:
|
|
return {
|
|
"transparency_receipt_status": "accepted" if self.accepted else "rejected",
|
|
"transparency_log_id": self.log_id,
|
|
"transparency_tree_size": self.tree_size,
|
|
"transparency_root_hash": self.root_hash,
|
|
"transparency_leaf_hash": self.leaf_hash,
|
|
"transparency_signature_status": self.signatures,
|
|
}
|
|
|
|
|
|
def leaf_hash(data: bytes) -> bytes:
|
|
return hashlib.sha256(b"\x00" + data).digest()
|
|
|
|
|
|
def node_hash(left: bytes, right: bytes) -> bytes:
|
|
return hashlib.sha256(b"\x01" + left + right).digest()
|
|
|
|
|
|
def merkle_root(leaves: list[bytes]) -> bytes:
|
|
if not leaves:
|
|
return hashlib.sha256(b"").digest()
|
|
if len(leaves) == 1:
|
|
return leaf_hash(leaves[0])
|
|
split = _largest_power_of_two_less_than(len(leaves))
|
|
return node_hash(merkle_root(leaves[:split]), merkle_root(leaves[split:]))
|
|
|
|
|
|
def inclusion_proof(leaves: list[bytes], index: int) -> list[bytes]:
|
|
if index < 0 or index >= len(leaves):
|
|
raise ValueError(f"Leaf index {index} is outside tree size {len(leaves)}")
|
|
return _inclusion_path(leaves, index)
|
|
|
|
|
|
def verify_inclusion(leaf: bytes, index: int, tree_size: int, proof: list[bytes], root_hash: bytes) -> bool:
|
|
if tree_size <= 0 or index < 0 or index >= tree_size:
|
|
return False
|
|
try:
|
|
calculated, consumed = _calculate_inclusion_root(leaf_hash(leaf), index, tree_size, proof, 0)
|
|
except ValueError:
|
|
return False
|
|
return consumed == len(proof) and calculated == root_hash
|
|
|
|
|
|
def consistency_proof(leaves: list[bytes], old_tree_size: int) -> list[bytes]:
|
|
if old_tree_size < 0 or old_tree_size > len(leaves):
|
|
raise ValueError("old_tree_size must be between 0 and the current tree size")
|
|
if old_tree_size in (0, len(leaves)):
|
|
return []
|
|
return _consistency_path(old_tree_size, leaves, complete=True)
|
|
|
|
|
|
def verify_consistency(
|
|
old_tree_size: int,
|
|
new_tree_size: int,
|
|
old_root_hash: bytes,
|
|
new_root_hash: bytes,
|
|
proof: list[bytes],
|
|
) -> bool:
|
|
if old_tree_size < 0 or new_tree_size < old_tree_size:
|
|
return False
|
|
if old_tree_size == 0:
|
|
return True
|
|
if old_tree_size == new_tree_size:
|
|
return old_root_hash == new_root_hash and not proof
|
|
if not proof:
|
|
return False
|
|
|
|
nodes = list(proof)
|
|
if _is_power_of_two(old_tree_size):
|
|
nodes.insert(0, old_root_hash)
|
|
fn = old_tree_size - 1
|
|
sn = new_tree_size - 1
|
|
while fn & 1:
|
|
fn >>= 1
|
|
sn >>= 1
|
|
|
|
old_hash = nodes.pop(0)
|
|
new_hash = old_hash
|
|
|
|
while nodes:
|
|
node = nodes.pop(0)
|
|
if sn == 0:
|
|
return False
|
|
if fn & 1 or fn == sn:
|
|
old_hash = node_hash(node, old_hash)
|
|
new_hash = node_hash(node, new_hash)
|
|
if not (fn & 1):
|
|
while fn & 1 == 0 and fn != 0:
|
|
fn >>= 1
|
|
sn >>= 1
|
|
else:
|
|
new_hash = node_hash(new_hash, node)
|
|
fn >>= 1
|
|
sn >>= 1
|
|
|
|
return old_hash == old_root_hash and new_hash == new_root_hash
|
|
|
|
|
|
def attestation_leaf(attestation: dict[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"schema_version": 1,
|
|
"type": LEAF_TYPE,
|
|
"attestation": attestation,
|
|
}
|
|
|
|
|
|
def leaf_bytes_for_attestation(attestation: dict[str, Any]) -> bytes:
|
|
return canonical_json(attestation_leaf(attestation))
|
|
|
|
|
|
def leaf_hash_hex_for_attestation(attestation: dict[str, Any]) -> str:
|
|
return leaf_hash(leaf_bytes_for_attestation(attestation)).hex()
|
|
|
|
|
|
def signed_tree_head_payload(sth: dict[str, Any]) -> bytes:
|
|
payload = {key: value for key, value in sth.items() if key != "signatures"}
|
|
return canonical_json(payload)
|
|
|
|
|
|
def make_signed_tree_head(
|
|
log_id: str,
|
|
tree_size: int,
|
|
root_hash_hex: str,
|
|
timestamp: str,
|
|
private_key_path: str | Path,
|
|
public_key_path: str | Path,
|
|
signing_provenance: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
sth: dict[str, Any] = {
|
|
"schema_version": 1,
|
|
"type": STH_TYPE,
|
|
"log_id": log_id,
|
|
"tree_size": tree_size,
|
|
"timestamp": timestamp,
|
|
"root_hash": root_hash_hex,
|
|
"hash_algorithm": HASH_ALGORITHM,
|
|
}
|
|
payload = signed_tree_head_payload(sth)
|
|
signature_base64, signing_backend = sign_payload_ed25519_detailed(payload, private_key_path)
|
|
sth["signatures"] = {
|
|
"ed25519": {
|
|
"scheme": "openssl-ed25519",
|
|
"signing_backend": signing_backend,
|
|
"status": "signed",
|
|
"payload_digest_sha256": hashlib.sha256(payload).hexdigest(),
|
|
"signature_base64": signature_base64,
|
|
"public_key_fingerprint_sha256": public_key_fingerprint(public_key_path),
|
|
**({"signing_provenance": signing_provenance} if signing_provenance else {}),
|
|
},
|
|
"ml_dsa": detect_ml_dsa().to_signature_slot(),
|
|
}
|
|
return sth
|
|
|
|
|
|
def verify_signed_tree_head(
|
|
sth: dict[str, Any],
|
|
public_key_path: str | Path,
|
|
require_signatures: str = "ed25519",
|
|
) -> tuple[bool, list[str], dict[str, str]]:
|
|
diagnostics: list[str] = []
|
|
statuses: dict[str, str] = {}
|
|
if sth.get("type") != STH_TYPE:
|
|
diagnostics.append(f"Unexpected signed tree head type: {sth.get('type')}")
|
|
if sth.get("hash_algorithm") != HASH_ALGORITHM:
|
|
diagnostics.append(f"Unexpected hash algorithm: {sth.get('hash_algorithm')}")
|
|
|
|
signatures = sth.get("signatures") or {}
|
|
ed25519 = signatures.get("ed25519") or {}
|
|
if ed25519.get("scheme") != "openssl-ed25519" or ed25519.get("status") != "signed":
|
|
diagnostics.append("Signed tree head is missing a usable Ed25519 signature.")
|
|
statuses["ed25519"] = str(ed25519.get("status") or "missing")
|
|
else:
|
|
payload = signed_tree_head_payload(sth)
|
|
expected_digest = ed25519.get("payload_digest_sha256")
|
|
actual_digest = hashlib.sha256(payload).hexdigest()
|
|
if expected_digest and expected_digest != actual_digest:
|
|
diagnostics.append("Signed tree head digest does not match signature metadata.")
|
|
statuses["ed25519"] = "digest_mismatch"
|
|
else:
|
|
expected_fingerprint = ed25519.get("public_key_fingerprint_sha256")
|
|
if expected_fingerprint and expected_fingerprint != public_key_fingerprint(public_key_path):
|
|
diagnostics.append("Signed tree head Ed25519 public-key fingerprint mismatch.")
|
|
statuses["ed25519"] = "key_mismatch"
|
|
else:
|
|
ok, error, backend = verify_payload_ed25519_detailed(payload, str(ed25519.get("signature_base64") or ""), public_key_path)
|
|
statuses["ed25519"] = "verified" if ok else "failed"
|
|
statuses["ed25519_backend"] = backend
|
|
if not ok:
|
|
diagnostics.append(f"Signed tree head Ed25519 verification failed: {error}")
|
|
|
|
ml_dsa = signatures.get("ml_dsa") or {}
|
|
statuses["ml_dsa"] = str(ml_dsa.get("status") or "missing")
|
|
if require_signatures == "both" and statuses["ml_dsa"] != "verified":
|
|
diagnostics.append("ML-DSA signed tree head signature is required by policy but is not verified.")
|
|
elif require_signatures not in {"ed25519", "both"}:
|
|
diagnostics.append(f"Unsupported transparency signature policy: {require_signatures}")
|
|
return not diagnostics, diagnostics, statuses
|
|
|
|
|
|
def load_receipt(path: str | Path) -> dict[str, Any]:
|
|
raw = load_data(path)
|
|
if not isinstance(raw, dict):
|
|
raise ValueError(f"Transparency receipt must be a mapping: {path}")
|
|
return raw
|
|
|
|
|
|
def verify_receipt(
|
|
attestation: dict[str, Any],
|
|
receipt: dict[str, Any],
|
|
log_public_key_path: str | Path,
|
|
require_signatures: str = "ed25519",
|
|
) -> ReceiptVerificationResult:
|
|
diagnostics: list[str] = []
|
|
if receipt.get("type") != RECEIPT_TYPE:
|
|
diagnostics.append(f"Unexpected transparency receipt type: {receipt.get('type')}")
|
|
if receipt.get("hash_algorithm") != HASH_ALGORITHM:
|
|
diagnostics.append(f"Unexpected transparency receipt hash algorithm: {receipt.get('hash_algorithm')}")
|
|
|
|
sth = receipt.get("sth") or {}
|
|
sth_ok, sth_diagnostics, statuses = verify_signed_tree_head(sth, log_public_key_path, require_signatures=require_signatures)
|
|
diagnostics.extend(sth_diagnostics)
|
|
try:
|
|
tree_size = int(receipt.get("tree_size"))
|
|
leaf_index = int(receipt.get("leaf_index"))
|
|
except (TypeError, ValueError):
|
|
tree_size = -1
|
|
leaf_index = -1
|
|
diagnostics.append("Transparency receipt has invalid tree_size or leaf_index.")
|
|
|
|
if sth.get("tree_size") != tree_size:
|
|
diagnostics.append("Transparency receipt tree_size does not match signed tree head.")
|
|
if receipt.get("log_id") != sth.get("log_id"):
|
|
diagnostics.append("Transparency receipt log_id does not match signed tree head.")
|
|
|
|
leaf_bytes = leaf_bytes_for_attestation(attestation)
|
|
expected_leaf_hash = leaf_hash(leaf_bytes).hex()
|
|
if receipt.get("leaf_hash") != expected_leaf_hash:
|
|
diagnostics.append("Transparency receipt leaf hash does not match attestation.")
|
|
expected_attestation_digest = hashlib.sha256(canonical_json(attestation)).hexdigest()
|
|
if receipt.get("attestation_digest_sha256") and receipt.get("attestation_digest_sha256") != expected_attestation_digest:
|
|
diagnostics.append("Transparency receipt attestation digest does not match attestation.")
|
|
|
|
try:
|
|
root = bytes.fromhex(str(sth.get("root_hash") or ""))
|
|
proof = [bytes.fromhex(str(item)) for item in (receipt.get("inclusion_proof") or [])]
|
|
except ValueError as exc:
|
|
root = b""
|
|
proof = []
|
|
diagnostics.append(f"Transparency proof contains invalid hex: {exc}")
|
|
|
|
if root and tree_size >= 0:
|
|
if not verify_inclusion(leaf_bytes, leaf_index, tree_size, proof, root):
|
|
diagnostics.append("Transparency inclusion proof does not verify against the signed tree head.")
|
|
|
|
accepted = not diagnostics and sth_ok
|
|
return ReceiptVerificationResult(
|
|
accepted=accepted,
|
|
diagnostics=diagnostics,
|
|
log_id=str(receipt.get("log_id") or sth.get("log_id") or "") or None,
|
|
tree_size=tree_size if tree_size >= 0 else None,
|
|
root_hash=str(sth.get("root_hash") or "") or None,
|
|
leaf_hash=str(receipt.get("leaf_hash") or "") or None,
|
|
signatures=statuses,
|
|
)
|
|
|
|
|
|
def proof_to_hex(proof: list[bytes]) -> list[str]:
|
|
return [item.hex() for item in proof]
|
|
|
|
|
|
def _inclusion_path(leaves: list[bytes], index: int) -> list[bytes]:
|
|
if len(leaves) == 1:
|
|
return []
|
|
split = _largest_power_of_two_less_than(len(leaves))
|
|
if index < split:
|
|
return _inclusion_path(leaves[:split], index) + [merkle_root(leaves[split:])]
|
|
return _inclusion_path(leaves[split:], index - split) + [merkle_root(leaves[:split])]
|
|
|
|
|
|
def _calculate_inclusion_root(
|
|
current_hash: bytes,
|
|
index: int,
|
|
tree_size: int,
|
|
proof: list[bytes],
|
|
proof_index: int,
|
|
) -> tuple[bytes, int]:
|
|
if tree_size == 1:
|
|
return current_hash, proof_index
|
|
split = _largest_power_of_two_less_than(tree_size)
|
|
if index < split:
|
|
left, consumed = _calculate_inclusion_root(current_hash, index, split, proof, proof_index)
|
|
if consumed >= len(proof):
|
|
raise ValueError("proof exhausted")
|
|
return node_hash(left, proof[consumed]), consumed + 1
|
|
right, consumed = _calculate_inclusion_root(current_hash, index - split, tree_size - split, proof, proof_index)
|
|
if consumed >= len(proof):
|
|
raise ValueError("proof exhausted")
|
|
return node_hash(proof[consumed], right), consumed + 1
|
|
|
|
|
|
def _consistency_path(old_tree_size: int, leaves: list[bytes], complete: bool) -> list[bytes]:
|
|
if old_tree_size == len(leaves):
|
|
return [] if complete else [merkle_root(leaves)]
|
|
split = _largest_power_of_two_less_than(len(leaves))
|
|
if old_tree_size <= split:
|
|
return _consistency_path(old_tree_size, leaves[:split], complete) + [merkle_root(leaves[split:])]
|
|
return _consistency_path(old_tree_size - split, leaves[split:], complete=False) + [merkle_root(leaves[:split])]
|
|
|
|
|
|
def _largest_power_of_two_less_than(value: int) -> int:
|
|
if value <= 1:
|
|
raise ValueError("value must be greater than one")
|
|
return 1 << ((value - 1).bit_length() - 1)
|
|
|
|
|
|
def _is_power_of_two(value: int) -> bool:
|
|
return value > 0 and value & (value - 1) == 0
|