Clean-room reference implementation (Ed25519 certificate chain, AES-256-GCM envelope, three-rung revocation ladder, crypto-erasure) with runnable examples and 36 tests that reproduce each demonstrated claim on stand-in payloads. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
204 lines
7.2 KiB
Python
204 lines
7.2 KiB
Python
"""Output provenance: bind an output to the certificates of its composition.
|
|
|
|
A produced output is bound, by digest and by Ed25519 signature, to the
|
|
certificates of the sources that composed it, on two axes:
|
|
|
|
* weight-source: the capability (for example a fine-tuned adapter) whose
|
|
weights produced the output;
|
|
* context-source: the licensed context or retrieved material that informed it.
|
|
|
|
The binding is signed by the producing capability's private key, so the
|
|
record cannot be forged or edited, and each referenced certificate is pinned
|
|
by content digest, so a certificate cannot be silently swapped for another
|
|
with the same serial.
|
|
|
|
Verification checks the output digest, the signature, and the full chain of
|
|
every referenced certificate against the trust store AND the revocation
|
|
ledger. The consequence, demonstrated in the examples and tests: an output
|
|
verifies VALID while its composition is intact, and flips to INVALID the
|
|
moment any referenced certificate is revoked or crypto-erased. Provenance is
|
|
not a static stamp; it is a live claim against the current state of the
|
|
authority.
|
|
|
|
Scope note (honest bound): this is OUTPUT-LEVEL binding. Per-unit-of-text
|
|
attribution of which source produced which span is a separate measured
|
|
research result, referenced from the README by result id, and is not
|
|
reimplemented here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
from typing import Dict, Iterable, Mapping, Optional, Sequence, Tuple
|
|
|
|
from cryptography.exceptions import InvalidSignature
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
|
|
from .certificates import Certificate, verify_chain
|
|
from .revocation import RevocationList
|
|
|
|
AXIS_WEIGHT_SOURCE = "weight-source"
|
|
AXIS_CONTEXT_SOURCE = "context-source"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SourceReference:
|
|
"""A composition source, pinned to an exact certificate by digest."""
|
|
|
|
axis: str
|
|
serial: str
|
|
cert_sha256: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProvenanceRecord:
|
|
output_sha256: str
|
|
sources: Tuple[SourceReference, ...]
|
|
signer_serial: str
|
|
issued_at: str
|
|
signature_hex: str = ""
|
|
|
|
def tbs_dict(self) -> Dict:
|
|
return {
|
|
"output_sha256": self.output_sha256,
|
|
"sources": [
|
|
{"axis": s.axis, "serial": s.serial, "cert_sha256": s.cert_sha256}
|
|
for s in self.sources
|
|
],
|
|
"signer_serial": self.signer_serial,
|
|
"issued_at": self.issued_at,
|
|
}
|
|
|
|
def tbs_bytes(self) -> bytes:
|
|
return json.dumps(self.tbs_dict(), sort_keys=True, separators=(",", ":")).encode(
|
|
"utf-8"
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProvenanceResult:
|
|
valid: bool
|
|
reason: str
|
|
checks: Tuple[str, ...] = ()
|
|
|
|
def __bool__(self) -> bool: # pragma: no cover - convenience only
|
|
return self.valid
|
|
|
|
|
|
def bind_output(
|
|
output: bytes,
|
|
sources: Sequence[Tuple[str, Certificate]],
|
|
signer_certificate: Certificate,
|
|
signer_private_key: Ed25519PrivateKey,
|
|
at: Optional[datetime] = None,
|
|
) -> ProvenanceRecord:
|
|
"""Create a signed provenance record binding ``output`` to its sources.
|
|
|
|
``sources`` is a sequence of ``(axis, certificate)`` pairs, one per
|
|
composition source. The signer is the producing capability (normally the
|
|
weight-source), holding its own leaf private key: the authority does not
|
|
sign provenance, the capability does, and any verifier checks it against
|
|
the capability's certificate.
|
|
"""
|
|
moment = at or datetime.now(timezone.utc)
|
|
unsigned = ProvenanceRecord(
|
|
output_sha256=hashlib.sha256(output).hexdigest(),
|
|
sources=tuple(
|
|
SourceReference(axis=axis, serial=cert.serial, cert_sha256=cert.sha256())
|
|
for axis, cert in sources
|
|
),
|
|
signer_serial=signer_certificate.serial,
|
|
issued_at=moment.astimezone(timezone.utc).isoformat(timespec="seconds"),
|
|
)
|
|
signature = signer_private_key.sign(unsigned.tbs_bytes()).hex()
|
|
return ProvenanceRecord(
|
|
output_sha256=unsigned.output_sha256,
|
|
sources=unsigned.sources,
|
|
signer_serial=unsigned.signer_serial,
|
|
issued_at=unsigned.issued_at,
|
|
signature_hex=signature,
|
|
)
|
|
|
|
|
|
def verify_output_provenance(
|
|
output: bytes,
|
|
record: ProvenanceRecord,
|
|
registry: Mapping[str, Certificate],
|
|
intermediates: Iterable[Certificate],
|
|
trusted_roots: Iterable[Certificate],
|
|
revocation_list: Optional[RevocationList] = None,
|
|
at: Optional[datetime] = None,
|
|
) -> ProvenanceResult:
|
|
"""Verify an output against its provenance record and the live authority state.
|
|
|
|
Returns VALID only if every check passes:
|
|
|
|
1. the output's digest matches the bound digest;
|
|
2. the signer's certificate is known and its signature over the record
|
|
verifies;
|
|
3. the signer's chain verifies to a trusted root and is not revoked;
|
|
4. every referenced source certificate is known, byte-identical to the
|
|
one bound at composition time, chain-valid, and not revoked or erased.
|
|
"""
|
|
checks = []
|
|
intermediates = tuple(intermediates)
|
|
trusted_roots = tuple(trusted_roots)
|
|
|
|
def fail(reason: str) -> ProvenanceResult:
|
|
return ProvenanceResult(False, reason, tuple(checks))
|
|
|
|
digest = hashlib.sha256(output).hexdigest()
|
|
if digest != record.output_sha256:
|
|
return fail(
|
|
"output does not match the bound digest (tampered or substituted output)"
|
|
)
|
|
checks.append(f"output digest matches ({digest[:16]}...)")
|
|
|
|
signer = registry.get(record.signer_serial)
|
|
if signer is None:
|
|
return fail(f"signer certificate not found: {record.signer_serial}")
|
|
try:
|
|
signer.public_key().verify(
|
|
bytes.fromhex(record.signature_hex), record.tbs_bytes()
|
|
)
|
|
except (InvalidSignature, ValueError):
|
|
return fail(
|
|
f"provenance signature does not verify against signer {signer.serial} "
|
|
"(forged or edited record)"
|
|
)
|
|
checks.append(f"record signature by {signer.serial} verifies")
|
|
|
|
signer_chain = verify_chain(
|
|
signer, intermediates, trusted_roots, at=at, revocation_list=revocation_list
|
|
)
|
|
if not signer_chain.valid:
|
|
return fail(f"signer chain invalid: {signer_chain.reason}")
|
|
checks.append("signer chain verifies to trusted root")
|
|
|
|
for source in record.sources:
|
|
certificate = registry.get(source.serial)
|
|
if certificate is None:
|
|
return fail(f"{source.axis} certificate not found: {source.serial}")
|
|
if certificate.sha256() != source.cert_sha256:
|
|
return fail(
|
|
f"{source.axis} certificate {source.serial} does not match the "
|
|
"content bound at composition time (substituted certificate)"
|
|
)
|
|
chain = verify_chain(
|
|
certificate,
|
|
intermediates,
|
|
trusted_roots,
|
|
at=at,
|
|
revocation_list=revocation_list,
|
|
)
|
|
if not chain.valid:
|
|
return fail(
|
|
f"{source.axis} certificate {source.serial} failed verification: "
|
|
f"{chain.reason}"
|
|
)
|
|
checks.append(f"{source.axis} {source.serial}: intact, chain valid, not revoked")
|
|
|
|
return ProvenanceResult(True, "output provenance verifies", tuple(checks))
|