Revocation Best Practices
This guide provides strategies, patterns, and recommendations for managing credential revocation effectively at scale.
Strategic Planning
Define Your Revocation Policy
Before issuing credentials, establish clear policies:
When to Revoke:
- Employee termination or role change
- Security breach or compromise
- Credential error or invalid data
- Qualification expiration
- Policy violation
- Holder request
Who Can Revoke:
- Define roles and permissions
- Implement approval workflows for sensitive credentials
- Audit all revocation actions
- Separate test and production environments
Response Times:
- Critical security: Immediate (< 5 minutes)
- Standard termination: Same business day
- Administrative correction: Within 24 hours
- Batch cleanup: Weekly or monthly
Document Your Processes
Create internal documentation covering:
- Step-by-step revocation procedures
- Escalation paths for urgent revocations
- Communication templates for holders
- Audit trail requirements
- Compliance reporting
Technical Best Practices
Design for Revocation from the Start
During Credential Design:
// Always include status metadata when issuing
const credential = await issueCredential({
template: "EmployeeCredential",
attributes: {...},
// Enable revocation
includeStatus: true,
statusPurpose: "revocation"
});
Use Separate Lists by Type:
// Separate status lists for different credential types
const employeeListId = await allocateStatusList({
issuerDid: orgDid,
vct: "EmployeeCredential",
purpose: "revocation"
});
const certificationListId = await allocateStatusList({
issuerDid: orgDid,
vct: "CertificationCredential",
purpose: "revocation"
});
Monitor List Capacity
Set up alerts when lists reach capacity:
// Check list utilization
const list = await StatusListModel.findById(listId);
const used = countUsedIndices(list.bits);
const utilizationPercent = (used / list.length) * 100;
if (utilizationPercent > 80) {
await notifyOps({
alert: "Status list nearing capacity",
listId: listId,
utilization: utilizationPercent
});
}
Implement Rate Limiting
Prevent abuse with rate limits:
// Limit revocations per issuer per hour
const revocationsLastHour = await getRevocationCount({
issuerDid: orgDid,
since: Date.now() - 3600000
});
if (revocationsLastHour > 100) {
throw new Error("Revocation rate limit exceeded");
}
Use Idempotent Operations
Handle duplicate revocation requests gracefully:
// Check if already revoked before updating
const binding = await StatusBindingModel.findOne({
credentialId: sessionId
});
if (binding.revoked) {
return {
success: true,
message: "Credential already revoked",
revokedAt: binding.updatedAt
};
}
// Proceed with revocation
await revoke(binding.listId, binding.statusListIndex);
Operational Best Practices
Establish Monitoring
Track key metrics continuously:
Revocation Metrics:
- Total revocations per day/week/month
- Revocation by reason/category
- Time from issue to revocation
- Failed revocation attempts
- List utilization trends
Performance Metrics:
- Revocation API latency
- Status list fetch time
- Cache hit ratios
- Error rates
Security Metrics:
- Unusual revocation patterns
- Unauthorized access attempts
- Cross-project revocation attempts
Implement Alerting
Set up alerts for:
alerts:
- name: High Revocation Rate
condition: revocations_per_hour > 50
severity: warning
- name: Status List Full
condition: list_utilization > 95%
severity: critical
- name: Failed Revocations
condition: failed_revocations > 5 in 10m
severity: warning
- name: Unauthorized Access
condition: 403_errors > 10 in 1m
severity: critical
Regular Audits
Schedule periodic reviews:
Weekly:
- Review all revocations from past week
- Verify proper authorization
- Check for anomalies or patterns
- Validate audit logs
Monthly:
- Analyze revocation trends
- Review list utilization
- Assess policy effectiveness
- Update procedures as needed
Quarterly:
- Comprehensive security review
- Compliance audit
- Performance optimization
- Training updates
Credential Lifecycle Management
Pre-Revocation Checks
Before revoking, verify:
- Correct Credential: Confirm credential ID and recipient
- Authorization: Verify requester has permission
- Impact Assessment: Understand downstream effects
- Communication Plan: How will holder be notified?
Revocation Execution
Follow a consistent process:
async function revokeCredentialWorkflow(
credentialId: string,
reason: string,
requestedBy: string
) {
// 1. Pre-revocation validation
const credential = await validateCredential(credentialId);
await checkAuthorization(requestedBy, credential.projectId);
// 2. Log the revocation intent
await auditLog.create({
action: "revocation_initiated",
credentialId,
reason,
requestedBy,
timestamp: new Date()
});
// 3. Perform revocation
const result = await revokeCredential(credentialId);
// 4. Update internal systems
await updateCRM({
credentialId,
status: "revoked",
revokedAt: new Date(),
reason
});
// 5. Notify stakeholders
await notifyStakeholders({
credentialId,
recipient: credential.recipientEmail,
reason
});
// 6. Log completion
await auditLog.create({
action: "revocation_completed",
credentialId,
result,
timestamp: new Date()
});
return result;
}
Post-Revocation Actions
After revocation:
- Verify Success: Check status bit updated correctly
- Update Records: Mark in internal systems
- Notify Holder: Send revocation notice if appropriate
- Document: Record reason and context
- Monitor: Watch for verification attempts
Communication Strategies
Holder Notification
When notifying holders of revocation:
Email Template:
Subject: Important: Your [Credential Type] has been revoked
Dear [Name],
We're writing to inform you that your [Credential Type]
issued on [Issue Date] has been revoked as of [Revocation Date].
Reason: [Brief explanation]
What this means:
- The credential can no longer be used for verification
- It will fail any verification checks
- You should remove it from your wallet
Next Steps:
[If applicable: Instructions for re-issuance or appeal]
If you have questions, please contact [Support Contact].
Best regards,
[Organization Name]
In-App Notification:
- Push notification to wallet (if supported)
- Email with clear subject line
- SMS for critical revocations
- Dashboard notification in your system
Internal Communication
Keep teams informed:
For HR/Admin:
Employee Credential Revoked
- Employee: Alice Smith
- Credential: Employee Badge #12345
- Revoked: 2024-01-15 10:30 AM
- Reason: Employment terminated
- Revoked by: admin@company.com
- Verification Status: Failed (as expected)
For Security Team:
Security Alert: Credential Revoked
- Type: Emergency revocation
- Reason: Suspected compromise
- Scope: 1 credential revoked
- Impact: Immediate verification failure
- Actions: Monitor for usage attempts
Compliance and Legal
Audit Trail Requirements
Maintain comprehensive logs:
interface RevocationAuditLog {
timestamp: Date;
action: "revocation_initiated" | "revocation_completed";
credentialId: string;
statusListId: string;
statusListIndex: number;
reason: string;
requestedBy: string;
approvedBy?: string;
ipAddress: string;
userAgent: string;
success: boolean;
errorMessage?: string;
}
Data Retention
Define retention policies:
- Active Credentials: Keep full audit trail
- Revoked Credentials: Retain per compliance (e.g., 7 years)
- Status Lists: Archive when all credentials expired/revoked
- Audit Logs: Permanent retention for compliance
Right to Erasure
Handle data deletion requests:
async function handleErasureRequest(credentialId: string) {
// 1. Revoke credential if not already revoked
const binding = await StatusBindingModel.findOne({
credentialId
});
if (!binding.revoked) {
await revoke(binding.listId, binding.statusListIndex);
}
// 2. Anonymize personal data
await CredentialModel.updateOne(
{ issuanceSessionId: credentialId },
{
recipientEmail: "[redacted]",
attributes: { _redacted: true }
}
);
// 3. Keep revocation record for compliance
// (Status binding remains for audit)
// 4. Log erasure action
await auditLog.create({
action: "data_erasure",
credentialId,
timestamp: new Date()
});
}
Scaling Strategies
Batch Revocations
For large-scale revocations:
async function batchRevoke(credentialIds: string[]) {
const BATCH_SIZE = 100;
for (let i = 0; i < credentialIds.length; i += BATCH_SIZE) {
const batch = credentialIds.slice(i, i + BATCH_SIZE);
// Process in parallel within batch
await Promise.all(
batch.map(id => revokeCredential(id))
);
// Brief delay between batches
await sleep(1000);
}
}
Scheduled Revocations
For planned revocations:
// Schedule future revocation
await scheduleRevocation({
credentialId: "session_123",
revokeAt: new Date("2024-12-31T23:59:59Z"),
reason: "Employment contract end date"
});
// Cron job to process scheduled revocations
cron.schedule("* * * * *", async () => {
const due = await ScheduledRevocation.find({
revokeAt: { $lte: new Date() },
processed: false
});
for (const scheduled of due) {
await revokeCredential(scheduled.credentialId);
scheduled.processed = true;
await scheduled.save();
}
});
Automatic Expiration Cleanup
Revoke expired credentials:
// Find and revoke expired credentials
async function cleanupExpired() {
const expired = await CredentialModel.find({
expiryDate: { $lt: new Date() },
state: { $ne: "Revoked" }
});
for (const credential of expired) {
await revokeCredential(credential.issuanceSessionId);
await auditLog.create({
action: "automatic_expiry_revocation",
credentialId: credential.issuanceSessionId,
timestamp: new Date()
});
}
}
// Run daily
cron.schedule("0 2 * * *", cleanupExpired);
Error Handling
Graceful Degradation
Handle failures gracefully:
async function safeRevoke(credentialId: string) {
try {
await revokeCredential(credentialId);
return { success: true };
} catch (error) {
// Log error
logger.error("Revocation failed", {
credentialId,
error: error.message
});
// Notify ops team
await notifyOps({
alert: "Revocation failure",
credentialId,
error: error.message
});
// Return partial success if possible
return {
success: false,
error: error.message,
retryable: isRetryable(error)
};
}
}
Retry Logic
Implement exponential backoff:
async function revokeWithRetry(
credentialId: string,
maxRetries = 3
) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await revokeCredential(credentialId);
} catch (error) {
if (attempt === maxRetries - 1) throw error;
const delay = Math.pow(2, attempt) * 1000;
await sleep(delay);
}
}
}
Testing and Validation
Test Revocation Flow
Include in your test suite:
describe("Credential Revocation", () => {
it("should revoke credential successfully", async () => {
// Issue credential
const credential = await issueCredential({...});
// Verify it's active
const status1 = await checkStatus(credential.id);
expect(status1.revoked).toBe(false);
// Revoke credential
await revokeCredential(credential.id);
// Verify it's revoked
const status2 = await checkStatus(credential.id);
expect(status2.revoked).toBe(true);
});
it("should fail verification after revocation", async () => {
const credential = await issueCredential({...});
await revokeCredential(credential.id);
const verification = await verifyCredential(credential);
expect(verification.valid).toBe(false);
expect(verification.reason).toContain("revoked");
});
it("should handle idempotent revocation", async () => {
const credential = await issueCredential({...});
// Revoke twice
await revokeCredential(credential.id);
await revokeCredential(credential.id);
// Should not throw error
const status = await checkStatus(credential.id);
expect(status.revoked).toBe(true);
});
});
Next Steps
- Revoke a Credential - Step-by-step guide
- Status Lists Technical Deep Dive - Implementation details
- API Reference - API documentation