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>
136 lines
4.7 KiB
Python
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)
|