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
|
|
@ -481,13 +481,75 @@ to end; what remains is deployment only.
|
|||
pattern: unit tests with self-consistent fakes hid all three, and each was
|
||||
found only by a real artifact or a real chain.
|
||||
|
||||
Destroy gate closed 2026-09-06 (inbound `FLEX-DEC-2026-007`, `GH-DEC-2026-008`,
|
||||
approval-engine schema v3). The vocabulary mapping this task was waiting on is
|
||||
not coming, and it should not have been waited on: gate-house rejected it
|
||||
outright, for the reason flex-auth and approval-engine both gave independently —
|
||||
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 of it are now enforced here.
|
||||
|
||||
- **The circularity we reported is fixed at source.** A `pdp_digest` recorded at
|
||||
approval-issue time can never equal the `request_digest` of a request that
|
||||
carries the claim in its hashed context. With `GH-DEC-2026-008` requiring that
|
||||
equality on the PDP path, destroy would have been permanently un-allowable,
|
||||
failing closed forever on a check no correct record could pass. flex-auth
|
||||
published `binding.approval_binding_digest` (commit `dd3ce4c`): the same
|
||||
canonical digest with `context.approval` removed, stable across attaching the
|
||||
claim. The comparison is against that field, never `request_digest`.
|
||||
- **`request_digest` deliberately still covers the claim**, so the two digests
|
||||
disagree on a claim-bearing request and that disagreement is load-bearing: two
|
||||
requests differing only in which approval was presented must not share a
|
||||
replay identity, or an allow obtained with a valid claim could be replayed
|
||||
against a request carrying none. `approval_binding_digest` is not a replay
|
||||
identity and is not used as one.
|
||||
- `authorization.approval_binding_digest` implements the published exclusion
|
||||
rule, including flex-auth's `json:"context,omitempty"` behaviour of dropping a
|
||||
context that strips empty. `digest_material` now drops an empty context for the
|
||||
same reason. `validate_decision_envelope` recomputes the field when present
|
||||
rather than trusting it, refuses a claim-bearing request whose decision records
|
||||
none, and takes the claim's digest from step 1 as `expected_approval_binding_digest`.
|
||||
- **`binding.pdp_path` is required true** before `pdp_digest` is used at all.
|
||||
It is approval-engine's declaration that the approval was requested against a
|
||||
bound CheckRequest; their `create()` refuses `pdp_path` true without a digest,
|
||||
so the declaration guarantees the digest. The converse is not inferred: a
|
||||
digest present for some other reason is no declaration, and pre-v3 approvals
|
||||
carry `pdp_path` false regardless of any digest they hold.
|
||||
- The cost is accepted rather than engineered around: an approval requested
|
||||
without a bound CheckRequest is not usable on this path and will not become
|
||||
usable later. If a real destroy workflow cannot bind at issue, that is the
|
||||
falsifier gate-house wrote into the reversal and it gets raised, not
|
||||
worked around.
|
||||
|
||||
The replay fixtures are re-vendored from `dd3ce4c`, and the pins moved a second
|
||||
and final time (`request_digest` `sha256:c749ee2d…`, context input-claim digest
|
||||
`sha256:8b73d29e…`; `approval_binding_digest` `sha256:fa07becf…`, which did
|
||||
**not** move — that is the point). The fixture now demonstrates the property
|
||||
instead of asserting it: `test_pdp_digest_equals_the_published_approval_binding_digest`
|
||||
rederives `fa07becf…` from the fixture's own request through our canonical
|
||||
implementation, which is a hermetic proof that this engine hashes the same
|
||||
material flex-auth does. A pin alone would only have asserted the constant.
|
||||
`decision_rotate.json` is unchanged and carries no `approval_binding_digest`;
|
||||
a test pins that omission so the field is not silently duplicated onto ordinary
|
||||
decisions.
|
||||
|
||||
Our own CheckRequest stays claim-free (`context` is `{"purpose": …}`), so the
|
||||
identity holds transitively today; the code is already correct for the day we
|
||||
carry a claim in context.
|
||||
|
||||
Still open and still ours: the T04 consumer-shape question flex-auth has asked
|
||||
twice — operator CLI reaching an in-cluster pin, a workload we do not have yet,
|
||||
or no pin at all. It is a deployment-shape answer, not engine work, and it is
|
||||
the reply owed on that thread.
|
||||
|
||||
Remaining to go live is configuration and deployment, not engine work:
|
||||
`SECRETS_ENGINE_PDP_URL`/`_PDP_TOKEN_FILE` (awaiting the `flex-auth-secrets-engine`
|
||||
pin, `FLEX-WP-0021-T04`/`T05`), `SECRETS_ENGINE_APPROVAL_URL`/`_TOKEN_FILE`
|
||||
(awaiting `APPROVAL-WP-0002-T03`), the policy pin (published but not to be set
|
||||
until T05), a KeyCape RS256 credential in place of the static Bearer token, and
|
||||
`approval.authorization_id` on each lane. Destroy additionally needs the
|
||||
vocabulary mapping or a `pdp_digest` guarantee.
|
||||
`approval.authorization_id` on each lane. Destroy's additional requirement is
|
||||
now met in code: the `pdp_path` + `approval_binding_digest` identity replaced the
|
||||
vocabulary mapping that was never going to exist.
|
||||
|
||||
Define and enforce the decision contract needed by production commands. A
|
||||
resolved approval must bind at least:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue