UNPKG

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

Version:

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

207 lines (182 loc) • 8.23 kB
import type { IAgentContext, ICredentialPlugin, IKeyManager } from '@veramo/core' import { type CompactJWT, type CWT, type CredentialProofFormat, StatusListType } from '@sphereon/ssi-types' import type { CheckStatusIndexArgs, CreateStatusListArgs, SignedStatusListData, StatusListResult, StatusOAuth, ToStatusListDetailsArgs, UpdateStatusListFromEncodedListArgs, UpdateStatusListIndexArgs, } from '../types' import { determineProofFormat, getAssertedValue, getAssertedValues } from '../utils' import type { IStatusList } from './IStatusList' import { StatusList } from '@sd-jwt/jwt-status-list' import type { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service' import type { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' import { createSignedJwt, decodeStatusListJWT } from './encoding/jwt' import { createSignedCbor, decodeStatusListCWT } from './encoding/cbor' type IRequiredContext = IAgentContext<ICredentialPlugin & IJwtService & IIdentifierResolution & IKeyManager> export const DEFAULT_BITS_PER_STATUS = 1 // 1 bit is sufficient for 0x00 - "VALID" 0x01 - "INVALID" saving space in the process export const DEFAULT_LIST_LENGTH = 250000 export const DEFAULT_PROOF_FORMAT = 'jwt' as CredentialProofFormat export class OAuthStatusListImplementation implements IStatusList { async createNewStatusList(args: CreateStatusListArgs, context: IRequiredContext): Promise<StatusListResult> { if (!args.oauthStatusList) { throw new Error('OAuthStatusList options are required for type OAuthStatusList') } const proofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT const { issuer, id, oauthStatusList, keyRef } = args const { bitsPerStatus, expiresAt } = oauthStatusList const length = args.length ?? DEFAULT_LIST_LENGTH const issuerString = typeof issuer === 'string' ? issuer : issuer.id const correlationId = getAssertedValue('correlationId', args.correlationId) const statusList = new StatusList(new Array(length).fill(0), bitsPerStatus ?? DEFAULT_BITS_PER_STATUS) const encodedList = statusList.compressStatusList() const { statusListCredential } = await this.createSignedStatusList(proofFormat, context, statusList, issuerString, id, expiresAt, keyRef) return { encodedList, statusListCredential, oauthStatusList: { bitsPerStatus }, length, type: StatusListType.OAuthStatusList, proofFormat, id, correlationId, issuer, statuslistContentType: this.buildContentType(proofFormat), } } async updateStatusListIndex(args: UpdateStatusListIndexArgs, context: IRequiredContext): Promise<StatusListResult> { const { statusListCredential, value, expiresAt, keyRef } = args if (typeof statusListCredential !== 'string') { return Promise.reject('statusListCredential in neither JWT nor CWT') } const proofFormat = determineProofFormat(statusListCredential) const decoded = proofFormat === 'jwt' ? decodeStatusListJWT(statusListCredential) : decodeStatusListCWT(statusListCredential) const { statusList, issuer, id } = decoded const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) if (index < 0 || index >= statusList.statusList.length) { throw new Error('Status list index out of bounds') } statusList.setStatus(index, value) const { statusListCredential: signedCredential, encodedList } = await this.createSignedStatusList( proofFormat, context, statusList, issuer, id, expiresAt, keyRef, ) return { statusListCredential: signedCredential, encodedList, oauthStatusList: { bitsPerStatus: statusList.getBitsPerStatus(), }, length: statusList.statusList.length, type: StatusListType.OAuthStatusList, proofFormat, id, issuer, statuslistContentType: this.buildContentType(proofFormat), } } // FIXME: This still assumes only two values (boolean), whilst this list supports 8 bits max async updateStatusListFromEncodedList(args: UpdateStatusListFromEncodedListArgs, context: IRequiredContext): Promise<StatusListResult> { if (!args.oauthStatusList) { throw new Error('OAuthStatusList options are required for type OAuthStatusList') } const { proofFormat, oauthStatusList, keyRef } = args const { bitsPerStatus, expiresAt } = oauthStatusList const { issuer, id } = getAssertedValues(args) const issuerString = typeof issuer === 'string' ? issuer : issuer.id const listToUpdate = StatusList.decompressStatusList(args.encodedList, bitsPerStatus ?? DEFAULT_BITS_PER_STATUS) const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) // FIXME: See above. listToUpdate.setStatus(index, args.value ? 1 : 0) const { statusListCredential, encodedList } = await this.createSignedStatusList( proofFormat ?? DEFAULT_PROOF_FORMAT, context, listToUpdate, issuerString, id, expiresAt, keyRef, ) return { encodedList, statusListCredential, oauthStatusList: { bitsPerStatus, expiresAt, }, length: listToUpdate.statusList.length, type: StatusListType.OAuthStatusList, proofFormat: proofFormat ?? DEFAULT_PROOF_FORMAT, id, issuer, statuslistContentType: this.buildContentType(proofFormat), } } private buildContentType(proofFormat: 'jwt' | 'lds' | 'EthereumEip712Signature2021' | 'cbor' | undefined) { return `application/statuslist+${proofFormat === 'cbor' ? 'cwt' : 'jwt'}` } async checkStatusIndex(args: CheckStatusIndexArgs): Promise<number | StatusOAuth> { const { statusListCredential, statusListIndex } = args if (typeof statusListCredential !== 'string') { return Promise.reject('statusListCredential in neither JWT nor CWT') } const proofFormat = determineProofFormat(statusListCredential) const { statusList } = proofFormat === 'jwt' ? decodeStatusListJWT(statusListCredential) : decodeStatusListCWT(statusListCredential) const index = typeof statusListIndex === 'number' ? statusListIndex : parseInt(statusListIndex) if (index < 0 || index >= statusList.statusList.length) { throw new Error('Status list index out of bounds') } return statusList.getStatus(index) } async toStatusListDetails(args: ToStatusListDetailsArgs): Promise<StatusListResult> { const { statusListPayload } = args as { statusListPayload: CompactJWT | CWT } const proofFormat = determineProofFormat(statusListPayload) const decoded = proofFormat === 'jwt' ? decodeStatusListJWT(statusListPayload) : decodeStatusListCWT(statusListPayload) const { statusList, issuer, id, exp } = decoded return { id, encodedList: statusList.compressStatusList(), issuer, type: StatusListType.OAuthStatusList, proofFormat, length: statusList.statusList.length, statusListCredential: statusListPayload, statuslistContentType: this.buildContentType(proofFormat), oauthStatusList: { bitsPerStatus: statusList.getBitsPerStatus(), ...(exp && { expiresAt: new Date(exp * 1000) }), }, ...(args.correlationId && { correlationId: args.correlationId }), ...(args.driverType && { driverType: args.driverType }), } } private async createSignedStatusList( proofFormat: 'jwt' | 'lds' | 'EthereumEip712Signature2021' | 'cbor', context: IAgentContext<ICredentialPlugin & IJwtService & IIdentifierResolution & IKeyManager>, statusList: StatusList, issuerString: string, id: string, expiresAt?: Date, keyRef?: string, ): Promise<SignedStatusListData> { switch (proofFormat) { case 'jwt': { return await createSignedJwt(context, statusList, issuerString, id, expiresAt, keyRef) } case 'cbor': { return await createSignedCbor(context, statusList, issuerString, id, expiresAt, keyRef) } default: throw new Error(`Invalid proof format '${proofFormat}' for OAuthStatusList`) } } }