Close the remaining PDP obligations: mechanical layer declaration check, registry-snapshot digest in provenance, explicit allow TTL, per-input-class freshness deadlines, and the published decision-record contract. Document the canonical request digest as the §6.4.2 replay test. Assistant: grok Assistant-Session: 01a06256-fb71-7102-b3a9-27e6734257d0
98 lines
3.6 KiB
Markdown
98 lines
3.6 KiB
Markdown
# 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.
|