Skip to main content

Understanding Status Lists

This page provides a deep technical dive into how status lists work in Skippy's credential revocation system.

Overview

Status lists are a privacy-preserving, efficient mechanism for managing credential revocation status. They use a bitstring approach where each credential is assigned a unique index, and the bit at that index indicates whether the credential is active (0) or revoked (1).

Technical Architecture

The Bitstring Model

A status list is essentially a binary array:

Bit Position:  0    1    2    3    4    5    ...  16383
Bit Value: 0 1 0 0 1 0 ... 0
Status: ✓ ✗ ✓ ✓ ✗ ✓ ... ✓
  • Bit = 0: Credential is active
  • Bit = 1: Credential is revoked

Data Structures

StatusList Model

interface StatusList {
_id: string; // e.g., "sl_abc123"
issuerDid: string; // DID of the issuing organization
purpose: 'revocation' | 'suspension';
url: string; // Public URL where the JWT is hosted
length: number; // Number of bits (default: 16,384)
bits: Buffer; // Raw bitstring stored as binary
encodedCache?: string; // zlib-compressed cache for performance
version: number; // Version for tracking updates
createdAt: Date;
updatedAt: Date;
}

StatusBinding Model

Links credentials to their position in the status list:

interface StatusBinding {
credentialId: string; // Issuance session ID
listId: string; // Which status list
statusListIndex: number; // Position in the bitstring (0-16383)
statusListUri?: string; // Full public URL
purpose: 'revocation' | 'suspension';
revoked: boolean; // Current status cache
createdAt: Date;
updatedAt: Date; // Timestamp of last revocation
}

Why 16,384 Bits?

The default size of 16,384 bits (2KB when stored as raw bytes) provides:

  • Efficiency: Small enough for fast network transfer
  • Capacity: Large enough for most organizations
  • Compression: Compresses well with zlib for even smaller size
  • Scalability: Multiple lists can be created as needed

How It Works

1. Allocation Process

When a credential is issued with revocation support:

// 1. Find or create a status list for this issuer
const list = await StatusListModel.findOne({
issuerDid: "did:key:z6Mk...",
purpose: "revocation"
});

// 2. Find the next free index (where bit = 0)
const idx = findNextFreeIndex(list.bits);

// 3. Create binding between credential and status list
await StatusBindingModel.create({
credentialId: "session_abc123",
listId: list._id,
statusListIndex: idx,
purpose: "revocation",
revoked: false
});

// 4. Return status metadata to include in credential
return {
listId: list._id,
idx: idx,
url: `https://agent.skippy.id/statuslist/${list._id}`
};

2. Credential Embedding

The status metadata is embedded in the issued credential:

{
"vct": "EmployeeCredential",
"iss": "did:key:z6Mk...",
"iat": 1234567890,
"exp": 1234567890,
"credentialSubject": {
"name": "Alice Smith",
"employeeId": "EMP001"
},
"credentialStatus": {
"type": "status_list",
"status_list_url": "https://agent.skippy.id/statuslist/sl_abc123",
"status_list_index": "42",
"status_purpose": "revocation"
}
}

3. Revocation Process

When revoking a credential:

// 1. Find the status list
const list = await StatusListModel.findById(listId);

// 2. Get the bitstring buffer
const bits = Buffer.from(list.bits);

// 3. Set the bit at index to 1
bits[idx] = 1;

// 4. Save the updated list
list.bits = bits;
list.encodedCache = undefined; // Invalidate cache
await list.save();

// 5. Update the binding record
await StatusBindingModel.updateOne(
{ listId, statusListIndex: idx },
{
revoked: true,
updatedAt: new Date()
}
);

4. Status List JWT Generation

The status list is served as a signed JWT:

// 1. Fetch raw bitstring
const { bits, issuerDid, url, purpose } = await getEncodedList(listId);

// 2. Compress using zlib
const compressed = zlib.gzipSync(bits);
const encoded = base64UrlEncode(compressed);

// 3. Create JWT payload
const payload = {
iss: issuerDid,
sub: url,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300, // 5 min TTL
status_list: {
encoded_list: encoded,
status_purpose: purpose
}
};

// 4. Sign with issuer's DID key
const jwt = await signJWT(payload, issuerDid);

// 5. Return with caching headers
return {
jwt,
headers: {
'Content-Type': 'application/statuslist+jwt',
'Cache-Control': 'public, max-age=300'
}
};

5. Verification Process

When a verifier checks credential status:

// 1. Extract status info from credential
const { status_list_url, status_list_index } = credential.credentialStatus;

// 2. Fetch the status list JWT
const response = await fetch(status_list_url);
const jwt = await response.text();

// 3. Verify JWT signature
const payload = await verifyJWT(jwt, issuerDid);

// 4. Decompress the bitstring
const compressed = base64UrlDecode(payload.status_list.encoded_list);
const bits = zlib.gunzipSync(compressed);

// 5. Check the bit at the credential's index
const isRevoked = bits[status_list_index] === 1;

// 6. Return verification result
if (isRevoked) {
return {
valid: false,
reason: "Credential has been revoked"
};
}

Privacy Features

Holder Privacy

Status lists provide herd privacy:

  • Verifiers cannot determine which specific credential is being checked
  • All credentials on the same list look identical from the outside
  • Bitstring compression hides patterns
  • No correlation between checks

Verifier Privacy

  • Verifiers don't need to register or identify themselves
  • Status checks are passive (just HTTP GET requests)
  • No callback to issuer required
  • Can cache results for performance

Issuer Privacy

  • Status lists don't reveal how many credentials are issued
  • Unused indices appear as active
  • Revocation patterns are not easily discernible
  • Multiple lists can segregate different credential types

Performance Characteristics

Space Efficiency

Status List SizeRaw SizeCompressedCredentials
16,384 bits2 KB~200 bytes16,384
131,072 bits16 KB~1.5 KB131,072
1,048,576 bits128 KB~12 KB1,048,576

Network Efficiency

  • CDN Caching: Status lists are cached at edge locations
  • Compression: gzip reduces transfer size by ~90%
  • TTL: 5-minute cache reduces load
  • Conditional Requests: ETags enable "not modified" responses

Database Operations

  • Revocation: Single document update (~10ms)
  • Allocation: Index scan, worst case O(n) (~50ms for full list)
  • Binding lookup: Indexed query (~2ms)
  • Status check: Memory operation after fetch (~1ms)

Scaling Strategies

Multiple Lists per Issuer

Create separate lists for different purposes:

// Separate list for employees
const employeeList = await allocate({
issuerDid: "did:key:z6Mk...",
vct: "EmployeeCredential",
purpose: "revocation"
});

// Separate list for certifications
const certList = await allocate({
issuerDid: "did:key:z6Mk...",
vct: "CertificationCredential",
purpose: "revocation"
});

List Rotation

When a list fills up:

  1. Create new list with same parameters
  2. Allocate new credentials to new list
  3. Keep old list(s) active for existing credentials
  4. Archive fully-revoked lists after retention period

Sharding by Time

Create time-based lists for better management:

const listId = `sl_${issuerDid}_${year}_${month}`;

Benefits:

  • Easier archival and cleanup
  • Predictable list sizes
  • Temporal correlation analysis harder

Security Considerations

JWT Signature Verification

Always verify the JWT signature matches the issuer:

const issuerDid = credential.iss;
const statusJWT = await fetch(credential.credentialStatus.status_list_url);
const verified = await verifyJWT(statusJWT, issuerDid);

if (!verified) {
throw new Error("Status list signature invalid");
}

Cache Poisoning Prevention

  • HTTPS only: All status lists served over HTTPS
  • Signed JWTs: Cryptographic integrity protection
  • Short TTLs: Limit impact of stale data
  • CDN configuration: Proper cache headers

Replay Attack Prevention

  • Expiry timestamps: JWTs expire after 5 minutes
  • Not-before timestamps: Prevents time-travel attacks
  • Sequence numbers: (optional) detect rollback

Monitoring and Operations

Metrics to Track

  • List utilization: Percentage of indices used
  • Revocation rate: Credentials revoked per time period
  • Cache hit ratio: CDN cache effectiveness
  • Fetch latency: Time to retrieve status lists
  • Verification failures: Due to revoked credentials

Operational Tasks

Daily:

  • Monitor list capacity (alert at 80% full)
  • Check revocation anomalies
  • Verify CDN health

Weekly:

  • Review list distribution across issuers
  • Analyze revocation patterns
  • Optimize cache settings

Monthly:

  • Audit unused lists
  • Archive fully-revoked lists
  • Review scaling needs

Standards Compliance

Skippy's status list implementation follows:

  • SD-JWT Status List: Draft specification for bitstring-based status
  • JWT Best Practices: RFC 8725 security considerations
  • W3C Verifiable Credentials: Status list vocabulary
  • OpenID4VC: Status checking in OpenID for Verifiable Credentials

Comparison with Alternatives

ApproachPrivacyEfficiencyComplexityReal-time
Status ListsHighHighMediumYes
CRLLowLowLowYes
OCSPMediumMediumLowYes
AccumulatorsHighMediumHighYes
BlockchainMediumLowHighEventual

Next Steps