approval-engine/docs/caller-authentication.md
tegwick 62233c7c52 Say what the tenant check is, and what view_hash is not
gate-house (GH-DEC-2026-013) was right that our wording implied a fact
about the person. The tenant comparison is store isolation — does this
caller belong to the store this engine serves — and a registration-
supplied claim satisfies that while satisfying no doctrine about the
approver's own membership. Exact equality cannot see the difference, so
state it, and note that provenance gets recorded on the entry the way v4
records principal_type once the claim carries it.

Answer informed-decision's R3 in the claim contract: view_hash and
binding.digest answer different questions and must not be merged. Three
hashes, three questions. Recommend their binding document carry our
digest rather than re-canonicalize the same five fields, so the act has
one canonicalization and a mismatch is detectable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyybaE7DUXrWYrhbnESCTe

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1275879@bnt-lap001
Assistant-Session: eb464208-f821-41b2-bc5a-a6c33d92a8ad
2026-09-10 07:56:28 +02:00

4 KiB

Caller authentication

Production accepts only RS256 JWTs verified against KeyCape JWKS with the exact configured issuer and approval-engine audience. exp, iat, sub, principal_type, tenant, roles, scope, and assurance are mandatory. Missing or unverifiable credentials fail closed. The development static-token mode is explicit, file-backed, and refused with --production. The verified tenant must exactly match the service's configured store tenant; cross-tenant reads and mutations are rejected before object lookup.

This check is store isolation, and it is not a statement about the person. It answers "does this caller belong to the store this engine serves", not "is this human a member of the platform zone". GH-DEC-2026-013 rules that a key-cape human tenant claim may be supplied by the client registration rather than asserted by the directory, and forbids a consumer treating the two as equivalent for a decision turning on a fact about the person. Exact string equality cannot see that difference — a string that matches exactly matches whoever asserted it — so the difference is stated here instead.

Consequences this engine accepts deliberately:

  • A registration-supplied tenant is admissible for admission to this store. Admission means "arrived through a channel the platform registered", the approver client is static and deployment-owned, and dynamic client registration is excluded from key-cape by design.
  • A registration-supplied tenant is not admissible for any doctrine that turns on the approver's own membership — "an approver must be a member of the platform zone" is a fact about the person, and this claim cannot carry it. No such doctrine exists today; if gate-house issues one, it needs a directory-sourced claim and this check does not become that claim by matching.
  • When key-cape emits provenance alongside the tenant, this engine records it on the approver entry the way schema v4 records principal_type — an evidence reader should not have to infer provenance from a string that cannot carry it.
Route Required scope
create approval approval:create
get approval or claim approval:read
add approval entry approval:approve
revoke approval:revoke
supersede approval:supersede
consume approval:consume and service/agent principal
cadence, outbox, storage approval:observe
explicit heartbeat approval:emit

Create additionally requires binding.actor == sub. Approval-entry subject, assurance, evidence reference, and principal type are derived from the verified JWT, never the request body.

principal_type is recorded on the entry (schema v4) because subject_id alone cannot answer what kind of principal bound the approval — user:alice is a naming convention, not a verified claim. This engine does not restrict /entries to human principals: whether a service or agent may supply approver evidence is approval doctrine and belongs to gate-house, and the approval-engine-operator registration holds approval:approve today. What this engine owes is that the evidence chain says which it was. Entries written before v4 stay null rather than being back-filled into a claim nobody made. KeyCape owns client registration and scope grants; approval-engine only verifies and enforces them. Requested registrations are:

  • audience/resource server approval-engine with the scopes above;
  • the secrets-engine PEP service client with approval:read and approval:consume;
  • separately governed lifecycle/operator clients with only their needed mutation or observation scopes.

Client credentials belong in OpenBao/operator custody and must not be placed in manifests, logs, State Hub, or this repository.

KeyCape's OpenBao service-auth contract currently emits aud as the OAuth clientId. That pattern must not be reused here. Tokens presented to this API MUST have resource-server audience approval-engine. Requested non-secret client fragments are in docs/keycape-service-registrations.md.