Skip to content

feat(auth): record the verified client identity signal on the grant - #117

Draft
tnunamak wants to merge 1 commit into
mainfrom
feat/cimd-trust-signal-retention
Draft

tnunamak wants to merge 1 commit into
mainfrom
feat/cimd-trust-signal-retention

Conversation

@tnunamak

@tnunamak tnunamak commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

When a client presents an https URL as its client_id, the AS verifies domain control and then throws the result away. The resolver returns a bare RegisteredClient, so nothing on the issued grant records which signal the AS relied on, or when it looked it up. spec-core.md#trust-registry-queries requires both. On main, trust_signal, looked_up_at and framework_uri match zero times in the tree, so anyone auditing a grant after the fact cannot tell whether the client's identity was ever checked.

This stamps the record where the check happens, inside resolveCimdClientForGrant, and reports it under pdpp.trust_signal in introspection. It carries method as well, separating the same-origin branch from the https fetch, so the decision can be reproduced later rather than re-derived.

The record goes in a new nullable grants.trust_signal_json column, not inside grant_json. ResolvedGrantSchema is vendored with additionalProperties: false, so an extra member there would fail every existing grant read. Pre-existing grants read NULL and are not back-filled. No signal was relied on for them, and inventing one would be the opposite of the point.

Staged-batch approval also now forwards the request's base URL into its re-resolution. That drop is pre-existing on main. Without the base URL, CIMD batches fall through to a network self-fetch and fail at approve, so the batch case below cannot be exercised without fixing it.

What the evidence shows

The leg-3 oracle from #56 fails 1 of 3 on main and passes 3/3 here, on SQLite and Postgres 16 — a grant issued through the https-document path reads its signal back through introspection.

cimd-trust-signal-batch-path.test.ts issues a staged batch and asserts each child reports the record through its own token's introspection, so the stamp survives the re-resolution path rather than only the direct one. Both backends.

Two controls were run and reverted after. Dropping g.trust_signal_json from the Postgres join reddens Postgres alone. Restoring the record onto the pending request's client, rather than the resolved child, reddens the batch test at review. Each assertion therefore depends on the change it claims to cover.

For the upgrade path, the column was dropped, a legacy row inserted, and bootstrap re-run on both backends: the column returns, the row survives, and it reads NULL.

Not verified

method: "https_document_fetch" is not exercised end to end. Both harnesses are same-origin by construction, so that branch is covered only by cimd.test.ts with an injected fetchImpl.

Assisted-by: AI

When the authorization server accepts a client whose client_id is an https
URL, it satisfies itself of that client's identity by retrieving the metadata
document from that URL and confirming the document names the same client_id
back — verified domain control, spec-core.md#client-display obligation 5. It
then discarded the fact. A relying party reading a grant back could see which
client held it but not which signal the server relied on to believe that, nor
when it looked.

spec-core.md#trust-registry-queries requires the record: "An authorization
server records the trust signal it relied on ... on its acceptance record or
resulting grant. The lookup time matters because a status may be withdrawn
later, and the record has to show what was true when the server relied on it."

Persist the reliance record on the grant and report it under `pdpp.trust_signal`
in introspection. It is captured at the moment of reliance, in the one function
that resolves a URL-hosted identity, so the lookup time is truthful rather than
re-derived at issuance. A grant issued to a pre-registered client carries no
signal and reports none: the server relied on its own registration table, not
on an assertion, and absence is the honest record of that.

The record lives in a new `grants.trust_signal_json` column rather than inside
`grant_json`, because the resolved-grant contract is `additionalProperties:
false` and ships from a vendored package this repo does not author; an extra
member there would fail every existing grant read. The column is additive and
nullable on both backends, so grants issued before it existed read as NULL. No
signal is back-filled, because none was relied on.

The record never rides the pending request. Approval re-resolves the client a
moment before it issues, so the grant takes the signal from that resolution on
every path, and the approval review artifact keeps its closed client shape. The
reliance record does not belong in that artifact in any case: it is grant
provenance, not a term the owner agreed to.

Beyond the tuple the spec names, the record carries one field: `method`,
distinguishing a same-origin document from an https fetch. A relying party
needs it to reproduce the decision the server made.

Forward the request's base URL through the staged-batch approval as well. The
re-resolution needs the issuer origin to read a same-origin metadata document
from local storage; without it the batch path falls through to a network
self-fetch of the server's own origin, which is what a CIMD client staging a
batch hits today. The forward is conditional on a base URL being present, so
every other batch makes the same call as before.

Carry the spec oracle this makes pass, and assert the record on a
batch-issued child grant under both storage backends.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
@tnunamak
tnunamak force-pushed the feat/cimd-trust-signal-retention branch from 5f473ad to 6714cf4 Compare September 14, 2026 16:12
@tnunamak tnunamak changed the title feat(auth): retain the trust signal the AS relied on, on the grant feat(auth): record the verified client identity signal on the grant Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant