Skip to content

Status List

Level: Intermediate lifecycle management

Simple explanation

Credentials need a way to be revoked or suspended after issuance. Status lists solve this without the verifier contacting the issuer for every check.

The issuer publishes a compressed bitstring where each credential has a position. Verifiers fetch the list and check locally. The issuer never learns which credential was checked.

What you will learn

  • Why status lists exist and how they preserve holder privacy
  • How the bitstring encoding maps credential indices to status values
  • How to create, update, and verify status lists with SdJwt.Net.StatusList
  • How to configure caching and fail-open vs fail-closed policies
Audience Developers implementing credential revocation or suspension, and operations teams managing status infrastructure.
Purpose Explain how status lists enable privacy-preserving credential lifecycle management (revocation, suspension) at scale, with working SdJwt.Net.StatusList code examples.
Scope Status list data model, token structure, bit encoding, issuer-side creation and updates, verifier-side checking with caching, and operational considerations (TTL, fail-open vs fail-closed). Out of scope: credential issuance (see VC), base SD-JWT format.
Success criteria Reader can create a status list for thousands of credentials, update individual status entries, verify credential status with proper caching, and configure fail-open/fail-closed policies.

Prerequisites

Before reading this document, you should understand:

Prerequisite Why Needed Resource
SD-JWT VC basics Status lists apply to VCs VC
JWT structure Status lists are signed JWTs SD-JWT

Glossary

Term Definition
Status List Compressed bitstring representing status of many credentials
Status List Token Signed JWT containing the status list data
Referenced Token A credential that points to a status list
Index Position of a credential's status within the status list
Bits Number of bits per status entry (1, 2, 4, or 8)
Revocation Permanent invalidation of a credential
Suspension Temporary invalidation (can be lifted)
TTL Time-to-live for caching the status list

Why status lists matter

A credential is valid when issued but may become invalid later:

  • Employee leaves company (revoke employment credential)
  • Driver's license suspended (suspend, don't revoke)
  • University discovers fraud (revoke degree)

Without status lists, a verifier must call the issuer for each credential check, revealing which credentials are being verified and adding load to issuer infrastructure.

With status lists, a single signed token represents the status of thousands of credentials. The verifier fetches it anonymously and caches it locally, so the issuer never learns which specific credential is being checked.

flowchart LR
    subgraph "Without Status List"
        V1[Verifier] -->|"Check credential #12345"| I1[Issuer]
        I1 -->|"Status: Revoked"| V1
        Note1[Privacy leak: Issuer knows which credential is checked]
    end

    subgraph "With Status List"
        V2[Verifier] -->|"GET /status/list1"| I2[Issuer]
        I2 -->|"Compressed bitstring for 100K credentials"| V2
        V2 -->|"Check bit #12345 locally"| V2
        Note2[Privacy preserved: Issuer doesn't know which credential]
    end

How it works: the data model

1. Credential points to status list

When issuing a credential, include a status claim:

{
  "iss": "https://university.example.edu",
  "vct": "https://credentials.example.edu/degree",
  "sub": "did:example:student123",
  "given_name": "Alice",
  "degree": "Bachelor of Science",
  "status": {
    "status_list": {
      "idx": 42,
      "uri": "https://university.example.edu/status/degrees-2024"
    }
  }
}
Field Purpose
status.status_list.idx This credential's position in the status list
status.status_list.uri Where to fetch the status list token

2. Status list token structure

The status endpoint returns a signed JWT (statuslist+jwt):

Header:

{
  "typ": "statuslist+jwt",
  "alg": "ES256",
  "kid": "status-key-2024"
}

Payload:

{
  "sub": "https://university.example.edu/status/degrees-2024",
  "iat": 1701234567,
  "exp": 1701238167,
  "ttl": 3600,
  "status_list": {
    "bits": 2,
    "lst": "eNrbuRgAAhcBXQ",
    "aggregation_uri": "https://university.example.edu/status/aggregation"
  }
}
Field Required Purpose
sub Yes Must match the uri in referenced credentials
iat Yes When the status list was created
exp No When the status list expires
ttl No How long verifiers may cache (seconds)
status_list.bits Yes Bits per entry: 1, 2, 4, or 8
status_list.lst Yes Base64url-encoded compressed bitstring
aggregation_uri No For discovering multiple status lists

3. Decoding the status value

The lst field is a compressed bitstring. To check credential at index 42:

1. Base64url decode -> compressed bytes
2. DEFLATE decompress -> raw bitstring
3. Extract bits at position (idx * bits_per_entry)
4. Interpret value according to status semantics

Status value semantics

The number of bits determines how many distinct statuses you can represent:

Bits Max Statuses Use Case
1 2 Valid (0) / Revoked (1)
2 4 Valid / Revoked / Suspended / Reserved
4 16 Application-specific needs
8 256 Rich status taxonomy

Status value semantics used by this implementation:

The spec defines a small set of well-known values. Deployments may assign additional application-specific meanings to higher values when bits > 1.

Value Hex Meaning
0 0x00 Valid
1 0x01 Invalid (Revoked)
2 0x02 Suspended
3 0x03 Application-specific

Revocation vs suspension: when to use which

Scenario Status Rationale
Employee terminated Revocation (1) Permanent; the credential should never be valid again
Driver's license renewal pending Suspension (2) Temporary; will become valid again after renewal
Fraud investigation in progress Suspension (2) May be cleared; revoke only if fraud is confirmed
University degree annulled Revocation (1) Permanent academic action
Payment dispute on a receipt credential Suspension (2) Credential may be restored after resolution

Fail-open vs fail-closed

When the status list endpoint is unreachable, your application must choose a default behavior:

Policy Behavior when status list is unavailable Best for
Fail-closed Reject the credential High-security contexts (banking, government, healthcare)
Fail-open Accept the credential (with stale cache or no check) Low-risk contexts (loyalty programs, event tickets)
Degrade with warning Accept but flag for review Medium-risk contexts (age verification, employment checks)

The library does not make this decision for you. Your application must configure the fallback behavior based on the business context.

A verifier should treat cached status lists as valid only within the configured TTL and token validity period. If both have elapsed, treat the status as unknown and apply the fail-open or fail-closed policy above.

Privacy: what the issuer does not learn

The status list is designed so the issuer never learns which specific credential a verifier is checking. The verifier fetches the entire list (covering thousands of credentials), checks the relevant bit locally, and never calls back with a credential identifier.

This means the issuer cannot:

  • Track which credentials are being verified
  • Correlate verification patterns to individual holders
  • Learn which verifiers are checking which credentials

For stronger privacy, publish status lists through a CDN or cacheable public endpoint. If verifiers fetch directly from issuer infrastructure, network logs may still reveal verifier activity even though the credential index is hidden inside the bulk download.

Complete verification flow

sequenceDiagram
    autonumber
    participant Issuer
    participant StatusEndpoint as Status Endpoint
    participant Wallet
    participant Verifier

    Note over Issuer: Issuance Phase
    Issuer->>Issuer: Assign index 42 to credential
    Issuer->>Wallet: Issue credential with status.idx=42

    Note over Verifier: Verification Phase
    Wallet->>Verifier: Present credential
    Verifier->>Verifier: Validate signature, structure, expiry

    alt Status claim present
        Verifier->>StatusEndpoint: GET status_list.uri
        StatusEndpoint-->>Verifier: statuslist+jwt
        Verifier->>Verifier: Validate status token signature
        Verifier->>Verifier: Check exp, iat freshness
        Verifier->>Verifier: Decompress lst
        Verifier->>Verifier: Read value at index 42

        alt Status = Valid (0)
            Verifier-->>Wallet: Accept credential
        else Status = Revoked (1)
            Verifier-->>Wallet: Reject: credential revoked
        else Status = Suspended (2)
            Verifier-->>Wallet: Reject: credential suspended
        end
    else No status claim
        Verifier-->>Wallet: Accept (no revocation checking)
    end

Code example: issuer creating status list

using SdJwt.Net.StatusList.Issuer;
using SdJwt.Net.StatusList.Models;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;

// Setup signing key (use separate key from credential signing)
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var statusSigningKey = new ECDsaSecurityKey(ecdsa) { KeyId = "status-key-2024" };

// Create status list manager
var manager = new StatusListManager(statusSigningKey, SecurityAlgorithms.EcdsaSha256);

// Initialize status values for 10,000 credentials
// All start as Valid (0)
var statusValues = new byte[10000];

// Revoke credential at index 42
statusValues[42] = (byte)StatusType.Invalid;

// Suspend credential at index 100
statusValues[100] = (byte)StatusType.Suspended;

// Create the status list token
string statusListToken = await manager.CreateStatusListTokenAsync(
    subject: "https://university.example.edu/status/degrees-2024",
    statusValues: statusValues,
    bits: 2,  // 2 bits = 4 possible states
    validUntil: DateTime.UtcNow.AddHours(24),
    timeToLive: 3600  // Cache for 1 hour
);

// Publish at the status endpoint
await PublishStatusListAsync(
    uri: "https://university.example.edu/status/degrees-2024",
    token: statusListToken
);

Code example: updating status (revocation)

// Revoke a credential
var updates = new Dictionary<int, StatusType>
{
    [42] = StatusType.Invalid,  // Revoke credential at index 42
    [99] = StatusType.Suspended  // Suspend credential at index 99
};

string updatedToken = await manager.UpdateStatusAsync(
    existingToken: currentStatusListToken,
    updates: updates
);

// Publish the updated status list
await PublishStatusListAsync(uri, updatedToken);

Code example: verifier checking status

using SdJwt.Net.StatusList.Verifier;
using SdJwt.Net.StatusList.Models;

// Create verifier (with caching)
var verifier = new StatusListVerifier(
    httpClient: httpClient,
    memoryCache: cache,
    logger: logger
);

// Extract status claim from credential
var statusClaim = new StatusClaim
{
    StatusList = new StatusListReference
    {
        Index = 42,
        Uri = "https://university.example.edu/status/degrees-2024"
    }
};

// Check status
var result = await verifier.CheckStatusAsync(
    statusClaim: statusClaim,
    issuerKeyProvider: async uri => await GetStatusSigningKey(uri),
    options: new StatusListOptions
    {
        EnableStatusChecking = true,
        CacheDuration = TimeSpan.FromMinutes(15),
        FailOnStatusCheckError = true  // Fail-closed behavior
    }
);

switch (result.Status)
{
    case StatusType.Valid:
        Console.WriteLine("Credential is valid");
        break;
    case StatusType.Invalid:
        Console.WriteLine($"Credential REVOKED at index {statusClaim.StatusList.Index}");
        break;
    case StatusType.Suspended:
        Console.WriteLine("Credential is SUSPENDED");
        break;
}

Operational considerations

Key separation

Use separate keys for credential signing and status list signing:

// Credential signing key (long-term, high security)
var credentialKey = LoadFromHsm("credential-signing-key");

// Status list signing key (can be rotated more frequently)
var statusKey = LoadFromHsm("status-signing-key");

Caching strategy

Scenario TTL Rationale
High-value credentials 5-15 minutes Near real-time revocation needed
Standard credentials 1-4 hours Balance freshness and load
Low-risk scenarios 24 hours Reduce issuer load

Fail-open vs fail-closed

Behavior When to Use Risk
Fail-closed High-security: financial, medical Service disruption if status unavailable
Fail-open Low-risk: newsletters, preferences May accept revoked credentials
// Fail-closed (reject if status check fails)
options.FailOnStatusCheckError = true;

// Fail-open (accept if status check fails)
options.FailOnStatusCheckError = false;

Implementation references

Component File Description
Status claim model StatusClaim.cs Credential status reference
Status list reference StatusListReference.cs idx + uri structure
Token payload StatusListTokenPayload.cs JWT payload model
Status list data StatusListData.cs Bits + lst structure
Status type enum StatusType.cs Valid/Invalid/Suspended
Issuer manager StatusListManager.cs Create/update status lists
Verifier StatusListVerifier.cs Check credential status
Package overview README.md Quick start
Sample code StatusListExample.cs Working examples

Beginner pitfalls to avoid

1. Not validating credential before status check

Always validate the credential (signature, structure, expiry) before checking status.

// WRONG order
var statusResult = await CheckStatusAsync(credential);
var signatureValid = await ValidateSignatureAsync(credential);

// RIGHT order
var signatureValid = await ValidateSignatureAsync(credential);
if (signatureValid)
{
    var statusResult = await CheckStatusAsync(credential);
}

2. Ignoring TTL and expiry

Honor ttl for cache duration and exp for validity. Do not cache status lists indefinitely.

// Check if status list has expired
if (statusListPayload.ExpiresAt.HasValue)
{
    var expiry = DateTimeOffset.FromUnixTimeSeconds(statusListPayload.ExpiresAt.Value);
    if (DateTimeOffset.UtcNow > expiry)
    {
        // Must fetch fresh status list
        await RefreshStatusListAsync(uri);
    }
}

3. Using same key for credentials and status lists

Use separate keys for credential issuance and status list signing, with potentially different rotation schedules.

4. Not handling status check failures

Define explicit fail-open or fail-closed behavior instead of crashing or hanging when the status endpoint is unavailable.

Frequently asked questions

Q: What happens if the status endpoint is down?

A: Depends on your configuration:

  • FailOnStatusCheckError = true: Verification fails (fail-closed)
  • FailOnStatusCheckError = false: Status check skipped (fail-open)

Choose based on your security requirements.

Q: How do I undo a revocation?

A: Use suspension instead of revocation if you might need to restore validity. Update the status value back to Valid (0).

Q: Can I have multiple status lists per issuer?

A: Yes. Each credential points to a specific uri. You might have separate status lists for:

  • Different credential types
  • Different time periods (degrees-2024, degrees-2025)
  • Different geographic regions

Q: How large can a status list be?

A: With DEFLATE compression, a status list for 1 million credentials with 2-bit entries is approximately 250KB. The compressed format is very efficient.

Q: Should I include status in every credential?

A: Include status if:

  • The credential can be revoked (employment, certifications)
  • The credential can be suspended (licenses)

Do not include status for immutable credentials where revocation is not meaningful.