Status lists and the catalog

Revoke IXO credentials with signed Token Status Lists hosted at the Credentials worker, and publish issuer and verifier entries in its public catalog.

This page covers the two public parts of the Credentials worker: the status-list registry, which serves issuer-signed revocation lists, and the catalog, which holds what each issuer says it issues and what each verifier says it accepts, with their names, logos and card looks. Issuers use both; verifiers read status lists on every check and publish a catalog entry; wallets read the catalog to show who sends an offer or a request.

Status lists

Each credential carries status.status_list = { uri, idx }. The uri names a list at the registry; idx is the credential's position. The list is a compressed array of bits signed by the issuer as a statuslist+jwt token (Token Status List, IETF draft 21, approved for publication as an RFC). Bit 0 means valid; any other value is treated as revoked.

CREDENTIALS_WORKER_URL/status/ISSUER_DID/LIST_ID

ISSUER_DID goes in the path exactly as written, not percent-encoded. LIST_ID is 1 to 64 letters, digits, _ or -.

For issuers

  1. Open a list with newStatusBits. Sign and publish it before you release the first credential that points to it.
  2. Allocate a random free index per credential with randomFreeIndex, and record it.
  3. Sign with createStatusListToken({ bits, uri, iss, signer, ttl, exp, iat }), using your assertionMethod key. The token's sub is exactly the uri in your credentials.
  4. Publish with PUT /status/ISSUER_DID/LIST_ID, Content-Type: application/statuslist+jwt, body the raw token. The UCAN invocation is for credentials/issue on ixo:credentials, signed with a key on your own DID document, bound by the fact { method: 'PUT', path: '/status/ISSUER_DID/LIST_ID' }.
  5. Revoke with setStatusBit, save, re-sign with a newer iat and publish. If publishing fails, retry from a job; your database holds the truth.
  6. Refresh every list from a scheduled job so no token expires. An expired token makes every credential on the list fail. Self (not live yet) signs tokens valid for 24 hours with ttl 300 seconds and republishes every list within 12 hours of expiry; the devnet test issuer signs tokens valid for 7 days with ttl 60 seconds, publishes at once on each revocation and refreshes every list at least daily.

The registry accepts a token only if all of these hold. It cannot change a bit itself.

CheckRule
CallerThe principal is ISSUER_DID, acting as itself (403 otherwise). A controller acting through ixo:credentials/ENTITY_DID cannot publish.
BodyAt most 64 KB, application/statuslist+jwt (415 otherwise)
Tokentyp statuslist+jwt; iss = ISSUER_DID; sub = this exact URL; signed (EdDSA or Ed25519) by the assertionMethod key its kid names
DatesWhole unix seconds; iat at most 60 seconds in the future; exp after iat, in the future and at most 400 days ahead
Orderiat strictly newer than the stored token's; equal or older is 409 stale
CountAt most 20 lists per issuer; a new list beyond that is 409 too_many_lists
StorageNear the worker's storage limit a new list is 507 storage_full; a newer version of an existing list is still accepted

A list whose exp passed more than 30 days ago is deleted.

For verifiers

GET the uri (public, rate limited per address, Access-Control-Allow-Origin: *, cacheable for at most the smaller of ttl and 60 seconds). Do not follow redirects. Verify the token with verifyStatusListToken(token, { uri, issuerDoc }) and read the bit with statusAt(bits, idx). Cache a verified token no longer than its ttl. An index outside the list is a hard failure (no_status). If the list cannot be fetched or does not verify, do not accept the presentation (VerificationUnavailable). The kit's verifyPresentation does all of this by default.

The uri is inside every credential for its whole life. Choose the host and list ids before you issue your first real credential; moving the registry later means issuing again.

The catalog

The catalog is a public directory. Each entry is a claim made by its own DID about itself. It is not a trust list: nobody curates it, the worker does not vouch for names or URLs, and listing does not make an issuer trustworthy or bind a verifier. Which issuers to accept is each verifier's own decision.

The catalog is also the only place wallets take a sender's name, logo and card look from. Offers and requests carry none (a request carries only its purpose line). So the look of a sender cannot change from one offer or request to the next. Because the entries are self-declared, a sender can copy another sender's name and look; see What protects a person.

Issuer entry

{
  "did": "ISSUER_DID",
  "name": "Self",
  "description": "Identity verification with a document and a liveness check",
  "url": "https://ISSUER_HOST",
  "logo_uri": "https://ISSUER_HOST/mark.png",
  "color": "#0b6b7e",
  "credential_issuer": "ISSUER_URL/identity",
  "types": [
    {
      "vct": "VCT",
      "name": "Identity Level 1",
      "description": "Your identity was checked against an ID document",
      "claims": [{ "name": "age_over_18", "label": "Over 18", "sd": "always" }],
      "display": {
        "background_color": "#0b6b7e",
        "text_color": "#ffffff",
        "logo_uri": "https://ISSUER_HOST/card-mark.png",
        "background_image_uri": "https://ISSUER_HOST/card.jpg"
      }
    }
  ]
}

logo_uri and color are the issuer's mark. types[].display is the card look of that type's credentials. Every look member is optional. A type may also carry type_metadata_uri. Each claim's sd is always, allowed or never. Credential types and schemas are not hosted on the Credentials worker: new types follow the IXO pattern of a type document in the GitHub repository ixofoundation/ns plus a protocol entity, as KYCAMLLevel1 does. Self's IdentityLevel1 keeps its current vct for now.

Verifier entry

{
  "did": "VERIFIER_DID",
  "name": "Yo Rewards",
  "description": "Rewards for young people",
  "url": "https://VERIFIER_HOST",
  "logo_uri": "https://VERIFIER_HOST/mark.png",
  "color": "#7a3e9d",
  "accepts": [
    {
      "purpose": "Claim a reward",
      "vct": "VCT",
      "claims": ["age_over_18"],
      "issuers": ["ISSUER_DID"]
    }
  ]
}

logo_uri and color are the verifier's mark. A verifier entry has no card look. Keep issuers the same as the issuer filter in your requests. A verifier without an entry is shown with a neutral mark.

The purpose in accepts describes your entry. What the person reads when you ask is the purpose of each request (see Request proof).

Name, logo and card look

RuleValue
Colorscolor, background_color, text_color: #rrggbb only. A wallet ignores text_color when its contrast with the background is below 4.5:1.
Imageslogo_uri, background_image_uri: https on the default port, no query string or fragment, a public host, at most 512 characters; png, jpeg or webp
FetchingThe worker stores the addresses and never fetches the images. The Auth Hub fetches them through its own media proxy, never from the person's browser.
Web addressesThe Auth Hub never shows url, credential_issuer or any other address from an entry to the person, not as a link and not as text to copy.
Invalid membersDropped, never a 400, and listed by path in the PUT answer's display_dropped (for example color or types.0.display.text_color)
Without a lookWallets show a neutral mark and card

Rules

RuleValue
SizeAt most 32 KB per entry
URLsurl, credential_issuer and type_metadata_uri must be https on a public host
TextNames, descriptions, labels and purposes are cleaned (control, bidi and zero-width characters removed, whitespace collapsed) and length-capped
WritePUT and DELETE /v1/catalog/issuers/DID with credentials/issue; /v1/catalog/verifiers/DID with credentials/verify. The body's did must equal the path. PUT replaces the whole entry: 201 when new, 200 when replaced, answering { sender_key, entry, updated_at, display_dropped }; DELETE answers 204. Near the worker's storage limit a new or larger entry is 507 storage_full.
ReadPublic: GET /v1/catalog/issuers?vct=VCT&limit=&cursor= answers { items, next_cursor } with each item { sender_key, entry, updated_at }; GET /v1/catalog/issuers/DID answers { sender_key, entry, updated_at } for exactly that DID; the same for verifiers. sender_key is the DID in its canonical form. Without a token, reads are limited per address and cacheable (max-age=60); with an optional credentials/hold token they are limited per principal instead.

Get listed in the catalog

Listing is self-service. There is no application, review or approval: any issuer or verifier with a did:ixo or did:web DID publishes its own entry, and wallets show it from then on.

  1. Choose the DID you send as. The entry belongs to exactly that DID, written exactly as in your offers or requests. Every DID is its own sender: an entry for did:web:example.com says nothing about did:web:a.example.com.
  2. Write the entry as JSON (issuer entry or verifier entry). Add your mark (logo_uri, color) and, for an issuer, each type's card look (types[].display). Host images as png, jpeg or webp on https; the Auth Hub's media proxy refuses SVG and files over 256 KB.
  3. Publish it with PUT /v1/catalog/issuers/YOUR_DID (ability credentials/issue) or PUT /v1/catalog/verifiers/YOUR_DID (ability credentials/verify), as your own DID, with a request-bound UCAN invocation. Do it on each network you work on: devnet, testnet and mainnet have separate catalogs.
  4. Check the answer. display_dropped names every member that was not stored, for example a color that is not #rrggbb. Then read the entry back with the public GET.
  5. Keep it current from a daily job, and update it whenever your types, claims or accepted issuers change. A verifier keeps its accepts the same as its requests.

The Auth Hub reads the catalog through a short cache (about 5 minutes) and changes a stored card look at most once a day, so a new name shows within minutes and a new look within a day.

Who may write an entry

The caller must act as the entry's DID:

  • The DID itself, signing with a key on its own DID document, resource ixo:credentials. Whoever controls an entity's DID document can add a key there and act as the entity, for the catalog and for everything else.
  • For an entity, also a controller of the entity, or a member of a controlling DAO group with voting weight above zero, resource ixo:credentials/ENTITY_DID. This path is for catalog writes only: it cannot deposit offers or requests or publish status lists. Any such member can rewrite the entity's entry, including credential_issuer and url, without a proposal.