Implementation Plan: Credential Lifecycle Controls¶
| Status | Accepted (partial - split into work packages) |
| Priority | P1 - Implement next (issuer/verifier lifecycle first) |
| Author | SD-JWT .NET Team |
| Created | 2026-03-04 |
| Reviewed | 2026-05-09 |
| Maturity | Spec-tracking (Token Status List is IETF draft-20, not yet RFC) |
| Package | SdJwt.Net.StatusList (extension) |
| New package? | Optional: SdJwt.Net.StatusList.Bitstring or SdJwt.Net.VcDm.StatusList for W3C Bitstring (deferred) |
| Public API? | Yes |
| Specifications | Token Status List draft-20, Bitstring Status List v1.0 |
Context / Problem statement¶
The current SdJwt.Net.StatusList package provides Token Status List creation, verification, freshness checks, and RFC 7662 token introspection. SdJwt.Net.Wallet also includes a status-list-backed document status resolver. SdJwt.Net.VcDm models W3C BitstringStatusListEntry, but it does not issue, publish, fetch, or validate Bitstring Status List credentials.
Remaining lifecycle work is focused on richer issuer controls and additional status formats.
Work packages¶
This proposal is split into three work packages, implemented in order:
- Issuer status lifecycle controls (implement first)
- Verifier status policy (implement first)
- Bitstring Status List v1.0 (implement later, separate package)
Wallet-side status polling is repositioned as an optional reference wallet refresh service, not a default wallet behavior.
Goals¶
- Provide fluent APIs for credential revocation, suspension, and reinstatement
- Support status index allocation and persistence
- Enforce both temporal validity and status list checks with configurable fail-open / fail-closed behavior
- Produce structured audit events for status mutations
- Handle concurrency and atomic batch updates safely
Non-Goals¶
- CRL (Certificate Revocation List) support
- OCSP (Online Certificate Status Protocol) support
- Real-time push notification of revocation events to wallets
- Wallet-side status polling as a default behavior (optional reference only)
Direction¶
- Keep Token Status List and W3C Bitstring Status List as separate implementations behind a shared status-check abstraction.
- Do not make W3C Bitstring Status List a dependency of the SD-JWT VC-only path; keep it as a separate optional package.
- Preserve existing
StatusListVerifier,HybridStatusChecker, and wallet document status resolver APIs. - Treat issuer-side mutation APIs as storage-backed operations, not in-memory-only helpers.
- If verifiers fetch status lists directly from issuer infrastructure, access logs can still reveal verifier activity. CDN/public caching is recommended to preserve privacy.
Implementation plan¶
Architecture¶
flowchart TB
subgraph Issuer["Issuer Side"]
LifecycleMgr["Credential Lifecycle Manager<br/>(new)"]
IndexAlloc["IStatusIndexAllocator<br/>(new)"]
StatusPublisher["Status Publisher<br/>(existing, enhanced)"]
end
subgraph Verifier["Verifier Side"]
StatusPolicy["Status Check Policy<br/>(enhanced)"]
end
subgraph Optional["Optional"]
StatusPoller["Status Poller<br/>(reference only)"]
BitstringPkg["Bitstring Status List<br/>(deferred, separate package)"]
end
LifecycleMgr --> IndexAlloc
LifecycleMgr --> StatusList["SdJwt.Net.StatusList"]
LifecycleMgr --> StatusPublisher
StatusPolicy --> StatusList
StatusPoller --> StatusList
StatusPoller --> WalletCore["SdJwt.Net.Wallet"]
Work Package 1: Issuer status lifecycle controls¶
IStatusIndexAllocator¶
/// <summary>
/// Allocates and persists status indexes for credential lifecycle tracking.
/// </summary>
public interface IStatusIndexAllocator
{
Task<CredentialStatusReference> AllocateAsync(
CredentialStatusAllocationRequest request,
CancellationToken ct = default);
}
public sealed class CredentialStatusAllocationRequest
{
public required string StatusListUri { get; init; }
public required string StatusPurpose { get; init; }
public int BitSize { get; init; } = 1;
public string? CredentialId { get; init; }
public string? TenantId { get; init; }
}
public sealed class CredentialStatusReference
{
public required string StatusListUri { get; init; }
public required int StatusIndex { get; init; }
public required string StatusPurpose { get; init; }
public int BitSize { get; init; }
}
CredentialLifecycleManager¶
public sealed class CredentialLifecycleManager
{
/// <summary>
/// Revoke with reason and audit metadata.
/// </summary>
public Task RevokeAsync(
int statusIndex,
RevocationReason reason,
StatusMutationContext context);
/// <summary>
/// Suspend (temporary, can be reinstated).
/// </summary>
public Task SuspendAsync(
int statusIndex,
string reason,
StatusMutationContext context);
/// <summary>
/// Reinstate a suspended credential.
/// </summary>
public Task ReinstateAsync(
int statusIndex,
StatusMutationContext context);
/// <summary>
/// Batch operations with concurrency control.
/// </summary>
public Task BatchUpdateAsync(
IEnumerable<StatusUpdate> updates,
StatusMutationContext context);
/// <summary>
/// Publish updated status list with versioning.
/// </summary>
public Task<StatusListPublishResult> PublishAsync(PublishOptions options);
}
public enum RevocationReason
{
Unspecified = 0,
KeyCompromise = 1,
AffiliationChanged = 2,
Superseded = 3,
PrivilegeWithdrawn = 4,
CessationOfOperation = 5
}
StatusMutationContext¶
Structured audit context for every status mutation:
public sealed class StatusMutationContext
{
public required string OperatorId { get; init; }
public string? CorrelationId { get; init; }
public string? Reason { get; init; }
}
Audit event model¶
The lifecycle manager produces structured events:
public sealed class CredentialStatusChangedEvent
{
public required string CredentialId { get; init; }
public required int StatusIndex { get; init; }
public required string OldStatus { get; init; }
public required string NewStatus { get; init; }
public required string Reason { get; init; }
public required string OperatorId { get; init; }
public required DateTimeOffset Timestamp { get; init; }
public string? CorrelationId { get; init; }
public string? StatusListVersion { get; init; }
}
Concurrency and publishing model¶
Status updates are stateful and require:
- Optimistic concurrency / ETag for status list documents
- Versioned status list documents
- Atomic batch update semantics
- Publish after mutation
- Rollback strategy on publish failure
public sealed class PublishOptions
{
public string? ExpectedVersion { get; init; }
public bool RequireVersionMatch { get; init; } = true;
}
public sealed class StatusListPublishResult
{
public required string StatusListUri { get; init; }
public required string Version { get; init; }
public required DateTimeOffset PublishedAt { get; init; }
}
Work Package 2: Verifier status policy¶
public sealed class StatusCheckPolicyOptions
{
public bool CheckExpiry { get; set; } = true;
public bool CheckNotBefore { get; set; } = true;
public bool CheckStatusList { get; set; } = true;
public bool FailClosedOnStatusError { get; set; } = true;
public TimeSpan ClockSkew { get; set; } = TimeSpan.FromSeconds(30);
public TimeSpan MaxStatusListAge { get; set; } = TimeSpan.FromMinutes(15);
}
Verification flow¶
flowchart TB
Start["Receive Credential"] --> SigCheck["1. Signature Verification"]
SigCheck -->|Fail| Reject["Reject"]
SigCheck -->|Pass| NotBefore["2. Check valid-from (nbf)"]
NotBefore -->|"Not yet valid"| Reject
NotBefore -->|Valid| Expiry["3. Check expiry (exp)"]
Expiry -->|Expired| Reject
Expiry -->|Valid| StatusCheck["4. Fetch Status List"]
StatusCheck -->|"Fetch failed"| FailMode{"Fail-closed?"}
FailMode -->|Yes| Reject
FailMode -->|No| Accept["Accept (degraded)"]
StatusCheck -->|"Fetched OK"| CheckBit["5. Check status bit"]
CheckBit -->|Revoked| Reject
CheckBit -->|Suspended| Reject
CheckBit -->|Valid| Accept
Work Package 3: Bitstring Status List v1.0 (deferred)¶
The W3C Bitstring Status List v1.0 Recommendation provides an alternative to IETF Token Status List for W3C Verifiable Credentials.
| Feature | IETF Token Status List | W3C Bitstring Status List |
|---|---|---|
| Format | JWT wrapping compressed bitstring | JSON-LD VerifiableCredential + bitstring |
| Ecosystem | OpenID4VC, SD-JWT VC | W3C VC Data Model, JSON-LD |
| Compression | ZLIB | GZIP |
| Status values | 1/2/4/8-bit configurable | 1-bit default; multi-bit with status messages |
Package boundary:
SdJwt.Net.StatusList // IETF Token Status List (existing)
SdJwt.Net.StatusList.Bitstring // W3C Bitstring Status List (new, optional)
Do not mix Bitstring into the SD-JWT VC happy path.
Optional: Wallet status polling¶
Wallet background polling is positioned as an optional reference wallet refresh service, not a default wallet behavior. It introduces privacy, UX, battery, and network implications.
public class StatusPollerOptions
{
public TimeSpan PollInterval { get; set; } = TimeSpan.FromHours(1);
public bool NotifyOnRevocation { get; set; } = true;
public bool RemoveRevokedCredentials { get; set; } = false;
}
Security considerations¶
| Concern | Mitigation |
|---|---|
| Status list staleness | Configurable max age + TTL headers |
| Privacy (verifier learns which credential is checked) | Status list download is unlinkable (verifier gets full list, checks locally). CDN/public caching recommended to prevent issuer access log leakage. |
| Revocation racing | Fail-closed by default; status list freshness validation |
| Unauthorized revocation | Lifecycle manager requires operator identity; structured audit events |
| Concurrent batch corruption | Optimistic concurrency / ETag; versioned status list documents |
Acceptance criteria¶
Given a credential with status index N,
when RevokeAsync is called with KeyCompromise reason,
then the status bit is set and a CredentialStatusChangedEvent is produced.
Given a suspended credential,
when ReinstateAsync is called,
then the credential returns to valid status.
Given a batch update of 100 status changes,
when PublishAsync is called with a version mismatch,
then the operation fails without partial updates.
Given a verifier with fail-closed policy,
when the status list fetch fails,
then the credential is rejected.
Given a verifier with fail-open policy,
when the status list fetch fails,
then the credential is accepted with degraded status.
Interop test requirements¶
- Status stale test: verifier rejects credential when status list exceeds max age
- Expiry test: credential with expired validity window rejected
- Replay test: revoked credential consistently rejected across fetches
- Batch test: concurrent batch updates produce consistent status list
- Negative test: revocation of already-revoked credential is idempotent
Estimated effort¶
| Component | Effort |
|---|---|
| Shared status-check contracts | 2 days |
IStatusIndexAllocator |
2 days |
CredentialLifecycleManager with audit events |
3 days |
| Concurrency/publishing model | 2 days |
| Verifier status check policy | 3 days |
| Tests + documentation | 4 days |
| Subtotal (Work Packages 1+2) | 16 days |
| Bitstring Status List v1.0 processor (deferred) | 6 days |
| Wallet status polling (optional) | 3 days |
Related documentation¶
- Status List - Current implementation
- Managing Revocation Guide - Current guide
- Wallet - Wallet integration