From 3c3cdd71532cd985417a5699d59e6a295132c777 Mon Sep 17 00:00:00 2001 From: Carlos Hernandez-Vaquero Date: Mon, 27 Jul 2026 18:29:51 +0200 Subject: [PATCH] Wire Cedar into cross-operator example --- examples/cross-operator-delegation/README.md | 10 ++++------ examples/cross-operator-delegation/demo.py | 19 ++++++++----------- pyproject.toml | 2 +- tests/unit/test_cedar.py | 15 +++++++++++++++ 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/examples/cross-operator-delegation/README.md b/examples/cross-operator-delegation/README.md index 2da8c48..b034209 100644 --- a/examples/cross-operator-delegation/README.md +++ b/examples/cross-operator-delegation/README.md @@ -39,11 +39,9 @@ so the labeling here is deliberate: ## Files - `ca2a-config.yaml`: runtime config (software-only, advisory). -- `policy.cedar`: the Child's local policy stated as a Cedar rule. It permits - `{task:read, task:audit}`. The demo enforces the same rule with the - `LocalPolicy` allow-set (the model [claim 3](../../experiments/claim3-scope-policy-intersection/) - uses), so it has no dependency on a specific Cedar engine version; binding the - Cedar engine in the peer path is tracked separately. +- `policy.cedar`: the Child's local policy. The demo loads this file into + `CedarPolicy`, which evaluates the Cedar rule permitting + `{task:read, task:audit}`. - `demo.py`: the end-to-end offline flow. Regenerates `chain.json` / `dag.json` on each run and re-verifies them through the CLI. - `chain.json`: a valid two-hop delegation chain, `task:admin` narrowing to @@ -68,7 +66,7 @@ Cross-operator delegation example (offline; synthetic SEV-SNP vectors) [2] mutual attestation binds each channel key (software-asserted): OK [3] attenuated delegation chain verifies (leaf scope narrows): OK leaf delegated scope : ['task:read', 'task:write'] - child local policy : ['task:audit', 'task:read'] + child Cedar policy : ['task:audit', 'task:read'] (policy.cedar) effective scope : ['task:read'] [4] effective scope = delegated ∩ policy = {task:read}: OK [5] child ALLOWS task:read (delegated and locally permitted): OK diff --git a/examples/cross-operator-delegation/demo.py b/examples/cross-operator-delegation/demo.py index d60d53d..b192ab0 100644 --- a/examples/cross-operator-delegation/demo.py +++ b/examples/cross-operator-delegation/demo.py @@ -43,11 +43,11 @@ from cryptography.hazmat.primitives.hashes import SHA384 # noqa: E402 from cryptography.x509.oid import NameOID # noqa: E402 +from ca2a_runtime.cedar import CedarPolicy # noqa: E402 from ca2a_runtime.channel import SealedChannel, generate_channel_keypair, open_sealed # noqa: E402 from ca2a_runtime.delegation import DelegationCredential, new_keypair # noqa: E402 from ca2a_runtime.errors import AttestationFailed, ScopeNotPermitted # noqa: E402 from ca2a_runtime.peer import effective_scope, enforce_peer_call # noqa: E402 -from ca2a_runtime.policy import LocalPolicy # noqa: E402 from ca2a_runtime.provenance import ( # noqa: E402 DelegationRecord, cross_check_chain, @@ -168,20 +168,17 @@ def main() -> int: sorted(chain[-1].scope) == ["task:read", "task:write"])) # ------------------------------------------------------------------ - # Scope ∩ local policy at the Child. The child was delegated - # {task:read, task:write}; its local policy permits {task:read, task:audit}. + # Scope ∩ local Cedar policy at the Child. The child was delegated + # {task:read, task:write}; policy.cedar permits {task:read, task:audit}. # Effective scope is the intersection: {task:read}. - # - # We enforce with the LocalPolicy allow-set (the same model claim 3 uses), - # so the demo has no dependency on a specific Cedar engine version. The - # committed policy.cedar states the SAME rule as a Cedar policy for the real - # engine (ca2a_runtime.cedar.CedarPolicy); binding that engine in the peer - # path is tracked separately (see policy.py and issue #10). # ------------------------------------------------------------------ - policy = LocalPolicy.of(["task:read", "task:audit"]) + policy_path = HERE / "policy.cedar" + policy = CedarPolicy(policy_path.read_text(encoding="utf-8")) eff = effective_scope(chain, policy) + policy_candidates = chain[-1].scope | {"task:audit"} + policy_allow = sorted(cap for cap in policy_candidates if policy.permits(cap)) print(f" leaf delegated scope : {sorted(chain[-1].scope)}") - print(f" child local policy : {sorted(policy.allow)}") + print(f" child Cedar policy : {policy_allow} ({policy_path.name})") print(f" effective scope : {sorted(eff)}") checks.append(step(4, "effective scope = delegated ∩ policy = {task:read}", sorted(eff) == ["task:read"])) diff --git a/pyproject.toml b/pyproject.toml index 3b15984..930f030 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ requires-python = ">=3.11" dependencies = [ "cryptography>=42.0", "pyyaml>=6.0", - "cedarpy>=4.8", + "cedarpy==4.8.7", # TRACE record models + RFC 8785 signing, reused rather than reimplemented # (see ROADMAP Tier 0). Pin a compatible minor; cross-repo skew is a known # risk documented in LIMITATIONS.md. diff --git a/tests/unit/test_cedar.py b/tests/unit/test_cedar.py index afbe71a..beefef4 100644 --- a/tests/unit/test_cedar.py +++ b/tests/unit/test_cedar.py @@ -2,6 +2,8 @@ from __future__ import annotations +from pathlib import Path + import pytest from ca2a_runtime.cedar import CedarPolicy @@ -14,6 +16,12 @@ 'permit(principal, action == Action::"read", resource);\n' 'permit(principal, action == Action::"write", resource);\n' ) +EXAMPLE_POLICY = ( + Path(__file__).resolve().parents[2] + / "examples" + / "cross-operator-delegation" + / "policy.cedar" +) def _chain(): @@ -55,3 +63,10 @@ def test_capability_with_colon() -> None: p = CedarPolicy('permit(principal, action == Action::"tool:read", resource);') assert p.permits("tool:read") assert not p.permits("tool:write") + + +def test_cross_operator_example_policy() -> None: + p = CedarPolicy(EXAMPLE_POLICY.read_text(encoding="utf-8")) + assert p.permits("task:read") + assert p.permits("task:audit") + assert not p.permits("task:write")