@sphereon/ssi-sdk.vc-status-list
Version:
Sphereon SSI-SDK plugin for Status List management, like StatusList2021.
250 lines (223 loc) • 8.88 kB
text/typescript
import type { IAgentContext, ICredentialPlugin, ProofFormat as VeramoProofFormat } from '@veramo/core'
import type { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution'
import {
CredentialMapper,
DocumentFormat,
type IIssuer,
type CredentialProofFormat,
type StatusListCredential,
StatusListType,
} from '@sphereon/ssi-types'
import { StatusList } from '@sphereon/vc-status-list'
import type { IStatusList } from './IStatusList'
import type {
CheckStatusIndexArgs,
CreateStatusListArgs,
StatusListResult,
ToStatusListDetailsArgs,
UpdateStatusListFromEncodedListArgs,
UpdateStatusListIndexArgs,
} from '../types'
import { Status2021 } from '../types'
import { assertValidProofType, getAssertedProperty, getAssertedValue, getAssertedValues } from '../utils'
export const DEFAULT_LIST_LENGTH = 250000
export const DEFAULT_PROOF_FORMAT = 'lds' as VeramoProofFormat
export class StatusList2021Implementation implements IStatusList {
async createNewStatusList(
args: CreateStatusListArgs,
context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
): Promise<StatusListResult> {
const length = args?.length ?? DEFAULT_LIST_LENGTH
const proofFormat: CredentialProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT
assertValidProofType(StatusListType.StatusList2021, proofFormat)
const veramoProofFormat: VeramoProofFormat = proofFormat as VeramoProofFormat
const { issuer, id } = args
const correlationId = getAssertedValue('correlationId', args.correlationId)
const list = new StatusList({ length })
const encodedList = await list.encode()
const statusPurpose = 'revocation'
const statusListCredential = await this.createVerifiableCredential(
{
...args,
encodedList,
proofFormat: veramoProofFormat,
},
context,
)
return {
encodedList,
statusListCredential: statusListCredential,
statusList2021: {
statusPurpose,
indexingDirection: 'rightToLeft',
},
length,
type: StatusListType.StatusList2021,
proofFormat,
id,
correlationId,
issuer,
statuslistContentType: this.buildContentType(proofFormat),
}
}
async updateStatusListIndex(
args: UpdateStatusListIndexArgs,
context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
): Promise<StatusListResult> {
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 = await StatusList.decode({ encodedList: origEncodedList })
statusList.setStatus(index, args.value != 0)
const encodedList = await statusList.encode()
const proofFormat = CredentialMapper.detectDocumentType(credential) === DocumentFormat.JWT ? 'jwt' : 'lds'
const updatedCredential = await this.createVerifiableCredential(
{
...args,
id,
issuer,
encodedList,
proofFormat: proofFormat,
},
context,
)
return {
statusListCredential: updatedCredential,
encodedList,
statusList2021: {
...('statusPurpose' in credentialSubject ? { statusPurpose: credentialSubject.statusPurpose } : {}),
indexingDirection: 'rightToLeft',
},
length: statusList.length - 1,
type: StatusListType.StatusList2021,
proofFormat: proofFormat,
id,
issuer,
statuslistContentType: this.buildContentType(proofFormat),
}
}
async updateStatusListFromEncodedList(
args: UpdateStatusListFromEncodedListArgs,
context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
): Promise<StatusListResult> {
if (!args.statusList2021) {
throw new Error('statusList2021 options required for type StatusList2021')
}
const proofFormat: CredentialProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT
assertValidProofType(StatusListType.StatusList2021, proofFormat)
const veramoProofFormat: VeramoProofFormat = proofFormat as VeramoProofFormat
const { issuer, id } = getAssertedValues(args)
const statusList = await StatusList.decode({ encodedList: args.encodedList })
const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex)
statusList.setStatus(index, args.value)
const newEncodedList = await statusList.encode()
const credential = await this.createVerifiableCredential(
{
id,
issuer,
encodedList: newEncodedList,
proofFormat: veramoProofFormat,
keyRef: args.keyRef,
},
context,
)
return {
type: StatusListType.StatusList2021,
statusListCredential: credential,
encodedList: newEncodedList,
statusList2021: {
statusPurpose: args.statusList2021.statusPurpose,
indexingDirection: 'rightToLeft',
},
length: statusList.length,
proofFormat: args.proofFormat ?? 'lds',
id: id,
issuer: issuer,
statuslistContentType: this.buildContentType(proofFormat),
}
}
async checkStatusIndex(args: CheckStatusIndexArgs): Promise<number | Status2021> {
const uniform = CredentialMapper.toUniformCredential(args.statusListCredential)
const { credentialSubject } = uniform
const encodedList = getAssertedProperty('encodedList', credentialSubject)
const statusList = await StatusList.decode({ encodedList })
const status = statusList.getStatus(typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex))
return status ? Status2021.Invalid : Status2021.Valid
}
async toStatusListDetails(args: ToStatusListDetailsArgs): Promise<StatusListResult> {
const { statusListPayload } = args
const uniform = CredentialMapper.toUniformCredential(statusListPayload)
const { issuer, credentialSubject } = uniform
const id = getAssertedValue('id', uniform.id)
const encodedList = getAssertedProperty('encodedList', credentialSubject)
const proofFormat: CredentialProofFormat = CredentialMapper.detectDocumentType(statusListPayload) === DocumentFormat.JWT ? 'jwt' : 'lds'
const statusPurpose = getAssertedProperty('statusPurpose', credentialSubject)
const list = await StatusList.decode({ encodedList })
return {
id,
encodedList,
issuer,
type: StatusListType.StatusList2021,
proofFormat,
length: list.length,
statusListCredential: statusListPayload,
statuslistContentType: this.buildContentType(proofFormat),
statusList2021: {
indexingDirection: 'rightToLeft',
statusPurpose,
},
...(args.correlationId && { correlationId: args.correlationId }),
...(args.driverType && { driverType: args.driverType }),
}
}
private async createVerifiableCredential(
args: {
id: string
issuer: string | IIssuer
encodedList: string
proofFormat: VeramoProofFormat
keyRef?: string
},
context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
): Promise<StatusListCredential> {
const identifier = await context.agent.identifierManagedGet({
identifier: typeof args.issuer === 'string' ? args.issuer : args.issuer.id,
vmRelationship: 'assertionMethod',
offlineWhenNoDIDRegistered: true,
})
const credential = {
'@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'],
id: args.id,
issuer: args.issuer,
type: ['VerifiableCredential', 'StatusList2021Credential'],
credentialSubject: {
id: args.id,
type: 'StatusList2021',
statusPurpose: 'revocation',
encodedList: args.encodedList,
},
}
const verifiableCredential = await context.agent.createVerifiableCredential({
credential,
keyRef: args.keyRef ?? identifier.kmsKeyRef,
proofFormat: args.proofFormat,
fetchRemoteContexts: true,
})
return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as StatusListCredential).original as StatusListCredential
}
private buildContentType(proofFormat: 'jwt' | 'lds' | 'EthereumEip712Signature2021' | 'cbor' | undefined) {
switch (proofFormat) {
case 'jwt':
return `application/statuslist+jwt`
case 'cbor':
return `application/statuslist+cwt`
case 'lds':
return 'application/statuslist+ld+json'
default:
throw Error(`Unsupported content type '${proofFormat}' for status lists`)
}
}
}