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