UNPKG

@sphereon/ssi-sdk.vc-status-list

Version:

Sphereon SSI-SDK plugin for Status List management, like StatusList2021.

497 lines (450 loc) • 20.1 kB
/** * Bitstring Status List Implementation * * This module implements the W3C Bitstring Status List specification for managing * credential status information. It provides functionality to create, update, and * check the status of verifiable credentials using compressed bitstring status lists. * * Key features: * - Create new bitstring status lists with configurable purposes and bit sizes * - Update individual credential status entries in existing lists * - Check the status of specific credentials by index * - Support for multiple proof formats (JWT, LDS, CBOR) * - Integration with Veramo agent context for credential signing * * @author Sphereon International B.V. * @since 2024 */ import type { IAgentContext } from '@veramo/core' import type { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' import { CredentialMapper, type CredentialProofFormat, DocumentFormat, type IIssuer, type StatusListCredential, StatusListType, } from '@sphereon/ssi-types' import type { IBitstringStatusListImplementationResult, IExtractedCredentialDetails, IStatusList } from './IStatusList' import { CheckStatusIndexArgs, CreateStatusListArgs, IMergeDetailsWithEntityArgs, IToDetailsFromCredentialArgs, StatusListResult, UpdateStatusListFromEncodedListArgs, UpdateStatusListIndexArgs, } from '../types' import { assertValidProofType, ensureDate, getAssertedProperty, getAssertedValue, getAssertedValues } from '../utils' import { BitstringStatusListCredential } from '../types/BitstringStatusList' import { BitstreamStatusList, BitstringStatusListCredentialUnsigned, BitstringStatusPurpose, createStatusListCredential, } from '@4sure-tech/vc-bitstring-status-lists' import { BitstringStatusListEntity, BitstringStatusListEntryCredentialStatus, IBitstringStatusListEntryEntity, IStatusListEntryEntity, StatusListEntity, } from '@sphereon/ssi-sdk.data-store' import { IVcdmCredentialPlugin } from '@sphereon/ssi-sdk.credential-vcdm' export const DEFAULT_LIST_LENGTH = 131072 // W3C spec minimum export const DEFAULT_PROOF_FORMAT = 'vc+jwt' as CredentialProofFormat export const DEFAULT_STATUS_PURPOSE: BitstringStatusPurpose = 'revocation' /** * Implementation of the IStatusList interface for W3C Bitstring Status Lists * * This class handles the creation, updating, and verification of bitstring status lists * according to the W3C Bitstring Status List specification. It supports multiple * status purposes (revocation, suspension, etc.) and various proof formats. */ export class BitstringStatusListImplementation implements IStatusList { /** * Creates a new bitstring status list with the specified configuration * * @param args - Configuration for the new status list including issuer, purpose, and size * @param context - Veramo agent context for credential operations * @returns Promise resolving to the created status list details */ async createNewStatusList( args: CreateStatusListArgs, context: IAgentContext<IVcdmCredentialPlugin & IIdentifierResolution>, ): Promise<StatusListResult> { if (!args.bitstringStatusList) { throw new Error('BitstringStatusList options are required for type BitstringStatusList') } const length = args?.length ?? DEFAULT_LIST_LENGTH const proofFormat: CredentialProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT assertValidProofType(StatusListType.BitstringStatusList, proofFormat) const { issuer, id } = args const correlationId = getAssertedValue('correlationId', args.correlationId) const { statusPurpose, bitsPerStatus, validFrom, validUntil, ttl } = args.bitstringStatusList const unsignedCredential: BitstringStatusListCredentialUnsigned = await createStatusListCredential({ id, issuer, statusPurpose: statusPurpose ?? DEFAULT_STATUS_PURPOSE, validFrom: ensureDate(validFrom), validUntil: ensureDate(validUntil), ttl, }) const statusListCredential = await this.createVerifiableCredential( { unsignedCredential, id, issuer, proofFormat, keyRef: args.keyRef, }, context, ) return { encodedList: unsignedCredential.credentialSubject.encodedList, statusListCredential, bitstringStatusList: { statusPurpose: statusPurpose ?? DEFAULT_STATUS_PURPOSE, ...(unsignedCredential.validFrom && { validFrom: new Date(unsignedCredential.validFrom) }), ...(unsignedCredential.validUntil && { validUntil: new Date(unsignedCredential.validUntil) }), ttl, bitsPerStatus, }, length, type: StatusListType.BitstringStatusList, proofFormat, id, correlationId, issuer, statuslistContentType: this.buildContentType(proofFormat), } } /** * Updates the status of a specific credential in an existing status list * * @param args - Update parameters including the status list credential, index, and new value * @param context - Veramo agent context for credential operations * @returns Promise resolving to the updated status list details */ async updateStatusListIndex( args: UpdateStatusListIndexArgs, context: IAgentContext<IVcdmCredentialPlugin & IIdentifierResolution>, ): Promise<StatusListResult> { if (!args.bitsPerStatus || args.bitsPerStatus < 1) { return Promise.reject(Error('bitsPerStatus must be set for bitstring status lists and must be 1 or higher. (updateStatusListIndex)')) } const credential = args.statusListCredential const uniform = CredentialMapper.toUniformCredential(credential) const { issuer, credentialSubject } = uniform const id = getAssertedValue('id', uniform.id) const origEncodedList = getAssertedProperty('encodedList', credentialSubject) const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) const statusList: BitstreamStatusList = await BitstreamStatusList.decode({ encodedList: origEncodedList, statusSize: args.bitsPerStatus }) const bitstringStatusId = args.value as number statusList.setStatus(index, bitstringStatusId) const proofFormat = CredentialMapper.detectDocumentType(credential) === DocumentFormat.JWT ? 'vc+jwt' : 'lds' const credSubject = Array.isArray(credentialSubject) ? credentialSubject[0] : credentialSubject const statusPurpose = getAssertedProperty('statusPurpose', credSubject) const validFrom = uniform.validFrom ? new Date(uniform.validFrom) : undefined const validUntil = uniform.validUntil ? new Date(uniform.validUntil) : undefined const ttl = credSubject.ttl const unsignedCredential: BitstringStatusListCredentialUnsigned = await createStatusListCredential({ id, issuer, statusList, statusPurpose: statusPurpose ?? DEFAULT_STATUS_PURPOSE, validFrom: ensureDate(validFrom), validUntil: ensureDate(validUntil), ttl, }) const updatedCredential = await this.createVerifiableCredential( { unsignedCredential, id, issuer, proofFormat, keyRef: args.keyRef, }, context, ) return { statusListCredential: updatedCredential, encodedList: unsignedCredential.credentialSubject.encodedList, bitstringStatusList: { statusPurpose, ...(unsignedCredential.validFrom && { validFrom: new Date(unsignedCredential.validFrom) }), ...(unsignedCredential.validUntil && { validUntil: new Date(unsignedCredential.validUntil) }), bitsPerStatus: args.bitsPerStatus, ttl, }, length: statusList.getLength(), type: StatusListType.BitstringStatusList, proofFormat, id, issuer, statuslistContentType: this.buildContentType(proofFormat), } } /** * Updates a status list by decoding an encoded list, modifying it, and re-encoding * * @param args - Update parameters including encoded list, index, and new value * @param context - Veramo agent context for credential operations * @returns Promise resolving to the updated status list details */ async updateStatusListFromEncodedList( args: UpdateStatusListFromEncodedListArgs, context: IAgentContext<IVcdmCredentialPlugin & IIdentifierResolution>, ): Promise<StatusListResult> { if (!args.bitstringStatusList) { throw new Error('bitstringStatusList options required for type BitstringStatusList') } if (args.bitstringStatusList.bitsPerStatus < 1) { return Promise.reject(Error('bitsPerStatus must be set for bitstring status lists and must be 1 or higher. (updateStatusListFromEncodedList)')) } const { statusPurpose, bitsPerStatus, ttl, validFrom, validUntil } = args.bitstringStatusList const proofFormat: CredentialProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT assertValidProofType(StatusListType.BitstringStatusList, proofFormat) const { issuer, id } = getAssertedValues(args) const statusList: BitstreamStatusList = await BitstreamStatusList.decode({ encodedList: args.encodedList, statusSize: bitsPerStatus }) const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) statusList.setStatus(index, args.value) const unsignedCredential: BitstringStatusListCredentialUnsigned = await createStatusListCredential({ id, issuer, statusList, statusPurpose: statusPurpose ?? DEFAULT_STATUS_PURPOSE, validFrom: ensureDate(validFrom), validUntil: ensureDate(validUntil), ttl, }) const credential = await this.createVerifiableCredential( { unsignedCredential, id, issuer, proofFormat, keyRef: args.keyRef, }, context, ) return { type: StatusListType.BitstringStatusList, statusListCredential: credential, encodedList: unsignedCredential.credentialSubject.encodedList, bitstringStatusList: { statusPurpose, bitsPerStatus, ...(unsignedCredential.validFrom && { validFrom: new Date(unsignedCredential.validFrom) }), ...(unsignedCredential.validUntil && { validUntil: new Date(unsignedCredential.validUntil) }), ttl, }, length: statusList.getLength(), proofFormat: args.proofFormat ?? 'lds', id, issuer, statuslistContentType: this.buildContentType(proofFormat), } } /** * Checks the status of a specific credential by its index in the status list * * @param args - Check parameters including the status list credential and index * @returns Promise resolving to the status value at the specified index */ async checkStatusIndex(args: CheckStatusIndexArgs): Promise<number> { if (!args.bitsPerStatus || args.bitsPerStatus < 1) { return Promise.reject(Error('bitsPerStatus must be set for bitstring status lists and must be 1 or higher. (checkStatusIndex)')) } const uniform = CredentialMapper.toUniformCredential(args.statusListCredential) const { credentialSubject } = uniform const encodedList = getAssertedProperty('encodedList', credentialSubject) const statusList = await BitstreamStatusList.decode({ encodedList, statusSize: args.bitsPerStatus }) const numIndex = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) if (statusList.getLength() <= numIndex) { throw new Error(`Status list index out of bounds, has ${statusList.getLength()} entries, requested ${numIndex}`) } return statusList.getStatus(numIndex) } /** * Performs the initial parsing of a StatusListCredential. * This method handles expensive operations like JWT/CWT decoding once. * It extracts all details available from the credential payload itself. */ async extractCredentialDetails(credential: StatusListCredential): Promise<IExtractedCredentialDetails> { const uniform = CredentialMapper.toUniformCredential(credential) const { issuer, credentialSubject } = uniform const subject = Array.isArray(credentialSubject) ? credentialSubject[0] : credentialSubject return { id: getAssertedValue('id', uniform.id), issuer, encodedList: getAssertedProperty('encodedList', subject), } } /** * Converts a status list credential payload to detailed status list information * * @param args - Conversion parameters including the status list payload * @returns Promise resolving to detailed status list information */ // For CREATE and READ contexts async toStatusListDetails(args: IToDetailsFromCredentialArgs): Promise<StatusListResult & IBitstringStatusListImplementationResult> // For UPDATE contexts async toStatusListDetails(args: IMergeDetailsWithEntityArgs): Promise<StatusListResult & IBitstringStatusListImplementationResult> async toStatusListDetails( args: IToDetailsFromCredentialArgs | IMergeDetailsWithEntityArgs, ): Promise<StatusListResult & IBitstringStatusListImplementationResult> { if ('statusListCredential' in args) { // CREATE/READ context const { statusListCredential, bitsPerStatus, correlationId, driverType } = args if (!bitsPerStatus || bitsPerStatus < 1) { return Promise.reject(Error('bitsPerStatus must be set for bitstring status lists and must be 1 or higher')) } const uniform = CredentialMapper.toUniformCredential(statusListCredential) const { issuer, credentialSubject } = uniform const subject = Array.isArray(credentialSubject) ? credentialSubject[0] : credentialSubject const id = getAssertedValue('id', uniform.id) const encodedList = getAssertedProperty('encodedList', subject) const statusPurpose = getAssertedProperty('statusPurpose', subject) const validFrom = uniform.validFrom ? new Date(uniform.validFrom) : undefined const validUntil = uniform.validUntil ? new Date(uniform.validUntil) : undefined const ttl = subject.ttl const proofFormat: CredentialProofFormat = CredentialMapper.detectDocumentType(statusListCredential) === DocumentFormat.JWT ? 'vc+jwt' : 'lds' const statuslistLength = BitstreamStatusList.getStatusListLength(encodedList, bitsPerStatus) return { id, encodedList, issuer, type: StatusListType.BitstringStatusList, proofFormat, length: statuslistLength, statusListCredential, statuslistContentType: this.buildContentType(proofFormat), correlationId, driverType, statusPurpose, bitsPerStatus, ...(validFrom && { validFrom }), ...(validUntil && { validUntil }), ...(ttl && { ttl }), bitstringStatusList: { statusPurpose, bitsPerStatus, ...(validFrom && { validFrom }), ...(validUntil && { validUntil }), ...(ttl && { ttl }), }, } } else { // UPDATE context const { extractedDetails, statusListEntity } = args const bitstringEntity = statusListEntity as BitstringStatusListEntity if (!bitstringEntity.bitsPerStatus) { return Promise.reject(Error('bitsPerStatus must be present for a bitstring status list')) } const proofFormat: CredentialProofFormat = CredentialMapper.detectDocumentType(statusListEntity.statusListCredential!) === DocumentFormat.JWT ? 'vc+jwt' : 'lds' const statuslistLength = BitstreamStatusList.getStatusListLength(extractedDetails.encodedList, bitstringEntity.bitsPerStatus) return { id: extractedDetails.id, encodedList: extractedDetails.encodedList, issuer: extractedDetails.issuer, type: StatusListType.BitstringStatusList, proofFormat, length: statuslistLength, statusListCredential: statusListEntity.statusListCredential!, statuslistContentType: this.buildContentType(proofFormat), correlationId: statusListEntity.correlationId, driverType: statusListEntity.driverType, statusPurpose: bitstringEntity.statusPurpose, bitsPerStatus: bitstringEntity.bitsPerStatus, ...(bitstringEntity.validFrom && { validFrom: bitstringEntity.validFrom }), ...(bitstringEntity.validUntil && { validUntil: bitstringEntity.validUntil }), ...(bitstringEntity.ttl && { ttl: bitstringEntity.ttl }), bitstringStatusList: { statusPurpose: bitstringEntity.statusPurpose, bitsPerStatus: bitstringEntity.bitsPerStatus, ...(bitstringEntity.validFrom && { validFrom: bitstringEntity.validFrom }), ...(bitstringEntity.validUntil && { validUntil: bitstringEntity.validUntil }), ...(bitstringEntity.ttl && { ttl: bitstringEntity.ttl }), }, } } } /** * Creates a credential status entry for a specific credential in a status list * * @param args - Parameters including the status list, entry details, and index * @returns Promise resolving to the credential status entry */ async createCredentialStatus(args: { statusList: StatusListEntity statusListEntry: IStatusListEntryEntity | IBitstringStatusListEntryEntity statusListIndex: number }): Promise<BitstringStatusListEntryCredentialStatus> { const { statusList, statusListEntry, statusListIndex } = args const bitstringStatusList = statusList as BitstringStatusListEntity const bitstringStatusListEntry = statusListEntry as IBitstringStatusListEntryEntity return { id: `${statusList.id}#${statusListIndex}`, type: 'BitstringStatusListEntry', statusPurpose: bitstringStatusListEntry.statusPurpose, statusListIndex: '' + statusListIndex, statusListCredential: statusList.id, bitsPerStatus: bitstringStatusList.bitsPerStatus, statusMessage: bitstringStatusListEntry.statusMessage, statusReference: bitstringStatusListEntry.statusReference, } satisfies BitstringStatusListEntryCredentialStatus } /** * Creates a signed verifiable credential from an unsigned status list credential * * @param args - Parameters including the unsigned credential and signing details * @param context - Veramo agent context for credential operations * @returns Promise resolving to the signed credential */ private async createVerifiableCredential( args: { unsignedCredential: BitstringStatusListCredentialUnsigned id: string issuer: string | IIssuer proofFormat: CredentialProofFormat keyRef?: string }, context: IAgentContext<IVcdmCredentialPlugin & IIdentifierResolution>, ): Promise<BitstringStatusListCredential> { const { unsignedCredential, issuer, proofFormat, keyRef } = args const identifier = await context.agent.identifierManagedGet({ identifier: typeof issuer === 'string' ? issuer : issuer.id, vmRelationship: 'assertionMethod', offlineWhenNoDIDRegistered: true, }) const verifiableCredential = await context.agent.createVerifiableCredential({ credential: unsignedCredential, keyRef: keyRef ?? identifier.kmsKeyRef, proofFormat, fetchRemoteContexts: true, }) return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as StatusListCredential).original as BitstringStatusListCredential } /** * Builds the appropriate content type string for a given proof format * * @param proofFormat - The proof format to build content type for * @returns The corresponding content type string */ private buildContentType(proofFormat: CredentialProofFormat | undefined): string { switch (proofFormat) { case 'jwt': return 'application/statuslist+jwt' case 'cbor': return 'application/statuslist+cwt' case 'vc+jwt': return 'application/statuslist+vc+jwt' case 'lds': return 'application/statuslist+ld+json' default: throw Error(`Unsupported content type '${proofFormat}' for status lists`) } } }