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>
129 lines
3.9 KiB
Python
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
|