Skip to main content

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 in spec/attestation-1.0.md. There is no API key: the signature is the authentication.
The protected header is {"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
Before posting, check:
  • You are accredited for the type. type is one of your trust marks, and your record is neither suspended nor withdrawn.
  • The agent has proven its key. For agent.identity and agent.security, key_thumbprint must be a key the agent registered and proved possession of (POST /v1/agents/{id}/keys/{thumbprint}/challenge and /prove). An attestation for a key that is not yet proven is refused with key_unproven.
  • Your status list is reachable. status.status_list.uri must be under your status_list_base, and the registry fetches it while it checks the attestation: GET, https, no redirects, served as application/statuslist+jwt, signed with a key in your record, fresh by exp (or iat + ttl), with the entry VALID. Otherwise the answer is status_stale (or passport_revoked for 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 its ttl, 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. A ttl of a few minutes to an hour is a good choice.
  • id is 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 an id you have used are 409.
  • 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).
The answer is 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 in Authorization:
The request’s header has "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
A request is valid for five minutes and only once. A refresh uses "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 is attested 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, or k) is refused, so a leaked private key is never published.
  • use, if present, is sig; key_ops, if present, is ["verify"]; alg, if present, matches the curve. The registry fills in alg.
  • At most 16 keys and 16 KiB.
Revoking a key is done by 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 from REGISTRY_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
The answer is 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 one key:
revoke-kid.json
A 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:
It is rate limited per client address and carries an 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.
Last modified on September 28, 2026