From 869cc7ace25f80518b5907c718316912878730e3 Mon Sep 17 00:00:00 2001 From: tommylauren Date: Mon, 20 Apr 2026 23:58:24 -0400 Subject: [PATCH] RFC 0111: Decision attestation fields Add two optional fields to Cedar's AuthorizationResponse so downstream systems can bind a decision to (a) a stable identifier and (b) the exact policy set that produced it: - decision_id: Option (time-ordered UUID v7) - policy_set_hash: Option (SHA-256 over canonical policy set) Both additive, both optional, zero breaking changes, opt-in via a request-level config field. Useful to any Cedar consumer that maintains persistent audit trails, not specific to any single domain. The RFC text follows the same structure as recent RFCs (0110). Signed-off-by: Tom Farley Signed-off-by: tommylauren --- text/0111-decision-attestation.md | 166 ++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 text/0111-decision-attestation.md diff --git a/text/0111-decision-attestation.md b/text/0111-decision-attestation.md new file mode 100644 index 0000000..0af2f92 --- /dev/null +++ b/text/0111-decision-attestation.md @@ -0,0 +1,166 @@ +# Decision attestation fields + +## Related issues and PRs + +- Reference issues: [cedar-for-agents#76](https://github.com/cedar-policy/cedar-for-agents/issues/76) (scope boundary context) +- Implementation PR(s): TBD + +## Timeline + +- Started: 2026-04-21 +- Accepted: TBD +- Stabilized: TBD + +## Summary + +Add two optional fields to Cedar's `AuthorizationResponse` so downstream systems can bind a decision to (a) a stable identifier and (b) the exact policy set that produced it. + +- `decision_id: Option`, a unique time-ordered identifier for the decision +- `policy_set_hash: Option`, a SHA-256 hash of the canonical policy set used + +Both fields are additive, both are optional, and both default to `None`. Zero breaking changes. + +## Motivation + +Cedar is increasingly used as the policy layer for systems that maintain persistent audit trails: agent-governance toolkits, compliance frameworks, signed-receipt emitters, transparency-log anchors, and multi-tenant authorization services. Every one of those downstream systems needs to answer two questions that are currently hard: + +1. **"Is this the same decision we saw before?"** Without a canonical `decision_id`, every downstream system generates its own identifier. The result is fragmentation: two systems that need to cross-reference the same decision cannot do so without a side-channel mapping. + +2. **"Which policies produced this decision?"** Without a canonical `policy_set_hash`, a consumer cannot attribute a past decision to a specific policy version once policies have rotated. Audit chains that say "decision at time T was permitted" cannot prove that the permit came from the same policy set a reviewer approved. + +Today, downstream systems work around the gap by: + +- Generating their own UUIDs, in incompatible formats across systems (v4, v7, ULID, integer sequences). +- Recomputing policy digests externally, which duplicates Cedar's internal state, requires re-parsing the policy set outside Cedar, and loses whatever canonicalization fidelity Cedar's internal representation provides. + +Both workarounds are less reliable than a field on the response produced at the point of evaluation. + +## Detailed design + +### Response shape + +```rust +pub struct AuthorizationResponse { + pub decision: Decision, + pub diagnostics: Diagnostics, + + // New fields, both optional, default None + pub decision_id: Option, + pub policy_set_hash: Option, +} +``` + +### `decision_id` + +- Format: UUID v7 (time-ordered, RFC 9562). +- Generated by the Authorizer at the time of the `is_authorized` call, after policy evaluation completes. +- Opt-in via a request-level config field (see "Opt-in mechanism" below), to avoid imposing UUID generation cost on callers that do not need it. + +Rationale for v7 over v4: + +- v7 is time-ordered. Downstream log and audit tooling benefits from this property: decisions sort naturally by generation time without a separate timestamp field. +- v7 preserves the uniqueness guarantees of v4 with only marginally less entropy. +- v4 is already widely used for Cedar's existing identifiers where time-ordering is not needed. v7 differentiates "this is a decision identifier" from other UUIDs in logs. + +### `policy_set_hash` + +- Format: `"sha256:" + 64-character lowercase hex string`. +- Computed over: the canonical JSON representation of the policy set used for this evaluation, where canonical means: + 1. Policies sorted lexicographically by policy ID. + 2. Each policy's source text normalized (trailing whitespace stripped, line endings normalized to `\n`). + 3. The resulting list of `(id, source)` pairs JCS-canonicalized (RFC 8785). + 4. SHA-256 over the canonical byte sequence. +- Opt-in via the same request-level config field as `decision_id`. + +This hash is NOT a substitute for `diagnostics.policy_ids`. That existing field returns the set of policy IDs that applied to the decision. `policy_set_hash` is a single stable hash over the entire policy set, regardless of which policies applied. Downstream systems that want to prove "this decision was produced under policy set P" check the hash against a reference value; they do not need to re-parse the policy set. + +### Opt-in mechanism + +A new field on the existing request or authorizer config: + +```rust +pub struct RequestConfig { + // ... existing fields ... + pub attest_decision: bool, // default false +} +``` + +When `attest_decision: true`, the authorizer populates both `decision_id` and `policy_set_hash` in the response. When `false` (the default), both fields remain `None`. + +A per-request flag (rather than a per-authorizer flag) lets a single authorizer handle both audited and non-audited paths without rebuilding. For systems that always want attestation, the flag can be set in a wrapper or middleware without changes to Cedar. + +### What this does NOT do + +- Does not change policy syntax, evaluator semantics, or any existing public API outside the response struct. +- Does not propose any signing, verification, or transparency-log integration. Those belong in downstream systems consuming the new fields. +- Does not enforce `decision_id` uniqueness across authorizer instances. Two authorizers handling two requests may generate UUIDs independently; they are unique per-request but not globally coordinated. +- Does not include entity store state in the hash. `policy_set_hash` covers policies only. Entities are runtime state and including them would make the hash non-deterministic across calls with different entity sets. + +## Drawbacks + +1. **Slight complexity added to `AuthorizationResponse`.** Two new optional fields on an otherwise stable type. Mitigated by making both optional with `None` defaults. + +2. **UUID generation and hash computation cost.** Both are small in isolation, but not free. Mitigated by the opt-in flag. + +3. **Policy canonicalization is load-bearing.** If the canonicalization rules change across Cedar versions, a hash computed under v0.X differs from v0.Y for the same policy set. Mitigated by defining the canonicalization rules in this RFC and treating changes to them as breaking changes requiring a major-version bump of `policy_set_hash`. + +4. **Encourages downstream systems to depend on Cedar internals.** A system that stores `decision_id` and `policy_set_hash` in its audit log has a dependency on Cedar's output format. Mitigated by both fields being protocol-stable strings / UUIDs with no internal-type leakage. + +## Alternatives + +### Alternative 1: external attestation service + +Build a separate service that wraps Cedar's authorizer and emits attestations. Consumers go through the wrapper rather than calling Cedar directly. + +Rejected because: +- Requires every deployment to stand up another service. +- The wrapper service must replicate Cedar's canonical policy form (duplicating internal state). +- Adds network latency to every authorization call. +- Does not solve the underlying fragmentation problem; now every wrapper has its own ID format. + +### Alternative 2: opaque token instead of structured fields + +Return a single opaque string `attestation_token` that encodes both the ID and the hash. + +Rejected because: +- Field-level access is more useful for log correlation, policy versioning queries, and transparency-log anchoring. +- Consumers would have to parse the token back into fields anyway. +- Adds encoding/decoding cost without benefit. + +### Alternative 3: always-computed instead of opt-in + +Simpler API. Every response always carries both fields. + +Rejected because: +- Imposes UUID generation and hash computation cost on every authorization call. +- Many Cedar deployments (stateless policy checks, internal API gates) have no use for either field. +- Opt-in preserves performance for callers that do not need the fields without preventing them from opting in. + +### Alternative 4: `policy_set_hash` as a method on Authorizer rather than a response field + +Expose a separate method `Authorizer::policy_set_hash() -> String` that returns the hash independently of any specific decision. + +Rejected because: +- Decouples the hash from the decision it produced. A caller must correlate "which hash was in effect at decision time" separately. +- Does not solve the "pin this decision to this policy version" problem cleanly. +- Keeping the hash on the response binds decision and policy version atomically. + +## Open questions + +1. **`decision_id` UUID version.** Proposed v7 for log correlation. Open to v4 if the Cedar team prefers to match existing conventions. + +2. **Canonicalization rules.** The proposal above uses sorted policy IDs + JCS canonicalization + SHA-256. Open to alternatives if the Cedar team has an internal canonical form they prefer to expose. + +3. **Opt-in field location.** The proposal suggests a `RequestConfig::attest_decision: bool` flag. Open to placing the flag on `AuthorizerConfig` instead if that matches existing conventions better. + +4. **`policy_set_hash` scope: policies only, or policies plus entity type definitions (schema)?** Current proposal is policies only, because entity definitions do not change during evaluation and do not contribute to the "which policies produced this" question. Open to including schema-hash as a separate field if the Cedar team sees value. + +5. **Interaction with `diagnostics.policy_ids`.** The two fields serve different purposes (`policy_ids` is the set that applied; `policy_set_hash` is the entire available set). Worth documenting the distinction clearly in the response type docs to prevent confusion. + +## Acknowledgments + +This RFC sits within the language-layer scope that Cedar owns. It does not cross the scope boundary established in [cedar-policy/cedar-for-agents#76](https://github.com/cedar-policy/cedar-for-agents/issues/76), which redirected domain schemas to community venues. Decision attestation fields are general-purpose infrastructure useful to any Cedar consumer that maintains audit trails, not specific to agent governance. + +The motivating use cases include AI-agent governance systems (via the author's work on [draft-farley-acta-signed-receipts](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/) and [@veritasacta/verify](https://github.com/VeritasActa/verify)), compliance frameworks, and transparency-log integrations ([in-toto attestation#549](https://github.com/in-toto/attestation/pull/549)). + +Thanks to the Cedar team for the clear scope boundary discussion on #76 and the design-vocabulary precedents in RFCs [#58](https://github.com/cedar-policy/rfcs/pull/58) (Cedar standard library) and [#69](https://github.com/cedar-policy/rfcs/pull/69) (Schema libraries). This RFC follows those conventions where they apply.