research/tests/test_provenance.py
Meanwhile Research c8a04e144b Initial public release: capability-licensing reference implementation + reproducible claim demos
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>
2026-07-19 16:31:58 +10:00

129 lines
3.9 KiB
Python

"""Claim 2: output provenance binding verifies and invalidates as stated."""
import dataclasses
import pytest
from capability_licensing import (
AXIS_CONTEXT_SOURCE,
AXIS_WEIGHT_SOURCE,
RevocationList,
RUNG_CRYPTO_ERASURE,
bind_output,
verify_output_provenance,
)
OUTPUT = b"Stand-in generated output bound to its composition certificates."
@pytest.fixture()
def bound(chain):
"""A second (context-source) certificate and a signed provenance record."""
context_source, _ = chain.authority.issue_capability(
chain.organisation.serial,
"licensed-reference-corpus",
claims={"axis": AXIS_CONTEXT_SOURCE},
)
record = bind_output(
OUTPUT,
sources=[
(AXIS_WEIGHT_SOURCE, chain.capability),
(AXIS_CONTEXT_SOURCE, context_source),
],
signer_certificate=chain.capability,
signer_private_key=chain.capability_key,
)
return chain, context_source, record
def _verify(bound, output=OUTPUT, revocations=None):
chain, _context_source, record = bound
return verify_output_provenance(
output,
record,
chain.authority.registry(),
[chain.organisation],
[chain.root],
revocations or RevocationList(),
)
def test_intact_composition_is_valid(bound):
result = _verify(bound)
assert result.valid, result.reason
# One digest check, one signature check, one signer-chain check, and one
# check per referenced source.
assert len(result.checks) == 5
def test_tampered_output_is_invalid(bound):
result = _verify(bound, output=OUTPUT + b" [edited]")
assert not result.valid
assert "does not match the bound digest" in result.reason
def test_forged_record_signature_is_invalid(bound):
chain, context_source, record = bound
forged = dataclasses.replace(record, output_sha256="0" * 64)
result = verify_output_provenance(
OUTPUT,
forged,
chain.authority.registry(),
[chain.organisation],
[chain.root],
)
assert not result.valid
def test_revoked_source_certificate_invalidates_the_output(bound):
_chain, context_source, _record = bound
revocations = RevocationList()
revocations.revoke(context_source.serial, "context licence terminated")
result = _verify(bound, revocations=revocations)
assert not result.valid
assert "revoked" in result.reason
assert context_source.serial in result.reason
def test_erased_source_certificate_invalidates_the_output(bound):
chain, _context_source, _record = bound
revocations = RevocationList()
revocations.revoke(
chain.capability.serial,
"capability unit crypto-erased",
rung=RUNG_CRYPTO_ERASURE,
)
result = _verify(bound, revocations=revocations)
assert not result.valid
assert "crypto-erasure" in result.reason
def test_reinstating_a_soft_revocation_restores_validity(bound):
_chain, context_source, _record = bound
revocations = RevocationList()
revocations.revoke(context_source.serial, "temporary hold")
assert not _verify(bound, revocations=revocations).valid
revocations.reinstate(context_source.serial)
assert _verify(bound, revocations=revocations).valid
def test_substituted_source_certificate_is_detected(bound):
chain, context_source, record = bound
# An attacker replaces the registry's copy of the context-source
# certificate with a different one under the same serial.
substituted, _ = chain.authority.issue_capability(
chain.organisation.serial, "look-alike-corpus", claims={}
)
registry = chain.authority.registry()
registry[context_source.serial] = dataclasses.replace(
substituted, serial=context_source.serial
)
result = verify_output_provenance(
OUTPUT,
record,
registry,
[chain.organisation],
[chain.root],
)
assert not result.valid
assert "substituted certificate" in result.reason