From bcf7e4ac927ac7e40b82c5b36197ac80219ca256 Mon Sep 17 00:00:00 2001 From: RobLe3 Date: Tue, 25 Aug 2026 11:50:09 +0200 Subject: [PATCH] feat: reject revoked policy disclosure authority --- ...policy-detail-disclosure-authority-v0.json | 226 ++++++++++++++++++ src/iicp_client/policy_detail_disclosure.py | 18 +- tests/test_policy_detail_disclosure.py | 33 ++- 3 files changed, 262 insertions(+), 15 deletions(-) create mode 100644 parity/policy-detail-disclosure-authority-v0.json diff --git a/parity/policy-detail-disclosure-authority-v0.json b/parity/policy-detail-disclosure-authority-v0.json new file mode 100644 index 0000000..0cec648 --- /dev/null +++ b/parity/policy-detail-disclosure-authority-v0.json @@ -0,0 +1,226 @@ +{ + "profile": "urn:iicp:profile:policy-detail-disclosure:v0", + "fixture_version": "0.2.0-draft", + "status": "pre-normative", + "allowed_detail_fields": [ + "retention_intervals", + "subprocessor_references", + "approval_evidence_references", + "operational_evidence_references" + ], + "reason_precedence": [ + "consumer_auth_required", + "consumer_auth_invalid", + "consumer_auth_expired", + "consumer_auth_revoked", + "dispatch_ticket_invalid", + "dispatch_ticket_expired", + "dispatch_ticket_revoked", + "disclosure_forbidden", + "resource_concealed", + "compatible" + ], + "cases": [ + { + "id": "POLICY-DISCLOSURE-01", + "context": { + "consumer_auth": "valid", + "disclosure_allowed": true, + "provider_node_id": "node-a", + "consumer_target_node_id": "node-a", + "ticket_target_node_id": "node-a", + "consumer_intent": "urn:iicp:intent:llm:chat:v1", + "ticket_intent": "urn:iicp:intent:llm:chat:v1", + "manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "ticket_manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "details": { + "retention_intervals": { + "task_payload_max_seconds": 30, + "logs_max_days": 7 + }, + "subprocessor_references": [ + "urn:iicp:processor:self-hosted" + ], + "approval_evidence_references": [ + "urn:iicp:evidence:approval:abc" + ], + "operational_evidence_references": [ + "urn:iicp:evidence:retention:def" + ], + "prompt": "must-not-leak", + "response": "must-not-leak", + "credential": "must-not-leak", + "endpoint": "https://private.example", + "natural_person_contact": "must-not-leak@example.invalid", + "backend_topology": { + "peer": "private" + } + }, + "dispatch_ticket": "valid" + }, + "expected": { + "status": 200, + "reason": "compatible" + } + }, + { + "id": "POLICY-DISCLOSURE-02", + "context": { + "consumer_auth": "missing", + "disclosure_allowed": true + }, + "expected": { + "status": 401, + "reason": "consumer_auth_required" + } + }, + { + "id": "POLICY-DISCLOSURE-03", + "context": { + "consumer_auth": "invalid", + "disclosure_allowed": true + }, + "expected": { + "status": 401, + "reason": "consumer_auth_invalid" + } + }, + { + "id": "POLICY-DISCLOSURE-04", + "context": { + "consumer_auth": "expired", + "disclosure_allowed": true + }, + "expected": { + "status": 401, + "reason": "consumer_auth_expired" + } + }, + { + "id": "POLICY-DISCLOSURE-05", + "context": { + "consumer_auth": "valid", + "disclosure_allowed": false, + "dispatch_ticket": "valid" + }, + "expected": { + "status": 403, + "reason": "disclosure_forbidden" + } + }, + { + "id": "POLICY-DISCLOSURE-06", + "context": { + "consumer_auth": "valid", + "disclosure_allowed": true, + "provider_node_id": "node-a", + "consumer_target_node_id": "node-b", + "ticket_target_node_id": "node-a", + "consumer_intent": "urn:iicp:intent:llm:chat:v1", + "ticket_intent": "urn:iicp:intent:llm:chat:v1", + "manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "ticket_manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "dispatch_ticket": "valid" + }, + "expected": { + "status": 404, + "reason": "resource_concealed" + } + }, + { + "id": "POLICY-DISCLOSURE-07", + "context": { + "consumer_auth": "valid", + "disclosure_allowed": true, + "provider_node_id": "node-a", + "consumer_target_node_id": "node-a", + "ticket_target_node_id": "node-a", + "consumer_intent": "urn:iicp:intent:llm:chat:v1", + "ticket_intent": "urn:iicp:intent:llm:embedding:v1", + "manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "ticket_manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "dispatch_ticket": "valid" + }, + "expected": { + "status": 404, + "reason": "resource_concealed" + } + }, + { + "id": "POLICY-DISCLOSURE-08", + "context": { + "consumer_auth": "valid", + "disclosure_allowed": true, + "provider_node_id": "node-a", + "consumer_target_node_id": "node-a", + "ticket_target_node_id": "node-a", + "consumer_intent": "urn:iicp:intent:llm:chat:v1", + "ticket_intent": "urn:iicp:intent:llm:chat:v1", + "manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "ticket_manifest_sha256": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "dispatch_ticket": "valid" + }, + "expected": { + "status": 404, + "reason": "resource_concealed" + } + }, + { + "id": "POLICY-DISCLOSURE-09", + "context": { + "consumer_auth": "revoked", + "disclosure_allowed": true + }, + "expected": { + "status": 401, + "reason": "consumer_auth_revoked" + } + }, + { + "id": "POLICY-DISCLOSURE-10", + "context": { + "consumer_auth": "valid", + "dispatch_ticket": "invalid", + "disclosure_allowed": true + }, + "expected": { + "status": 401, + "reason": "dispatch_ticket_invalid" + } + }, + { + "id": "POLICY-DISCLOSURE-11", + "context": { + "consumer_auth": "valid", + "dispatch_ticket": "expired", + "disclosure_allowed": true + }, + "expected": { + "status": 401, + "reason": "dispatch_ticket_expired" + } + }, + { + "id": "POLICY-DISCLOSURE-12", + "context": { + "consumer_auth": "valid", + "dispatch_ticket": "revoked", + "disclosure_allowed": true + }, + "expected": { + "status": 401, + "reason": "dispatch_ticket_revoked" + } + } + ], + "crypto_vectors": { + "evaluated_at_unix": 1800000000, + "public_key_hex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "expected_subject": "consumer-a", + "expected_target_node_id": "node-a", + "expected_intent": "urn:iicp:intent:llm:chat:v1", + "valid_consumer_token": "eyJ2IjoxLCJpc3MiOiJodHRwczovL2lpY3AubmV0d29yayIsInN1YiI6ImNvbnN1bWVyLWEiLCJhdWQiOiJub2RlLWEiLCJpbnRlbnQiOiJ1cm46aWljcDppbnRlbnQ6bGxtOmNoYXQ6djEiLCJpYXQiOjE4MDAwMDAwMDAsImV4cCI6MTgwMDAwMDMwMH0.1e84446db9af38ec1b34f1bb81a29609cc972772a2dc6e409b35574e74f74803d721f8a673d46ba789dc45e4bf35afe05099a776b90600c57a6abc6fd9a8e801", + "expired_consumer_token": "eyJ2IjoxLCJpc3MiOiJodHRwczovL2lpY3AubmV0d29yayIsInN1YiI6ImNvbnN1bWVyLWEiLCJhdWQiOiJub2RlLWEiLCJpbnRlbnQiOiJ1cm46aWljcDppbnRlbnQ6bGxtOmNoYXQ6djEiLCJpYXQiOjE4MDAwMDAwMDAsImV4cCI6MTc5OTk5OTk5OX0.4bb5e2260800b4934db2e8873f6a0ac96dc0ad2912afb66587646b54ce1a26937f0b4245993261fd4c514753a667e243c81eb215cc7da88eb458b61a50073e02", + "tampered_consumer_token": "eyJ2IjoxLCJpc3MiOiJodHRwczovL2lpY3AubmV0d29yayIsInN1YiI6ImNvbnN1bWVyLWEiLCJhdWQiOiJub2RlLWEiLCJpbnRlbnQiOiJ1cm46aWljcDppbnRlbnQ6bGxtOmNoYXQ6djEiLCJpYXQiOjE4MDAwMDAwMDAsImV4cCI6MTgwMDAwMDMwMH0.1e84446db9af38ec1b34f1bb81a29609cc972772a2dc6e409b35574e74f74803d721f8a673d46ba789dc45e4bf35afe05099a776b90600c57a6abc6fd9a8e800" + } +} diff --git a/src/iicp_client/policy_detail_disclosure.py b/src/iicp_client/policy_detail_disclosure.py index f297419..50b109b 100644 --- a/src/iicp_client/policy_detail_disclosure.py +++ b/src/iicp_client/policy_detail_disclosure.py @@ -65,17 +65,29 @@ def verify_policy_detail_consumer_token( def evaluate_policy_detail_disclosure(context: dict[str, Any]) -> PolicyDetailDisclosureDecision: """Apply the portable authorization, concealment and redaction contract. - ``consumer_auth`` MUST be the result of cryptographic verification by the - integration adapter, never a value copied from an untrusted request body. + ``consumer_auth`` and ``dispatch_ticket`` MUST be verification results from + the integration adapter, never values copied from an untrusted request body. """ auth = context.get("consumer_auth") if auth == "missing": return PolicyDetailDisclosureDecision(401, "consumer_auth_required") - if auth == "invalid" or auth not in {"valid", "expired"}: + if auth == "invalid" or auth not in {"valid", "expired", "revoked"}: return PolicyDetailDisclosureDecision(401, "consumer_auth_invalid") if auth == "expired": return PolicyDetailDisclosureDecision(401, "consumer_auth_expired") + if auth == "revoked": + return PolicyDetailDisclosureDecision(401, "consumer_auth_revoked") + + ticket = context.get("dispatch_ticket") + if ticket is None: + pass # Historical v0 fixture: ticket authorization was not projected. + elif ticket == "expired": + return PolicyDetailDisclosureDecision(401, "dispatch_ticket_expired") + elif ticket == "revoked": + return PolicyDetailDisclosureDecision(401, "dispatch_ticket_revoked") + elif ticket != "valid": + return PolicyDetailDisclosureDecision(401, "dispatch_ticket_invalid") if context.get("disclosure_allowed") is not True: return PolicyDetailDisclosureDecision(403, "disclosure_forbidden") diff --git a/tests/test_policy_detail_disclosure.py b/tests/test_policy_detail_disclosure.py index 33b48a2..b6e44ad 100644 --- a/tests/test_policy_detail_disclosure.py +++ b/tests/test_policy_detail_disclosure.py @@ -7,21 +7,30 @@ verify_policy_detail_consumer_token, ) -FIXTURE = json.loads((Path(__file__).parents[1] / "parity/policy-detail-disclosure-v0.json").read_text()) +PARITY = Path(__file__).parents[1] / "parity" +FIXTURES = [ + json.loads((PARITY / name).read_text()) + for name in ( + "policy-detail-disclosure-v0.json", + "policy-detail-disclosure-authority-v0.json", + ) +] +FIXTURE = FIXTURES[0] def test_policy_detail_disclosure_fixture() -> None: - assert tuple(FIXTURE["allowed_detail_fields"]) == ALLOWED_DETAIL_FIELDS - for case in FIXTURE["cases"]: - decision = evaluate_policy_detail_disclosure(case["context"]) - assert decision.status == case["expected"]["status"], case["id"] - assert decision.reason == case["expected"]["reason"], case["id"] - if decision.status == 200: - assert decision.body is not None - assert set(decision.body["details"]) <= set(ALLOWED_DETAIL_FIELDS) - serialized = json.dumps(decision.body) - for forbidden in ("must-not-leak", "private.example", "backend_topology", "natural_person_contact"): - assert forbidden not in serialized + for fixture in FIXTURES: + assert tuple(fixture["allowed_detail_fields"]) == ALLOWED_DETAIL_FIELDS + for case in fixture["cases"]: + decision = evaluate_policy_detail_disclosure(case["context"]) + assert decision.status == case["expected"]["status"], case["id"] + assert decision.reason == case["expected"]["reason"], case["id"] + if decision.status == 200: + assert decision.body is not None + assert set(decision.body["details"]) <= set(ALLOWED_DETAIL_FIELDS) + serialized = json.dumps(decision.body) + for forbidden in ("must-not-leak", "private.example", "backend_topology", "natural_person_contact"): + assert forbidden not in serialized def test_unrecognized_auth_state_fails_as_invalid() -> None: