ARIA1.0
English only · Spanish [PLANNED]

Check revocation, read a Status List

Task: find out whether a credential has been revoked, and understand why "how old is your answer" matters as much as the answer.

Where the pointer is

Every AID carries a credentialStatus entry: which list, which position.

from a real AID · credentialStatus
"credentialStatus": {
"type": "BitstringStatusListEntry",
"statusPurpose": "revocation",
"statusListCredential": "https://api.aria.bar/v1/status/1",
"statusListIndex": "18"
}

Fetch the list — one list per id, never per DID

The list is itself a signed credential (BitstringStatusListCredential), addressed by list id. There is one document holding a bit for every credential that points at it; you read the bit your own credential names in statusListIndex.

There is deliberately no lookup by DID. An endpoint that answered "is this DID revoked?" would be a different design — one that tells anyone who asks which identifiers exist, and learns which credentials each verifier is checking. The bitstring exists so that the registry learns nothing from your check. GET /v1/status/<anything that is not a list id> answers 404.

Verify the list before you read it

The list is only evidence if its proof verifies. An unsigned or tampered list is a revocation claim over transport security, not proof of anything, so check the composite signature first (§7) — both halves, exactly as for an AID — and only then read the bit.

node · runs against @aria-registry/verify@1.1.0 and https://api.aria.bar/v1/status/1
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
import { ed25519 } from '@noble/curves/ed25519.js';
import {
canonicalJson,
REGISTRY_PUBLIC_KEY_MLDSA65,
REGISTRY_PUBLIC_KEY_ED25519,
} from '@aria-registry/verify';

const list = await (await fetch('https://api.aria.bar/v1/status/1')).json();
const { proof, ...unsigned } = list;

// proofValue: optional multibase "u", then u32be(pq length) ‖ ML-DSA-65 ‖ Ed25519
const raw = Buffer.from(proof.proofValue.replace(/^u/, ''), 'base64url');
const pqLen = raw.readUInt32BE(0);
const payload = new TextEncoder().encode(canonicalJson(unsigned));
const ok =
ml_dsa65.verify(raw.subarray(4, 4 + pqLen), payload, REGISTRY_PUBLIC_KEY_MLDSA65) &&
ed25519.verify(raw.subarray(4 + pqLen), payload, REGISTRY_PUBLIC_KEY_ED25519);

if (!ok) throw new Error('status list proof does not verify: do not read it');
console.log('list proof verifies · signed', list.validFrom);

const bits = Buffer.from(list.credentialSubject.encodedList, 'base64url');
const bit = (i) => (bits[i >> 3] >> (7 - (i & 7))) & 1;
console.log('index 18 revoked:', bit(18) === 1);

You should see list proof verifies · signed <date> and then index 18 revoked: false for an active credential, true for a revoked one. The validFrom is the moment the current version of the list was signed; it moves every time a bit changes, so it is the age your answer has.

The deployed list is served uncompressed. The W3C Bitstring Status List specification describes a GZIP-compressed encodedList; ARIA's is not compressed, and the snippet reads the form that is served.

Why the age matters

The registry sets the bit within 60 seconds of a revocation (COM-05, §7). Your answer is as old as your last fetch. ATP's fresh= parameter is the receiver-side bound: default one hour, floor 60 seconds (§8). A verifier that caches the list for a day will admit a credential revoked this morning. The SDK enforces the bound through maxOfflineAge; never set it to null (H1).

What revocation is not

Revocation is terminal for the credential, not for the identifier: the DID never returns to the pool, and it may be re-issued only to the same controller (§3.5.1, §3.5.4). To see the history of one identifier, walk the issuance chain.