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 Size | Raw Size | Compressed | Credentials |
|---|---|---|---|
| 16,384 bits | 2 KB | ~200 bytes | 16,384 |
| 131,072 bits | 16 KB | ~1.5 KB | 131,072 |
| 1,048,576 bits | 128 KB | ~12 KB | 1,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:
- Create new list with same parameters
- Allocate new credentials to new list
- Keep old list(s) active for existing credentials
- 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
| Approach | Privacy | Efficiency | Complexity | Real-time |
|---|---|---|---|---|
| Status Lists | High | High | Medium | Yes |
| CRL | Low | Low | Low | Yes |
| OCSP | Medium | Medium | Low | Yes |
| Accumulators | High | Medium | High | Yes |
| Blockchain | Medium | Low | High | Eventual |
Next Steps
- Revoke a Credential - Step-by-step revocation guide
- Best Practices - Strategies for managing revocations
- API Reference - API documentation