# Canonical request digest Status: published Date: 2026-09-02 Standard: security-layer-model_v0.7 ยง6.4.2 The digest is the mechanical test for replaying a flex-auth verdict. A consumer may reuse an allow **iff** a new request produces the same digest **and** the decision's lifetime still holds. Negative caching of a deny uses the same test. Constructor: `api.NewDecisionBinding` / `api.RequestDigest`. Field: `DecisionEnvelope.binding.request_digest`. Format: `sha256:` plus 64 lowercase hex characters. ## What is hashed Canonical JSON (Go `encoding/json`, which sorts map keys) of: | Field | Source | | --- | --- | | `tenant` | `CheckRequest.tenant` | | `subject` | normalized `CheckRequest.subject` | | `action` | `CheckRequest.action` | | `resource` | normalized `CheckRequest.resource` | | `context` | `CheckRequest.context` | Empty optional maps and omitempty strings are omitted. Attribute maps inside subject and resource are part of the material once the evaluator has enriched them from the registry. **Not hashed:** `id`, `policy_version`, and `caring_context`. The request id is correlation only. Policy version is recorded in provenance. CARING context is hashed separately as `provenance.input_claim_digests.caring_context`. ## Normalization 1. Tenant on the request is copied onto subject and resource when those refs omit it. 2. A registry hit copies type, tenant, and selected attributes onto the refs the digest sees. 3. Context is a shallow copy. Key order is not significant. A consumer that re-hashes the **original** unenriched request will not match a decision that turned on registry attributes. Compare structured `binding` fields to the proposed action, and treat `request_digest` as the evaluator's statement of what it hashed. To recompute independently, hash the same normalized tuple the binding carries (tenant, subject, action, resource, context). ## Stability The digest is stable across process restarts for the same normalized tuple. It is **not** stable across: - a change to any hashed field, including registry-enriched attributes - a change to this contract (would require a new `contract_version`) It does not include the policy package, snapshot, or clock. Those live in provenance and `lifetime`. Two allows over the same tuple but different snapshots share a digest and differ in `registry_snapshot_digest`. ## Worked example โ€” permitted retry Request (after enrichment): ```json { "tenant": "tenant:alpha", "subject": {"id": "user:alice", "type": "Human", "tenant": "tenant:alpha"}, "action": "read", "resource": { "id": "document:internal-note", "type": "document", "system": "markitect-tool", "tenant": "tenant:alpha" }, "context": {"purpose": "project-delivery"} } ``` The evaluator stores `binding.request_digest` for that tuple and `lifetime.expires_at` in the future. The same subject, action, resource, and context presented again before expiry is a permitted retry of that allow. ## Worked example โ€” refused replay Any of the following refuses replay: - `action` changes from `read` to `destroy` (digest mismatch) - `resource.attributes.stage` changes from `build` to `production` (digest mismatch) - `lifetime.expires_at` is in the past (lifetime does not hold) - the stored effect is `deny` and the consumer is attempting to treat a cached refusal as still binding after a policy change โ€” negative cache is valid only for the same digest **and** while the recorded lifetime would have held; a new `Check` is required once either fails Local fixtures, State Hub `/decisions/{uuid}` records, and free-form `decided_by` values are not this test.