"""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