Posting attestations
Once accredited, an issuer posts each attestation it makes to the registry. An attestation is a compact JWS signed with one of the keys in the issuer’s record; the full profile is inspec/attestation-1.0.md.
There is no API key: the signature is the authentication.
{"typ": "grantex-attestation+jwt", "alg": "ES256", "kid": "issuer-2026-01"},
and the payload, for the agent shopper-01 running Nimbus Shopper 2.4:
attestation-payload
- You are accredited for the type.
typeis one of your trust marks, and your record is neither suspended nor withdrawn. - The agent has proven its key. For
agent.identityandagent.security,key_thumbprintmust be a key the agent registered and proved possession of (POST /v1/agents/{id}/keys/{thumbprint}/challengeand/prove). An attestation for a key that is not yet proven is refused withkey_unproven. - Your status list is reachable.
status.status_list.urimust be under yourstatus_list_base, and the registry fetches it while it checks the attestation:GET,https, no redirects, served asapplication/statuslist+jwt, signed with a key in your record, fresh byexp(oriat+ttl), with the entry VALID. Otherwise the answer isstatus_stale(orpassport_revokedfor an entry that is not VALID). - Your status list stays reachable. The registry relies on each read of
your list until the earliest of its
exp, the time of reading plus itsttl, and one day, and reads it again shortly before then. A revocation or suspension you publish reaches relying parties within that time. While the registry cannot read your list, your attestations stop counting toward trust levels once the last read runs out, and count again after the next successful read. Attlof a few minutes to an hour is a good choice. idis new. Posting the same bytes again is harmless and answers the existing record, without checking them again, so a retry after a timeout succeeds even while your status list is briefly unreachable; other bytes under anidyou have used are409.- The hash is
sha-256:and 43 base64url characters, the SHA-256 of the credential you checked (for an Agent Passport, of its issuer-signed JWT).
201 with the registry’s record. Keep its id (ratt_...) to
withdraw or refresh the attestation later, and its acceptance.status_list:
that is the registry’s own entry saying it accepts the attestation, which
relying parties check next to your status list.
Withdrawing and refreshing
To take an attestation back, or to renew it with a new external credential, sign a request with the same key set and send it inAuthorization:
"typ": "grantex-attestation-request+jwt", and its
payload names the registry, the attestation (by the id you minted) and the
action, with a fresh single-use nonce:
withdraw-request
"action": "refresh", and its body is a complete new attestation for the same
subject and type with a new id and a new external_credential_id. The old
attestation is then superseded, and the registry’s entry for it becomes
INVALID. The registry operator can do either with its operator key instead.
How attestations count
Relying parties do not read attestations one by one: the registry computes a trust level for each agent. An agent isattested when it has an accepted
agent.identity attestation bound to a key it has proven and its provider has
an accepted provider.entity attestation, both from accredited issuers that
are not the provider itself; attested_verified when its provider’s domain is
also DNS-verified. A suspension of the agent, its provider, an attestation or
its issuer drops it to basic. Renew attestations before they expire:
thirty days before exp the agent carries the attestation_expiring flag.
An accredited issuer is an organisation whose Agent Passports and
attestations the Grantex registry accepts. Accreditation is decided outside the
registry, by the registry operator, against the evidence the operator requires.
What the registry holds is the outcome: one record per issuer that says who the
issuer is, what it is accredited to attest, the keys it signs with and where
its status lists live. Relying parties and the registry’s own checks read that
record; nobody reads the evidence through it.
This page covers Phase 1 of the registry. In Phase 1 an issuer’s keys are a
static JWK Set recorded at accreditation. Resolving keys and trust marks
through OpenID Federation is Phase 2 and is not available yet. The protocol
text is in spec/registry-federation.md.
The issuer record
The registry adds
id, status, suspended_effective_from, accredited_at,
revoked_keys, created_at and updated_at.
Trust marks
A trust mark type says what an accredited issuer may attest. Phase 1 has five, and the registry refuses any other value:
An issuer is accredited for a mark only while its record lists it. Removing a
mark from the record ends the accreditation for that mark at once.
Keys
jwks is a JWK Set (RFC 7517 section 5) of public signing keys:
- EC keys on P-256, for ES256 (RFC 7518 sections 3.4 and 6.2), are supported.
- OKP keys on Ed25519, for EdDSA (RFC 8037), are supported as well.
- Every key has a
kid, and no two keys share one. - No private members: a key carrying
d(or any RSA private member, ork) is refused, so a leaked private key is never published. use, if present, issig;key_ops, if present, is["verify"];alg, if present, matches the curve. The registry fills inalg.- At most 16 keys and 16 KiB.
kid. The registry stops serving a revoked key
immediately, and the kid cannot be registered again for that issuer: rotate
to a new kid instead.
Status
When a check refuses an issuer it answers
issuer_not_accredited (unknown or
withdrawn), issuer_suspended (a suspension in effect) or trust_mark_missing
(the issuer is not accredited for the mark in question).
How the operator records an issuer
The registry operator’s routes take a key fromREGISTRY_OPERATOR_API_KEYS
(see self-hosting, section 5). An issuer does not call
them; the operator does, once accreditation is decided. Each change is
recorded on the registry’s audit chain with the reason given for it.
Accredit issuer.example:
accredit.json
201 with the whole record, including its id (aiss_...).
Later changes go to PATCH /v1/registry/issuers/{id} with a reason.
Suspend it from a given time (a time in the past takes effect at once; leave
effective_from out to suspend now):
suspend.json
revoke-kid.json
PATCH can also set status to active or withdrawn, replace
trust_marks with a new list, or replace jwks with a new set.
What relying parties see
GET /v1/registry/issuers needs no key, so it is served only when the
operator sets REGISTRY_PUBLIC_ENDPOINTS_ENABLED=true (exactly true; the
default is off). Off, the route is not registered and a request is answered
as for any unknown route: 401 without an API key, 404 with one. The
operator routes and the accreditation lookups work either way.
It lists the issuers with only entity_id, trust_marks, status (as it
stands at the time of the request), status_list_base and jwks without
revoked keys, ordered by entity_id. It is paged with page (from 1,
default 1) and pageSize (1 to 500, default 100), and reports total, the
number of issuers in all; a page past the end is empty and still carries
total, and any other value answers 400. Read pages until you have total
issuers, or until a page comes back empty:
ETag for each page:
send it back in If-None-Match and an unchanged page answers 304. It is
sent with Cache-Control: no-cache, so a cache checks back on every read and
never serves a revoked key.
Trying it locally
The repository has a mock accredited issuer,https://mock-issuer.example,
that issues Agent Passports, publishes its passport status lists and builds
attestations with no external party and no network. Accredit it in a local
registry with the entity id, status_list_base and JWKS its keys command
prints; see Running the Mock Issuer.