Skip to content

Attestations

Statements one domain signs about an identity, usually one issued by another domain. An attestation is created as an offer; its subject accepts it by proving possession of an identity key, and receives a credential bound to that key. The credential is an SD-JWT VC: each claim is disclosed or withheld by its holder, and a presentation is bound to one verifier and one occasion. The issuer's key is certified by its root key, so the DNS pin anchors every credential, and a status list tells whether a credential still stands without the issuer learning who asked. Which attesters to believe is left to whoever verifies.

GET /.well-known/jwt-vc-issuer

Credential issuer metadata

Unauthenticated

Callable with no credential.

The key this server signs credentials and status lists with, in the form SD-JWT VC defines for issuer metadata. issuer is https://<domain>, the value every credential issued here carries as iss.

The key is ECDSA P-256 (ES256). It is not the root key: it is a separate key the root key certifies. revoked_certification carries that certification in the format of every root-key statement — payload (base64url of a JSON body with type: credential-key-v1, the domain, and the key's RFC 7638 thumbprint as kid) and signature (hex). A verifier that walked the DNS pin to the root key checks the certification and so ties every credential to the domain. A key served here without a certification under the pinned root key proves nothing.

Responses

Status Meaning
200 The issuer identifier and its key set.

Response fields

Field Type Notes
issuer string https://<domain>.
jwks object

GET /api/attestations/status

Status list of the credentials issued here

Unauthenticated

Callable with no credential.

The revocation state of every credential this server issued, as one Token Status List: a JWT of type statuslist+jwt, signed with the credential key, whose status_list.lst is a zlib-compressed bit array, one bit per credential. A set bit means revoked. Each credential names its position in its status claim.

A verifier reads the whole list and looks its credential up in it, so this server does not learn which credential was checked or by whom. The list is at least twice as long as the number of credentials issued, and positions are assigned at random.

A revocation is never undone, and it outlives the attestation: a deleted attestation, or one whose workspace was deleted, stays revoked. A position the list does not reach says nothing about its credential, which is not the same as revoked.

Responses

Status Meaning
200 The status list token, as application/statuslist+jwt. It may be kept for ttl seconds (five minutes), which is the revocation latency in the usual case, and is not valid past exp.
429 Too many attempts. Password gates, probes and challenges are limited per client, so a slug cannot be brute-forced.

GET /api/attestations/offers/{token}

What an offer states

Unauthenticated

Callable with no credential.

Shows an offer to whoever holds its token, before anything is decided: the attesting server, the workspace, the type, and every claim with its value, so the subject sees what would be stated about them. Once the offer was accepted the claims are left out.

server names the domain and root key fingerprint to walk the DNS chain against.

Parameters

Name In Required Notes
token path yes The offer's one-time token, returned once in the X-Attestation-Token header when the attestation was created.

Responses

Status Meaning
200 The offer.
404 No such resource, or the caller may not see it.
410 attestation_offer_expired or attestation_offer_revoked.
429 Too many attempts. Password gates, probes and challenges are limited per client, so a slug cannot be brute-forced.

Response fields

Field Type Notes
type string
label string
status string (offered | active)
claims object Present while the offer is open.
expiresAt string When the credential stops being valid; empty for none.
offerExpiresAt string Until when the offer can be accepted.
subjectFingerprint string Set when the offer was made for one identity, or accepted by one.
server ServerClaim
workspace object

POST /api/attestations/offers/{token}/claim

Accept an offer and receive the credential

Unauthenticated

Callable with no credential.

Issues the credential to an identity that proves its key. The caller is usually the subject's own server, relaying for the app (POST /api/credentials/claim there); nothing but the token and the proof is needed.

The challenge comes from GET /api/challenges/attestation/{token} with the identity's id, and is signed on the device with the identity key. This server then checks the identity with the server that issued it: the certificate is the one fingerprinted, that server's DNS-pinned root key signed the fingerprint, and it still vouches for the identity. An identity issued here is checked against this server's own records.

The credential is an SD-JWT VC signed with the credential key. It names the issuer (iss), the type (vct), the holder's key (cnf.jwk), the identity it is about (revoked_holder: fingerprint and domain) and its position in the status list (status). Every claim of the attestation is selectively disclosable.

An offer is accepted once. The identity that accepted it may claim it again until the offer's date and receives the same statement again, so a lost answer costs nothing; any other identity is refused with attestation_offer_claimed.

Parameters

Name In Required Notes
token path yes

Request body

application/json

Field Type Notes
identity object The public half of the subject's identity.
challengeNonce string
challengeSignature string

Responses

Status Meaning
200 The credential, with every disclosure.
400 Refused, with a named reason.
401 challenge_required, challenge_invalid or signature_invalid.
403 remote_identity_invalid when the identity could not be verified with the server that issued it, identity_revoked when that server withdrew it, attestation_identity_mismatch when the offer was made for another identity.
404 No such resource, or the caller may not see it.
410 attestation_offer_claimed, attestation_offer_expired or attestation_offer_revoked.
429 Too many attempts. Password gates, probes and challenges are limited per client, so a slug cannot be brute-forced.

Response fields

Field Type Notes
credential string <issuer JWT>~<disclosure>~…~
issuer string The attesting domain.

POST /api/attestations/verify

Verify a presented credential

Unauthenticated

Callable with no credential.

Checks a presentation along both chains and answers with a verdict.

The attester's chain: the issuer's domain is read from iss, its root key is tied to the domain through the _revoked TXT record, the credential key is checked against the root key's certification, the credential's signature against that key, and its position in the issuer's status list.

The holder's chain: the key binding is checked against the key the credential names, for the audience and nonce given here, and the server that issued the holder's identity is asked whether that identity still stands.

The verdict states who attested and what was disclosed. It does not decide whether the attester deserves to be believed: which domains are accepted for which statement is the caller's policy.

Without audience and nonce no key binding is checked, and the verdict is never verified: a credential without a key binding could be presented by anyone holding a copy. A service chooses a fresh nonce for every request and passes its own origin as audience.

Always answers 200; a presentation that cannot be read is the verdict invalid, and a check that could not be completed is unverified, never an error to ignore.

Request body

application/json

Field Type Notes
presentation string <issuer JWT>~<disclosure>~…~<key binding JWT>
audience string The verifier as the presentation names it
nonce string The value the verifier chose for this occasion.

Responses

Status Meaning
200 The verdict.
400 Refused, with a named reason.
429 Too many attempts. Password gates, probes and challenges are limited per client, so a slug cannot be brute-forced.

Response fields

Field Type Notes
state string (verified | unverified | dnsMissing | spoofed | revoked | expired | invalid)
reason string The first reason the credential was held back
issuer string The attesting domain.
issuerState string How the issuer's key was tied to its domain.
type string The credential's vct.
claims object The disclosed claims. Left out unless the issuer's signature verified.
withheld integer How many claims the credential holds that were not disclosed.
status string (valid | revoked | unknown) What the issuer's status list says.
issuedAt integer Unix seconds.
expiresAt integer Unix seconds; absent when the credential does not expire.
holder object The identity the credential was issued to.
keyBound boolean Whether the presentation proved possession of the holder's key for the audience and nonce given.

POST /api/credentials/claim

Accept an offer and keep the credential

Auth: Session

Accepts an attestation offer for the person signed in here and keeps the credential for them. This is what the app calls; the offer may live on this server or on any other.

The app obtains the challenge from the offer's server and signs it on the device. This server adds the identity's public half, calls the offer's server, and checks what comes back before keeping it: the server that was asked signed it, with a key tied to its domain, and it is bound to the key and the identity that asked. A credential that fails this is not kept, and credential_invalid is returned; the offer can be accepted again.

origin is the address of the offer's server and is left out for an offer on this server. Accepting an offer again replaces the kept copy.

Request body

application/json

Field Type Notes
origin string The other server's address, e.g. https://verify.example. Plain http is accepted only for a domain listed in PEER_OVERRIDES.
token string
identityId string An identity of the signed-in person on this server.
challengeNonce string
challengeSignature string

Responses

Status Meaning
200 The kept credential, as its credentials record.
400 Refused, with a named reason.
401 Refused, with a named reason.
403 Refused, with a named reason.
404 No such resource, or the caller may not see it.
410 Refused, with a named reason.
429 Too many attempts. Password gates, probes and challenges are limited per client, so a slug cannot be brute-forced.
502 remote_unreachable when the other server could not be reached, credential_invalid when what it returned did not verify.

Response fields

Field Type Notes
id string
issuer string The attesting domain.
type string
identity string
credential string The SD-JWT with every disclosure.
claims object
issuedAt string
expiresAt string

GET /api/credentials/{id}/status

Whether a held credential still stands

Auth: Session

The holder's view of a credential they keep: the same checks as POST /api/attestations/verify, without a key binding, since nothing is being presented. It tells whether the attester's key still verifies, whether the credential was revoked or has expired, and whether the identity it is bound to still stands.

Parameters

Name In Required Notes
id path yes

Responses

Status Meaning
200 The verdict. keyBound is always false here.
401 Refused, with a named reason.
404 No such resource, or the caller may not see it.
429 Too many attempts. Password gates, probes and challenges are limited per client, so a slug cannot be brute-forced.

Response fields

Field Type Notes
state string (verified | unverified | dnsMissing | spoofed | revoked | expired | invalid)
reason string The first reason the credential was held back
issuer string The attesting domain.
issuerState string How the issuer's key was tied to its domain.
type string The credential's vct.
claims object The disclosed claims. Left out unless the issuer's signature verified.
withheld integer How many claims the credential holds that were not disclosed.
status string (valid | revoked | unknown) What the issuer's status list says.
issuedAt integer Unix seconds.
expiresAt integer Unix seconds; absent when the credential does not expire.
holder object The identity the credential was issued to.
keyBound boolean Whether the presentation proved possession of the holder's key for the audience and nonce given.