approval-engine/docs/keycape-service-registrations.md
tegwick 5c87ba8610 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
2026-09-06 20:35:59 +02:00

4.3 KiB

Requested KeyCape registrations

Status: requested by APPROVAL-WP-0002-T01. Non-secret. KeyCape owns issuance, client disablement, and the exact claim contract. This file is a consumer request, not a live registration.

Tokens presented to approval-engine MUST use resource-server audience approval-engine. Do not reuse the OpenBao service-auth pattern that sets aud to the OAuth clientId.

Required claims remain those in docs/caller-authentication.md: iss, sub, aud, exp, iat, principal_type, tenant, roles, scope, assurance. principal_type for consume callers must be service or agent.

Resource server

Field Value
Audience approval-engine
Issuer the deployed KeyCape issuer (manifest uses https://kc.coulomb.social)
JWKS GET /jwks on the KeyCape service
Scopes approval:create, approval:read, approval:approve, approval:revoke, approval:supersede, approval:consume, approval:observe, approval:emit

Tenant reconciliation — unresolved, blocks token issuance

This is a live collision, not a naming preference. ApiApplication.identity compares the verified JWT tenant claim to the engine's configured store tenant with exact string equality and raises Forbidden before any object lookup (approval_engine/api.py:66). There is no mapping table, no normalisation, and no prefix handling anywhere in this engine.

The three values currently in play:

Value Where it is set Current content
Store tenant deploy/approval-engine.yaml --tenant (CLI default platform) platform
JWT tenant claim the client registrations below tenant:coulomb
CheckRequest tenant flex-auth policy subject tenant:platformnever read by this engine

platform != tenant:coulomb, so tokens issued under the registrations below would be denied 403 on every non-health route. Spelling similarity between platform and tenant:platform is not a mapping either; the flex-auth policy subject is a PDP input this engine never inspects, so it cannot participate in the comparison at all.

Denial evidence: tests/test_auth.py::test_wrong_tenant_is_forbidden — a signature-valid token whose tenant differs from the store tenant is refused without mutation.

Resolution is an owner decision and is deliberately not taken here. Either KeyCape issues tenant: platform to match the store, or this repo's manifest sets --tenant tenant:coulomb to match the registration. Which is correct depends on whether platform and tenant:coulomb name the same layer — a question this engine cannot answer, and answering it wrongly grants a token cross-tenant access to the approval store. The values below are left as requested until that mapping is stated by an owner, so the mismatch stays visible rather than being silently resolved by whichever document was edited last.

Tracked against APPROVAL-WP-0002-T01; related KEY-WP-0013-T02, SECRETS-WP-0009-T03.

Clients

Confidential client secrets stay in OpenBao/operator custody. secretRef names below are placeholders for that custody path.

clients:
  - clientId: secrets-engine-approval
    displayName: secrets-engine PEP consume client
    audience: approval-engine
    allowedScopes: [approval:read, approval:consume]
    grantTypes: [client_credentials]
    clientType: confidential
    secretRef: env:KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET
    serviceSubject: service:secrets-engine
    principal_type: service
    tenant: tenant:coulomb
    roles: [secrets-engine]
    tokenLifetime: 15m

  - clientId: approval-engine-operator
    displayName: approval-engine lifecycle operator
    audience: approval-engine
    allowedScopes:
      - approval:create
      - approval:read
      - approval:approve
      - approval:revoke
      - approval:supersede
      - approval:observe
      - approval:emit
    grantTypes: [client_credentials]
    clientType: confidential
    secretRef: env:KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET
    serviceSubject: service:approval-engine-operator
    principal_type: service
    tenant: tenant:coulomb
    roles: [approval-operator]
    tokenLifetime: 15m

Human approvers use the existing KeyCape human flow with approval:approve only, still with aud=approval-engine. They must not receive approval:consume.