Skip to main content

Verifiable Credential Revocation

Overview

The example below demonstrates two methods that an issuer can use to revoke a verifiable credential using the IOTA Identity Framework:

  1. By using the credentialStatus field in a credential and linking to a revocation bitmap, using the RevocationBitmap2022.
  2. By removing the verification method that signed the credential. This invalidates all credentials that were signed with that verification method.

Revocation Bitmap

One of the ways for an issuer to control the status of its credentials is by using a revocation list. At the most basic level, revocation information for all verifiable credentials issued by an issuer are expressed as simple binary values. The issuer keeps a list of all verifiable credentials it has issued in a bitmap. Each verifiable credential is associated with a unique index in the list. If the binary value of the index in the bitmap is 1 (one), the verifiable credential is revoked, if it is 0 (zero) it is not revoked.

For example, with this approach the issuer adds an index to a credential in the credentialStatus field, such as "5". This part of the credential might then look like this:

"credentialStatus": {
"id": "did:iota:EvaQhPXXsJsGgxSXGhZGMCvTt63KuAFtaGThx6a5nSpw#revocation",
"type": "RevocationBitmap2022",
"revocationBitmapIndex": "5"
},

The verifier uses the id field (did:iota:EvaQhPXXsJsGgxSXGhZGMCvTt63KuAFtaGThx6a5nSpw#revocation) to look up the service in the issuer's DID document. This is an example of such a service:

{
"id": "did:iota:EvaQhPXXsJsGgxSXGhZGMCvTt63KuAFtaGThx6a5nSpw#revocation",
"type": "RevocationBitmap2022",
"serviceEndpoint": "data:application/octet-stream;base64,ZUp5ek1tQmdZR1NBQUFFZ1ptVUFBQWZPQUlF"
}

During verification the verifier decodes the revocation bitmap embedded in the data url. This bitmap written as a bitstring looks like this: 000001. Here, the 5th bit is set, which means the credential with that index is revoked, while all other credentials aren't revoked.

Removing the verification method

A less efficient alternative is to remove the verification method that signed the credential from the DID Document of the issuer. This means the VC can no longer be validated. However, this would invalidate every VC signed with that verification method, meaning the issuer would have to sign every VC with a different key to retain precise control over which credential is revoked.

Example

The following code exemplifies how you can revoke a Verifiable Credential (VC).

// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { Client, MnemonicSecretManager } from "@iota/client-wasm/node";
import { Bip39 } from "@iota/crypto.js";
import {
Credential,
CredentialValidationOptions,
CredentialValidator,
FailFast,
IotaDocument,
IotaIdentityClient,
ProofOptions,
Resolver,
RevocationBitmap,
Service,
VerificationMethod,
} from "@iota/identity-wasm/node";
import { IAliasOutput, IRent, TransactionHelper } from "@iota/iota.js";
import { API_ENDPOINT, createDid } from "../util";

/**
* This example shows how to revoke a verifiable credential.
* It demonstrates two methods for revocation. The first uses a revocation bitmap of type `RevocationBitmap2022`,
* while the second method simply removes the verification method (public key) that signed the credential
* from the DID Document of the issuer.
*
* Note: make sure `API_ENDPOINT` and `FAUCET_ENDPOINT` are set to the correct network endpoints.
*/
export async function revokeVC() {
// ===========================================================================
// Create a Verifiable Credential.
// ===========================================================================

const client = new Client({
primaryNode: API_ENDPOINT,
localPow: true,
});
const didClient = new IotaIdentityClient(client);

// Generate a random mnemonic for our wallet.
const secretManager: MnemonicSecretManager = {
mnemonic: Bip39.randomMnemonic(),
};

// Create an identity for the issuer with one verification method `key-1`.
let { document: issuerDocument, keypair: keypairIssuer } = await createDid(client, secretManager);

// Create an identity for the holder, in this case also the subject.
const { document: aliceDocument } = await createDid(client, secretManager);

// Create a new empty revocation bitmap. No credential is revoked yet.
const revocationBitmap = new RevocationBitmap();

// Add the revocation bitmap to the DID Document of the issuer as a service.
const service: Service = new Service({
id: issuerDocument.id().join("#my-revocation-service"),
type: RevocationBitmap.type(),
serviceEndpoint: revocationBitmap.toEndpoint(),
});
issuerDocument.insertService(service);

// Resolve the latest output and update it with the given document.
let aliasOutput: IAliasOutput = await didClient.updateDidOutput(issuerDocument);

// Because the size of the DID document increased, we have to increase the allocated storage deposit.
// This increases the deposit amount to the new minimum.
let rentStructure: IRent = await didClient.getRentStructure();
aliasOutput.amount = TransactionHelper.getStorageDeposit(aliasOutput, rentStructure).toString();

// Publish the document.
issuerDocument = await didClient.publishDidOutput(secretManager, aliasOutput);

// Create a credential subject indicating the degree earned by Alice, linked to their DID.
const subject = {
id: aliceDocument.id(),
name: "Alice",
degreeName: "Bachelor of Science and Arts",
degreeType: "BachelorDegree",
GPA: "4.0",
};

// Create an unsigned `UniversityDegree` credential for Alice.
// The issuer also chooses a unique `RevocationBitmap` index to be able to revoke it later.
const CREDENTIAL_INDEX = 5;
const unsignedVc = new Credential({
id: "https://example.edu/credentials/3732",
type: "UniversityDegreeCredential",
credentialStatus: {
id: issuerDocument.id() + "#my-revocation-service",
type: RevocationBitmap.type(),
revocationBitmapIndex: CREDENTIAL_INDEX.toString(),
},
issuer: issuerDocument.id(),
credentialSubject: subject,
});

// Sign Credential.
let signedVc = issuerDocument.signCredential(unsignedVc, keypairIssuer.private(), "#key-1", ProofOptions.default());
console.log(`Credential JSON > ${JSON.stringify(signedVc, null, 2)}`);

// Validate the credential's signature using the issuer's DID Document.
CredentialValidator.validate(signedVc, issuerDocument, CredentialValidationOptions.default(), FailFast.AllErrors);

// ===========================================================================
// Revocation of the Verifiable Credential.
// ===========================================================================

// Update the RevocationBitmap service in the issuer's DID Document.
// This revokes the credential's unique index.
issuerDocument.revokeCredentials("my-revocation-service", CREDENTIAL_INDEX);

// Publish the changes.
aliasOutput = await didClient.updateDidOutput(issuerDocument);
rentStructure = await didClient.getRentStructure();
aliasOutput.amount = TransactionHelper.getStorageDeposit(aliasOutput, rentStructure).toString();
const update2: IotaDocument = await didClient.publishDidOutput(secretManager, aliasOutput);

// Credential verification now fails.
try {
CredentialValidator.validate(signedVc, update2, CredentialValidationOptions.default(), FailFast.FirstError);
console.log("Revocation Failed!");
} catch (e) {
console.log(`Error during validation: ${e}`);
}

// ===========================================================================
// Alternative revocation of the Verifiable Credential.
// ===========================================================================

// By removing the verification method, that signed the credential, from the issuer's DID document,
// we effectively revoke the credential, as it will no longer be possible to validate the signature.
let originalMethod = issuerDocument.resolveMethod("#key-1") as VerificationMethod;
await issuerDocument.removeMethod(originalMethod.id());

// Publish the changes.
aliasOutput = await didClient.updateDidOutput(issuerDocument);
rentStructure = await didClient.getRentStructure();
aliasOutput.amount = TransactionHelper.getStorageDeposit(aliasOutput, rentStructure).toString();
issuerDocument = await didClient.publishDidOutput(secretManager, aliasOutput);

// We expect the verifiable credential to be revoked.
const resolver = new Resolver({ client: didClient });
try {
// Resolve the issuer's updated DID Document to ensure the key was revoked successfully.
const resolvedIssuerDoc = await resolver.resolve(issuerDocument.id().toString());
CredentialValidator.validate(
signedVc,
resolvedIssuerDoc,
CredentialValidationOptions.default(),
FailFast.FirstError,
);

// `CredentialValidator.validate` will throw an error, hence this will not be reached.
console.log("Revocation failed!");
} catch (e) {
console.log(`Error during validation: ${e}`);
console.log(`Credential successfully revoked!`);
}
}