Verifying agents with the registry
This guide is for a relying party: a merchant, an API or any service that receives requests from AI agents and decides whether to act on them. It covers looking an agent up in the registry, what anyone can read and what an authenticated relying party reads in addition, and the signed registry manifest a relying party without OpenID Federation support uses to check Agent Passports and attestations offline. The normative text isspec/registry-federation.md (“Agent lookup” and
“Registry manifest”). The examples below use registry.example for the
registry, issuer.example for an accredited issuer, provider.example for
the agent’s provider and merchant.example for you. The example agent is
shopper-01, running Nimbus Shopper 2.4.
Before you start
The unauthenticated lookup and the manifest are served only when the registry operator setsREGISTRY_PUBLIC_ENDPOINTS_ENABLED=true. Without it,
the lookup answers only requests that carry a developer API key, and
/.well-known/agent-registry.json does not exist.
Looking an agent up
Look an agent up by its DID:lookup-by-did
lookup-by-key
lookup-by-credential
404 as for a credential the registry has
never seen, so there is nothing to learn from guessing. Leaving one out is
400.
Reading the answer
lookup-by-key-response
levelis computed when you ask:basic,verified,attestedorattested_verified. A suspension anywhere in the chain readsbasic. Compare it with your policy’s minimum and deny withlevel_below_policy.flagswarn of something you may want to act on, such askey_compromisedorissuer_suspended.key_currentsays whether the key may sign now:active, orrotatedand still inside its overlap. If the key that signed the request is not current, deny withkey_not_active.attestationslists what counts toward the level, with each expiry.
What is public and what needs a key
To read the second column, send your developer API key as
Authorization: Bearer <key>. A key the registry does not accept is refused
with 401, never answered as public. In Phase 1 a developer API key is the
relying-party credential; a dedicated one will follow.
Public answers may be cached for 60 seconds (Cache-Control: public, max-age=60); authenticated answers are private and revalidated on every
read. Send the ETag back in If-None-Match to get 304 when nothing has
changed. Each lookup route allows 120 requests a minute per client address.
The registry manifest
To check an Agent Passport or an attestation without calling the registry for each one, fetch the signed manifest:manifest
application/grantex-registry-manifest+jwt, not
JSON. It lists every accredited issuer with its status and current keys, the
trust mark types, the registry’s acceptance status lists and the lookup
endpoints. Before using it:
- Fetch the registry’s JWK Set from
https://registry.example/.well-known/jwks.json. - Check the protected header:
typis exactlygrantex-registry-manifest+jwt,algisRS256orES256, and there is nojwk,jku,x5u,x5corcrit. - Verify the signature with the key from the JWK Set whose
kidmatches, under that algorithm only. - Check
issis the registry you trust. Configure that value; never take it from the manifest, and refuse every manifest if it is not set. - Check freshness: now is before
exp, not more than one hour afteriat, andiatis not in the future.
passport_invalid_signature for a manifest that does not verify,
status_stale for one that is too old). Never fall back to an expired copy.
Then, for a passport or attestation signed by an issuer:
- find the issuer by
entity_idinissuers; if it is absent orwithdrawn, deny withissuer_not_accredited; ifsuspended, deny withissuer_suspended; if itstrust_marksdo not include the type you need, deny withtrust_mark_missing; - verify the signature only with a key from that issuer’s
jwks(revoked keys are already left out); - check the registry’s acceptance entry in the status list the attestation
names (
acceptance_status_lists) and deny withattestation_not_acceptedunless it is VALID.
ETag stays the same while
the registry has not changed and the manifest has not been re-signed, so a
conditional request is cheap. The manifest route allows 60 requests a minute
per client address.
Verifying a request in Python
grantex-verifier (packages/verifier-py, 0.1.0, not yet published)
does everything above for each request: it checks the manifest, the Agent
Passport against the issuer’s keys in it, both status lists, the grant, its
revocation and audience, that the passport, the grant and the request
signature name one key, the key’s status in the lookup, the RFC 9421
signature and the transaction’s fit with the grant. The checks, their order,
the staleness matrix and the denial codes are specified in
spec/verification.md section 7.
make_config once when the process starts and pass the same
configuration to every verify_checkout call. Its nonce store is what
refuses a replayed request (request_signature_invalid); a configuration
built per request starts with an empty store and accepts every replay. If
several processes serve the same origin, give them one shared nonce store.
result.checks reports every check with ok, detail and cached_at, and
result.denial_code is the code of the first that failed. Keep
result.evidence with the order: it records the passport’s hash, the
attestation id, both status results and the level when you verified.
To verify in front of a WSGI application (an ASGI middleware is included
too):
401 for a request signature failure, 503
for status_stale (the request may be good, but you could not establish
it) and 403 for any other code, with {"denial_code": ..., "check": ...}.
Both examples are run by the package’s tests.