feat: bind the destroy gate to approval_binding_digest and pdp_path
The vocabulary mapping this path was waiting on is not coming: gate-house rejected it in GH-DEC-2026-008, because a translation can be confidently wrong and fails open by accepting a claim approved for a different action. The stronger option arrived instead, and both halves are enforced here. flex-auth published binding.approval_binding_digest (FLEX-DEC-2026-007) to fix the circularity this repo reported: a pdp_digest recorded at issue time can never equal the request_digest of the request that carries the claim in its hashed context, so with GH-DEC-2026-008 requiring that equality, destroy would have failed closed forever on a check no correct record could pass. - authorization.approval_binding_digest implements the published exclusion rule, including Go's context,omitempty behaviour when stripping empties the context; digest_material drops an empty context for the same reason. - validate_decision_envelope recomputes the field rather than trusting it, refuses a claim-bearing request whose decision records none, and compares the claim's digest from step 1 against it -- never against request_digest, which still covers the claim so it stays a sound replay identity. - validate_approval_claim requires binding.pdp_path true before using pdp_digest at all. Path intent is never inferred from a digest that happens to be present; pre-schema-v3 approvals carry pdp_path false regardless of any digest they hold. Replay fixtures re-vendored from dd3ce4c. The destroy pins moved a second and final time; approval_binding_digest did not, which is the point. The fixture now demonstrates the property instead of asserting it: we rederive fa07becf... from its own request through our canonical implementation, proving we hash the same material flex-auth does rather than pinning a constant we cannot reproduce. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
This commit is contained in:
parent
67b28f48a8
commit
c44306b1b2
11 changed files with 411 additions and 40 deletions
|
|
@ -43,16 +43,62 @@ never compared to each other:
|
|||
of `{action, actor, principal, purpose, target}`.
|
||||
- `decision.binding.request_digest` — flex-auth canonical CheckRequest digest.
|
||||
|
||||
`claim.binding.pdp_digest` is the **only** comparison usable today, and its
|
||||
absence fails the action closed. The claim's `binding.action` and
|
||||
`binding.target` speak approval-engine's vocabulary (`secrets.kv.destroy`,
|
||||
`{"id": ..., "stage": ...}`) while ours speaks the catalog's (`destroy`,
|
||||
`catalog:<id>`), and no mapping between them is published. flex-auth makes no
|
||||
cross-check either and states the correspondence is ours. Computing a native
|
||||
digest from our own vocabulary would compare two different languages and never
|
||||
match, so this engine does not compute one. Closing that gap needs a published
|
||||
mapping co-authored by approval-engine and flex-auth; it is a prerequisite for
|
||||
`SECRETS-WP-0007-T04` making `destroy` reachable.
|
||||
`claim.binding.pdp_digest` is the **only** comparison usable, and its absence
|
||||
fails the action closed. The claim's `binding.action` and `binding.target` speak
|
||||
approval-engine's vocabulary (`secrets.kv.destroy`, `{"id": ..., "stage": ...}`)
|
||||
while ours speaks the catalog's (`destroy`, `catalog:<id>`). No mapping between
|
||||
them exists and none is coming: gate-house rejected one outright in
|
||||
`GH-DEC-2026-008`, because a translation can be confidently wrong and fails
|
||||
**open** by silently accepting a claim approved for a different action. An
|
||||
identity check cannot. So this engine computes no native digest.
|
||||
|
||||
### The two gates on the pdp path
|
||||
|
||||
**1. `binding.pdp_path` must be `true`.** This is approval-engine's declaration
|
||||
(schema v3) that the approval was requested against a bound CheckRequest. Their
|
||||
`create()` refuses `pdp_path` true without a `pdp_digest`, so the declaration
|
||||
*guarantees* the digest. The converse does not hold, and we must not infer it: a
|
||||
digest recorded for some other reason is no declaration anybody made, and every
|
||||
approval issued before schema v3 carries `pdp_path` false regardless of any
|
||||
digest it holds.
|
||||
|
||||
The cost is real and is ours to carry — an approval requested without a bound
|
||||
CheckRequest is not usable here and never becomes usable later. `GH-DEC-2026-008`
|
||||
holds that correct: an approval granted against an unspecified action does not
|
||||
become an approval for a specific one because a consumer later found a use for
|
||||
it. If a real destroy workflow cannot bind at issue, that is the falsifier
|
||||
gate-house wrote into the reversal, and it should be **raised**, not worked
|
||||
around.
|
||||
|
||||
**2. The digest identity, against the right comparand.**
|
||||
|
||||
```text
|
||||
claim.binding.pdp_digest == decision.binding.approval_binding_digest correct
|
||||
claim.binding.pdp_digest == decision.binding.request_digest can never pass
|
||||
```
|
||||
|
||||
`pdp_digest` is recorded at *issue* time, and issue precedes the decision. A
|
||||
request that carries the claim inside its hashed `context` therefore has a
|
||||
different `request_digest` by construction — the claim is part of the material
|
||||
being hashed. This engine found that circularity against the T03 replay fixture;
|
||||
flex-auth fixed it in `FLEX-DEC-2026-007` by publishing
|
||||
`binding.approval_binding_digest`, the same canonical digest with
|
||||
`context.approval` removed, which is stable across attaching the claim.
|
||||
|
||||
`approval_binding_digest` is **not** a replay identity. `request_digest` still
|
||||
covers the claim and still moves when it changes, because two requests differing
|
||||
only in which approval was presented must not share a replay identity — one
|
||||
allows, the other denies `dual_control_required`. Collapsing them would let an
|
||||
allow obtained with a valid claim be replayed against a request carrying none.
|
||||
|
||||
Our own CheckRequest is claim-free today (`context` is `{"purpose": ...}`), so
|
||||
flex-auth emits no `approval_binding_digest` for it and the identity holds
|
||||
transitively: step 1 compares the claim's `pdp_digest` to the canonical digest of
|
||||
the exact claim-free request we are about to send, and step 2 confirms the
|
||||
decision's `request_digest` is that same value. `approval_binding_digest(request)`
|
||||
implements the published exclusion rule so that the check is already correct if
|
||||
we ever carry the claim in context; when the field is present it is recomputed
|
||||
and never taken on faith.
|
||||
|
||||
### What is hashed in the flex-auth digest
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue