light-mode-image
Learn
Guides

How to implement mDocs revocation status checks in your verifier application

Overview

This guide demonstrates how to implement revocation status checks for mDocs in your verifier applications. By implementing status checks, your verifier can confirm whether credentials have been revoked, suspended, or remain valid before accepting them.

MATTR's implementation of mDocs revocation is based on the IETF Token Status List draft. A revocable mDoc includes a reference to a status list which is managed by the issuer. The list contains the revocation status of multiple credentials, and each credential references the index of its status within a specific status list.

Status lists are automatically created and managed by the issuer's MATTR VII tenant when issuing revocable mDocs. They are publicly available and mainly consumed by verifier applications to check the status of presented mDocs.

For detailed information about how mDocs revocation works, including status list structure, tokens, and signing, see the Revocation documentation.

Prerequisites

This guide builds on the In-person Verification tutorial. It is recommended to complete that tutorial first, then return here to learn how to implement status checks.

Understanding revocation status

Status values

When verifying a presented mDoc, the revocation status can be one of the following:

  • Valid: The mDoc is valid.
  • Invalid: The mDoc is permanently revoked.
  • Suspended: The mDoc is temporarily revoked.
  • Unknown: The status cannot be determined, typically because the status list is unavailable or has expired. The verifier application must decide whether to accept or reject credentials with unknown status based on their security requirements.

How status information is stored

When a revocable mDoc is issued, it includes a status object in its MSO payload that references a status list:

mDoc status reference
"status": {
    "status_list": {
        "uri": "https://learn.vii.au01.mattr.global/v2/credentials/mobile/status-lists/f331c9be-f526-4577-bbac-ae93d6228f7a/token",
        "idx": 0
    }
}
  • status_list: References the status list that holds the status information for this mDoc.
  • uri: The publicly available endpoint where the status list token can be retrieved.
  • idx: The index of this mDoc's status within the referenced status list.

When a verifier retrieves a status list, the issuer cannot tell what specific mDoc they are checking the status for. This preserves holder privacy - the issuer does not know how often or to whom an mDoc is being presented.

Status list caching and updates

When retrieving a status list, the response is a signed status list token (a CBOR Web Token) that includes:

  • iat: Timestamp when the status list token was signed.
  • exp: Expiry timestamp.
  • ttl: Recommended duration in seconds before fetching a new token.
  • status_list: The compressed status list containing the status of all mDocs included in this list.

Retrieving a status list for every credential operation would create performance issues and make offline presentation impossible. To address this, MATTR uses a caching mechanism based on the ttl and exp values:

  • After retrieving a status list, the application should not fetch it again until the ttl has passed, as there are unlikely to be any changes. This optimizes performance and reduces unnecessary network requests.
  • If the application fails to retrieve an updated status list after the TTL (for example, because the device is offline), it can continue using the cached status until the status list token expiry date (exp).
  • If the expiry date passes without a successful update, the credential status can no longer be trusted and it is changed to Unknown. It is then up to the application to decide how to handle credentials with Unknown status.

How the SDK manages cache updates

The Verifier SDK automatically calculates a nextUpdateDate for each cached status list token. This date is determined as the earlier of:

  • Token retrieval time + ttl
  • Token expiry time (exp)

Based on this calculation:

  • Before nextUpdateDate: The SDK continues to use the cached token without attempting to fetch a new one.
  • After nextUpdateDate: The SDK attempts to retrieve a new status list token when status verification is performed.

Offline behavior

When the verifier device is offline:

  • If the current time is after ttl but before exp: The SDK cannot retrieve a status list (as the device is offline), the SDK will continue to use the cached status.
  • If the current time is after exp: The cached status list is no longer valid, and the status will be returned as Unknown.

This behavior ensures that verifiers don't rely on stale status information beyond the issuer's intended timeframe.

Implementing status checks

When sending a proximity presentation request, you can control whether status checks should be performed:

Proximity presentation with status check
// Request mDoc with automatic status check (default behavior)
const response = await verifier.sendProximityPresentationRequest({
    mobileCredentialRequests,
    skipStatusCheck: false
});

// Request mDoc without status check
const response = await verifier.sendProximityPresentationRequest({  
    mobileCredentialRequests,  
    skipStatusCheck: true  
}); 
Proximity presentation with status check
// Request mDoc with automatic status check (default behavior)
val response = MobileCredentialVerifier.sendProximityPresentationRequest(
    request,
    skipStatusCheck = false
)

// Request mDoc without status check
val response = MobileCredentialVerifier.sendProximityPresentationRequest(
    request,
    skipStatusCheck = true
)
Proximity presentation with status check
// Request mDoc with automatic status check (default behavior)
const response = await verifier.sendProximityPresentationRequest(
    request,
    false // skipStatusCheck
);

// Request mDoc without status check (for faster verification)
const response = await verifier.sendProximityPresentationRequest(
    request,
    true // skipStatusCheck
);

When skipStatusCheck is false (the default):

  1. The SDK checks if the current time is after the cached status list's nextUpdateDate.
  2. If after nextUpdateDate, the SDK attempts to retrieve an updated status list.
  3. If the retrieval succeeds, the status is updated based on the new status list.
  4. If the retrieval fails (e.g., device is offline):
    • If the cached status list hasn't expired, the SDK continues using the cached status.
    • If the cached status list has expired, the status becomes Unknown.

Setting skipStatusCheck: true will bypass status check entirely. Use this only when you need faster verification and have an alternative method to check credential status, or when operating in environments where status checking is not required.

Proactive cache management

For better control over status list freshness and offline use cases, the Verifier SDK provides methods to manage the cache proactively:

Updating status lists

Use updateTrustedIssuerStatusLists() to force a refresh of all relevant status list tokens. This method fetches the latest status list tokens from all trusted issuers.

Update status lists
do {
    try await verifier.updateTrustedIssuerStatusLists()
    print("All status lists updated successfully")
} catch {
    print("Failed to update status lists: \(error.localizedDescription)")
}
Update status lists
MobileCredentialVerifier.updateTrustedIssuerStatusLists()
Update status lists
try {
    await verifier.updateTrustedIssuerStatusLists();
    console.log("All status lists updated successfully");
} catch (error) {
    console.error("Failed to update status lists:", error);
}

When to use this method:

  • Before the nextUpdateDate: Call this regularly (e.g., when the app starts or resumes) to ensure up-to-date verification without waiting for the nextUpdateDate to pass.
  • During idle periods: Update status lists when the verifier app is not actively verifying credentials.
  • After connectivity is restored: If the device was offline and is now back online, refresh the status lists.

Inspecting cache metadata

Use getTrustedIssuerStatusListsCacheInfo() to inspect the nextUpdate date for the status lists.

Inspect cache info
do {
    let cacheInfo = try await verifier.getTrustedIssuerStatusListsCacheInfo()
    
    for info in cacheInfo {
        print("Issuer: \(info.issuer)")
        print("Next update date: \(info.nextUpdateDate)")
        print("Expiry date: \(info.expiryDate)")
        
        // Check if update is needed
        if Date() > info.nextUpdateDate {
            print("Status list should be updated")
        }
    }
} catch {
    print("Failed to get cache info: \(error.localizedDescription)")
}
Inspect cache info
val cacheInfo = MobileCredentialVerifier.getTrustedIssuerStatusListsCacheInfo()
val nextUpdate = cacheInfo.nextUpdate
if (nextUpdate != null && nextUpdate < Clock.System.now()) {
    MobileCredentialVerifier.updateTrustedIssuerStatusLists()
}
Inspect cache info
try {
    const cacheInfo = await verifier.getTrustedIssuerStatusListsCacheInfo();
    
    for (const info of cacheInfo) {
        console.log("Issuer:", info.issuer);
        console.log("Next update date:", info.nextUpdateDate);
        console.log("Expiry date:", info.expiryDate);
        
        // Check if update is needed
        if (Date.now() > info.nextUpdateDate) {
            console.log("Status list should be updated");
        }
    }
} catch (error) {
    console.error("Failed to get cache info:", error);
}

When to use this method:

  • Monitoring cache health: Check when the next update is due.
  • Conditional updates: Decide whether to call updateTrustedIssuerStatusLists() based on the nextUpdateDate.

Checking credential status

After receiving a presentation response, you can check the status of each credential to determine whether it should be accepted:

Checking credential status
let response = try await verifier.sendProximityPresentationRequest(
    request: request,
    skipStatusCheck: false
)

for credential in response.credentials {
    // Check the overall verification result
    if credential.verificationResult.verified {
        print("Credential verification passed")
        
        // Check the credential status
        if let status = credential.status {
            switch status {
            case .valid:
                print("Status: Valid - Accept credential")
                // Accept the credential
            case .invalid:
                print("Status: Invalid - Reject credential (permanently revoked)")
                // Reject the credential
            case .suspended:
                print("Status: Suspended - Reject credential (temporarily revoked)")
                // Reject the credential
            case .unknown:
                print("Status: Unknown - Cannot determine status")
                // Handle according to your security requirements
            @unknown default:
                print("Status: Unexpected value")
            }
        } else {
            print("No status information available (credential may not be revocable)")
            // Handle non-revocable credentials
        }
    } else {
        print("Credential verification failed: \(credential.verificationResult.reason?.message ?? "Unknown reason")")
        // Reject the credential
    }
}
Checking credential status
val response = verifier.sendProximityPresentationRequest(
    request = request,
    skipStatusCheck = false
)

response.credentials.forEach { credential ->
val verificationResult = credential.verificationResult
if (verificationResult?.verified == true) {
    Log.d("Tag", "Credential is valid.")
} else {
    when (verificationResult?.failureType) {
        MobileCredentialVerificationFailureType.StatusRevoked -> {
            Log.d("Tag", "Credential has been revoked.")
            // Display warning, prevent presentation
        }

        MobileCredentialVerificationFailureType.StatusSuspended -> {
            Log.d("Tag", "Credential has been suspended.")
            // Display warning, prevent presentation
        }

        MobileCredentialVerificationFailureType.StatusUnknown -> {
            Log.d("Tag", "Credential status is unknown.")
            // Display warning, handle according to your security requirements
        }

        else -> {
            // Handle other failure types
        }
    }
}
}
Checking credential status
const response = await verifier.sendProximityPresentationRequest(
    request,
    false // skipStatusCheck
);

for (const credential of response.credentials) {
    // Check the overall verification result
    if (credential.verificationResult.verified) {
        console.log("Credential verification passed");
        
        // Check the credential status
        if (credential.status) {
            switch (credential.status) {
                case CredentialStatus.Valid:
                    console.log("Status: Valid - Accept credential");
                    // Accept the credential
                    break;
                case CredentialStatus.Invalid:
                    console.log("Status: Invalid - Reject credential (permanently revoked)");
                    // Reject the credential
                    break;
                case CredentialStatus.Suspended:
                    console.log("Status: Suspended - Reject credential (temporarily revoked)");
                    // Reject the credential
                    break;
                case CredentialStatus.Unknown:
                    console.log("Status: Unknown - Cannot determine status");
                    // Handle according to your security requirements
                    break;
            }
        } else {
            console.log("No status information available (credential may not be revocable)");
            // Handle non-revocable credentials
        }
    } else {
        console.log(`Credential verification failed: ${credential.verificationResult.reason?.message}`);
        // Reject the credential
    }
}

Handling offline scenarios

  • Cache reliance: When offline, the SDK relies on cached status lists.
    • Cached status after TTL: If the device is offline and ttl has passed (but expiry hasn't), the cached status will be used.
    • Unknown status after expiry: If the device is offline and exp has passed, the status will always be returned as Unknown.
  • Unknown status handling: Define your application's policy for handling credentials with Unknown status. Options include:
    • Rejecting credentials (most secure) - Recommended for high-security scenarios.
    • Accepting with warnings - May be appropriate for lower-risk scenarios.
  • User communication: Clearly inform verifiers when status checks fail and provide guidance on how to proceed.

How would you rate this page?

On this page