research/tests/test_revocation.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

136 lines
4.7 KiB
Python

"""Claim 3: the three-rung revocation ladder behaves as stated."""
import pytest
from capability_licensing import (
IrreversibleRevocationError,
KeyDestroyedError,
KeyState,
KeyWithheldError,
LicenceInvalidError,
RevocationList,
RUNG_CRYPTO_ERASURE,
verify_chain,
)
def test_baseline_valid_licence_decrypts(sealed_setup):
plaintext = sealed_setup.keys.request_decrypt(
sealed_setup.sealed, sealed_setup.chain.capability
)
assert plaintext == sealed_setup.payload
# ---- rung (a): SOFT / advisory ------------------------------------------
def test_soft_revocation_reported_by_verifiers(sealed_setup):
chain = sealed_setup.chain
sealed_setup.revocations.revoke(chain.capability.serial, "terms breached")
result = verify_chain(
chain.capability,
[chain.organisation],
[chain.root],
revocation_list=sealed_setup.revocations,
)
assert not result.valid
assert "revoked" in result.reason
def test_soft_revocation_denies_at_a_compliant_authority(sealed_setup):
chain = sealed_setup.chain
sealed_setup.revocations.revoke(chain.capability.serial, "terms breached")
with pytest.raises(LicenceInvalidError, match="revoked"):
sealed_setup.keys.request_decrypt(sealed_setup.sealed, chain.capability)
def test_soft_revocation_is_advisory_key_still_exists(sealed_setup):
sealed_setup.revocations.revoke(
sealed_setup.chain.capability.serial, "terms breached"
)
assert sealed_setup.keys.key_state(sealed_setup.sealed.unit_id) is KeyState.RELEASED
def test_soft_revocation_is_reversible(sealed_setup):
chain = sealed_setup.chain
sealed_setup.revocations.revoke(chain.capability.serial, "terms breached")
sealed_setup.revocations.reinstate(chain.capability.serial)
plaintext = sealed_setup.keys.request_decrypt(sealed_setup.sealed, chain.capability)
assert plaintext == sealed_setup.payload
# ---- rung (b): ACCESS-GATED ---------------------------------------------
def test_withheld_key_denies_even_a_valid_licence(sealed_setup):
sealed_setup.keys.withhold(sealed_setup.sealed.unit_id)
with pytest.raises(KeyWithheldError):
sealed_setup.keys.request_decrypt(
sealed_setup.sealed, sealed_setup.chain.capability
)
def test_withheld_key_is_recoverable_by_re_release(sealed_setup):
sealed_setup.keys.withhold(sealed_setup.sealed.unit_id)
sealed_setup.keys.re_release(sealed_setup.sealed.unit_id)
plaintext = sealed_setup.keys.request_decrypt(
sealed_setup.sealed, sealed_setup.chain.capability
)
assert plaintext == sealed_setup.payload
# ---- rung (c): CRYPTO-ERASURE -------------------------------------------
def test_destroyed_key_denies_permanently(sealed_setup):
sealed_setup.keys.destroy(sealed_setup.sealed.unit_id)
with pytest.raises(KeyDestroyedError):
sealed_setup.keys.request_decrypt(
sealed_setup.sealed, sealed_setup.chain.capability
)
def test_destroyed_key_cannot_be_re_released(sealed_setup):
sealed_setup.keys.destroy(sealed_setup.sealed.unit_id)
with pytest.raises(KeyDestroyedError):
sealed_setup.keys.re_release(sealed_setup.sealed.unit_id)
def test_destroyed_key_cannot_be_withheld_or_destroyed_again(sealed_setup):
sealed_setup.keys.destroy(sealed_setup.sealed.unit_id)
with pytest.raises(KeyDestroyedError):
sealed_setup.keys.withhold(sealed_setup.sealed.unit_id)
with pytest.raises(KeyDestroyedError):
sealed_setup.keys.destroy(sealed_setup.sealed.unit_id)
def test_erasure_ledger_entry_cannot_be_reinstated():
revocations = RevocationList()
revocations.revoke("CERT-example", "unit erased", rung=RUNG_CRYPTO_ERASURE)
with pytest.raises(IrreversibleRevocationError):
revocations.reinstate("CERT-example")
# ---- licence checks at the authority ------------------------------------
def test_wrong_certificate_is_refused(sealed_setup):
chain = sealed_setup.chain
other_leaf, _ = chain.authority.issue_capability(
chain.organisation.serial, "some-other-capability", claims={}
)
with pytest.raises(LicenceInvalidError, match="does not match"):
sealed_setup.keys.request_decrypt(sealed_setup.sealed, other_leaf)
def test_audit_log_records_the_ladder(sealed_setup):
keys = sealed_setup.keys
unit_id = sealed_setup.sealed.unit_id
keys.withhold(unit_id)
keys.re_release(unit_id)
keys.destroy(unit_id)
events = [event for _stamp, logged_unit, event in keys.audit_log() if logged_unit == unit_id]
assert any("sealed" in event for event in events)
assert any("withheld" in event for event in events)
assert any("re-released" in event for event in events)
assert any("DESTROYED" in event for event in events)