Name the execute-time digest target; record the tenant collision
flex-auth published binding.approval_binding_digest (FLEX-DEC-2026-007) after
secrets-engine found that a claim-bearing request's request_digest covers the
carried claim, so it can never equal a pdp_digest recorded before that claim
existed. A consumer obeying GH-DEC-2026-008 against request_digest would have
failed closed permanently on every claim rather than on a bad one.
The value recorded at issue was already correct, so no code changes. What was
wrong was this repo's description of the comparison target: a reader would
reach for request_digest and fail closed forever. The schema and
docs/approval-claim.md now name approval_binding_digest as the execute-time
target and state that request_digest is never it, while leaving the issue-time
description as it stood.
Separately, docs/keycape-service-registrations.md now records a live collision
in the deployment inputs: the manifest serves --tenant platform while the
requested registrations issue tenant:coulomb, and ApiApplication.identity
compares them with exact string equality before any object lookup, so those
tokens would be denied 403 on every non-health route. flex-auth's
tenant:platform is a PDP subject this engine never reads and cannot bridge the
two. The values are left as-is on purpose — resolving it needs an owner
statement on whether the two name the same layer, and guessing grants
cross-tenant access to the approval store. The doc's stale issuer is corrected
to the live https://kc.coulomb.social from 06544b0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM5HnEAhokxdfcPqBNpT7D
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715850@bnt-lap001
Assistant-Session: eb557e93-7cb1-45d0-9e57-7d15b3edc60e
This commit is contained in:
parent
06544b0fb4
commit
5c87ba8610
4 changed files with 115 additions and 12 deletions
|
|
@ -47,7 +47,7 @@ contract.
|
|||
| --- | --- |
|
||||
| `approval_id` | Reconstructable from the decision record. |
|
||||
| `binding.digest` | Distinguishes *approved* from *approved for this exact request*. |
|
||||
| `binding.pdp_digest` | Optional. When recorded at issue, compare to `NewDecisionBinding.request_digest`. |
|
||||
| `binding.pdp_digest` | Optional. When recorded at issue, compare to the PDP's claim-excluded digest (`binding.approval_binding_digest`), not `request_digest`. |
|
||||
| `issuer` | Always `approval-engine`. |
|
||||
| `freshness` | So the PDP can state a deadline for this input class (§9.7.2), not a single fiction covering every source. |
|
||||
| `valid_now` | Current-state predicate. Not permission. |
|
||||
|
|
@ -106,16 +106,18 @@ finds itself wanting one should read that as a signal it is about to compare
|
|||
the wrong two things.
|
||||
|
||||
**`binding.pdp_digest` is the correspondence.** It sidesteps vocabulary
|
||||
entirely: it is the PDP's own `NewDecisionBinding.request_digest`, recorded at
|
||||
issue time, so comparing
|
||||
entirely: it is the PDP's own `NewDecisionBinding` digest, recorded at issue
|
||||
time, so comparing
|
||||
|
||||
```text
|
||||
claim.binding.pdp_digest == decision.binding.request_digest
|
||||
claim.binding.pdp_digest == decision.binding.approval_binding_digest
|
||||
```
|
||||
|
||||
compares the PDP's digest to the PDP's digest, in one vocabulary, with no
|
||||
translation by anyone. That is strictly stronger than a name-to-name mapping
|
||||
could be.
|
||||
could be. On the execute-time side the target is the PDP's *claim-excluded*
|
||||
digest, not `request_digest` — see "The comparison target at execute time"
|
||||
below.
|
||||
|
||||
`pdp_digest` is **always present** on the claim and is `null` when the approval
|
||||
was not issued against a PDP decision — a stated fact rather than a missing
|
||||
|
|
@ -146,6 +148,38 @@ produces a confident wrong answer, and comparing two digests derived under
|
|||
different rules fails open in the direction of accepting a claim bound to a
|
||||
different request.
|
||||
|
||||
### The comparison target at execute time
|
||||
|
||||
The issue-time description above is unchanged: this engine records the
|
||||
`NewDecisionBinding` digest it was handed at issue. What changed is the field a
|
||||
consumer compares it *against* at execute time.
|
||||
|
||||
flex-auth's `request_digest` hashes `context`, and the dual-control pattern
|
||||
carries the claim in `context.approval`. So a claim-bearing request's
|
||||
`request_digest` covers the claim itself, and can never equal a digest recorded
|
||||
before that claim existed. A consumer obeying `GH-DEC-2026-008` literally
|
||||
against `request_digest` would fail closed permanently — not on a bad claim, but
|
||||
on every claim, forever.
|
||||
|
||||
flex-auth resolved this by publishing `binding.approval_binding_digest`
|
||||
(`FLEX-DEC-2026-007`): the same digest material with `context.approval`
|
||||
removed, emitted only when a claim was carried. An approval issued against a
|
||||
claim-free Check records that Check's `request_digest`, and the claim-bearing
|
||||
request reproduces the identical value in the new field. So:
|
||||
|
||||
```text
|
||||
claim.binding.pdp_digest == decision.binding.approval_binding_digest
|
||||
```
|
||||
|
||||
`request_digest` keeps covering the claim on purpose — it is the replay
|
||||
identity, and two requests differing only in which approval was presented must
|
||||
not share one when their decisions differ.
|
||||
|
||||
**No change is required in this engine.** The value recorded at issue was
|
||||
already the right one; only the consumer-side target needed naming. This
|
||||
section names it so a reader of `pdp_digest` does not reach for
|
||||
`request_digest` and fail closed forever.
|
||||
|
||||
### Declared path intent — `binding.pdp_path`
|
||||
|
||||
`GH-DEC-2026-008` requires `pdp_digest` on the `GH-DEC-2026-003` path. This
|
||||
|
|
@ -178,7 +212,7 @@ When the issuer recorded the PDP digest at issue time, it is in
|
|||
`binding.pdp_digest`. **Prefer that comparison** at decision time:
|
||||
|
||||
```text
|
||||
claim.binding.pdp_digest == decision.binding.request_digest
|
||||
claim.binding.pdp_digest == decision.binding.approval_binding_digest
|
||||
```
|
||||
|
||||
and still require `claim.approval_id` to match the approval named on the
|
||||
|
|
@ -204,10 +238,12 @@ A production consumer of this claim, before treating it as an input, checks:
|
|||
2. `issuer` is `approval-engine`.
|
||||
3. `valid_now` is true and `consumed` is false.
|
||||
4. `binding.digest` equals the digest of the binding the consumer computed
|
||||
from the proposed action, **or** `binding.pdp_digest` equals the
|
||||
`NewDecisionBinding` digest of that request. On a privileged lane, take the
|
||||
second: require `binding.pdp_digest` to be non-null and equal, and refuse
|
||||
the claim otherwise. See "Action and target vocabulary" above.
|
||||
from the proposed action, **or** `binding.pdp_digest` equals the PDP's
|
||||
claim-excluded digest for that request — `binding.approval_binding_digest`
|
||||
on a claim-bearing flex-auth decision, never `binding.request_digest`. On a
|
||||
privileged lane, take the second: require `binding.pdp_digest` to be
|
||||
non-null and equal, and refuse the claim otherwise. See "Action and target
|
||||
vocabulary" and "The comparison target at execute time" above.
|
||||
5. `freshness.not_after` is still in the future.
|
||||
6. `reason_code` is `ok`.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue