From 64fd2a544d3c9f621f3e6b80fa6d39c51e88892e Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 10 Jan 2025 14:31:26 +0100 Subject: [PATCH 01/24] chore: prepare interface for OAuth2StatusList args --- packages/ssi-types/src/types/w3c-vc.ts | 3 +- .../src/drivers.ts | 19 +----- packages/vc-status-list/package.json | 1 + packages/vc-status-list/src/types/index.ts | 63 +++++++++++++------ pnpm-lock.yaml | 20 +++++- 5 files changed, 69 insertions(+), 37 deletions(-) diff --git a/packages/ssi-types/src/types/w3c-vc.ts b/packages/ssi-types/src/types/w3c-vc.ts index 7214810c9..33b90b5f6 100644 --- a/packages/ssi-types/src/types/w3c-vc.ts +++ b/packages/ssi-types/src/types/w3c-vc.ts @@ -225,7 +225,7 @@ export interface IVerifyResult { verified: boolean error?: IError log: [{ id: string; valid: boolean }] - }, + } ] statusResult?: IVerifyStatusResult @@ -278,6 +278,7 @@ export interface IErrorDetails { export enum StatusListType { StatusList2021 = 'StatusList2021', + OAuth2StatusList = 'OAuth2StatusList', } export type StatusPurpose2021 = 'revocation' | 'suspension' | string diff --git a/packages/vc-status-list-issuer-drivers/src/drivers.ts b/packages/vc-status-list-issuer-drivers/src/drivers.ts index b5b30df52..01b8501ba 100644 --- a/packages/vc-status-list-issuer-drivers/src/drivers.ts +++ b/packages/vc-status-list-issuer-drivers/src/drivers.ts @@ -131,28 +131,15 @@ export class AgentDataSourceStatusListDriver implements IStatusListDriver { } const credentialIdMode = args.credentialIdMode ?? StatusListCredentialIdMode.ISSUANCE const details = await statusListCredentialToDetails({ ...args, correlationId, driverType: this.getType() }) - const entity = await ( - await this.statusListStore.getStatusListRepo() - ).findOne({ - where: [ - { - id: details.id, - }, - { - correlationId, - }, - ], - }) - if (entity) { - throw Error(`Status list ${details.id}, correlationId ${args.correlationId} already exists`) - } - this._statusListLength = details.length + + // (StatusListStore does the duplicate entity check) await this.statusListStore.addStatusList({ ...details, credentialIdMode, correlationId, driverType: this.getType(), }) + this._statusListLength = details.length return details } diff --git a/packages/vc-status-list/package.json b/packages/vc-status-list/package.json index b420f1d63..adeeb778c 100644 --- a/packages/vc-status-list/package.json +++ b/packages/vc-status-list/package.json @@ -10,6 +10,7 @@ "build:clean": "tsc --build --clean && tsc --build" }, "dependencies": { + "@sd-jwt/jwt-status-list": "^0.9.1", "@sphereon/ssi-sdk-ext.did-utils": "0.27.0", "@sphereon/ssi-sdk-ext.identifier-resolution": "0.27.0", "@sphereon/ssi-types": "workspace:*", diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index 6b3906d1a..5930eb8a1 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -23,24 +23,31 @@ import { } from '@veramo/core' import { DataSource } from 'typeorm' -export interface CreateNewStatusListFuncArgs extends Omit { - correlationId: string - length?: number +export type StatusList2021Args = { + encodedList: string + indexingDirection: StatusListIndexingDirection + statusPurpose?: StatusPurpose2021 + // todo: validFrom and validUntil } -export interface UpdateStatusListFromEncodedListArgs extends StatusList2021ToVerifiableCredentialArgs { - statusListIndex: number | string - value: boolean +export type OAuth2StatusListArgs = { + status: 'active' | 'suspended' | 'revoked' + expiresAt?: string } -export interface UpdateStatusListFromStatusListCredentialArgs { - statusListCredential: OriginalVerifiableCredential +export type BaseCreateNewStatusListArgs = { + type: StatusListType + id: string + issuer: string | IIssuer + correlationId?: string + length?: number + proofFormat?: ProofFormat keyRef?: string - statusListIndex: number | string - value: boolean + statusList2021Args?: StatusList2021Args + oauth2StatusListArgs?: OAuth2StatusListArgs } -export interface StatusList2021ToVerifiableCredentialArgs { +export type UpdateStatusList2021Args = { issuer: string | IIssuer id: string type?: StatusListType @@ -48,8 +55,25 @@ export interface StatusList2021ToVerifiableCredentialArgs { encodedList: string proofFormat?: ProofFormat keyRef?: string +} - // todo: validFrom and validUntil +export type UpdateOAuth2StatusListArgs = { + status: 'active' | 'suspended' | 'revoked' + expiresAt?: string +} + +export interface UpdateStatusListFromEncodedListArgs { + statusListIndex: number | string + value: boolean + statusList2021Args?: UpdateStatusList2021Args + oauth2StatusListArgs?: UpdateOAuth2StatusListArgs +} + +export interface UpdateStatusListFromStatusListCredentialArgs { + statusListCredential: OriginalVerifiableCredential + keyRef?: string + statusListIndex: number | string + value: boolean } export interface StatusListDetails { @@ -117,13 +141,20 @@ export interface IStatusListPlugin extends IPluginMethodMap { slGetStatusList(args: GetStatusListArgs, context: IRequiredContext): Promise } +export type CreateNewStatusListFuncArgs = BaseCreateNewStatusListArgs + +export type CreateNewStatusListArgs = BaseCreateNewStatusListArgs & { + dataSource?: OrPromise + dbName?: string + isDefault?: boolean +} + export type IAddStatusToCredentialArgs = Omit & { credential: CredentialWithStatusSupport } export interface IIssueCredentialStatusOpts { dataSource?: DataSource - credentialId?: string // An id to use for the credential. Normally should be set as the crdential.id value statusListId?: string // Explicit status list to use. Determines the id from the credentialStatus object in the VC itself or uses the default otherwise statusListIndex?: number | string @@ -138,12 +169,6 @@ export type GetStatusListArgs = { dbName?: string } -export type CreateNewStatusListArgs = CreateNewStatusListFuncArgs & { - dataSource?: OrPromise - dbName?: string - isDefault?: boolean -} - export type CredentialWithStatusSupport = ICredential | CredentialPayload | IVerifiableCredential export type IRequiredPlugins = ICredentialPlugin & IIdentifierResolution diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6eb3118e2..cdf9bd7b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3221,6 +3221,9 @@ importers: packages/vc-status-list: dependencies: + '@sd-jwt/jwt-status-list': + specifier: ^0.9.1 + version: 0.9.1 '@sphereon/ssi-sdk-ext.did-utils': specifier: 0.27.0 version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) @@ -5915,6 +5918,10 @@ packages: resolution: {integrity: sha512-o/Mg/Zg21poFsPXuxtPD9sdXq2b/0L+rb9gxU2k1rp1aT+DWmqD0k8v0Ttr2tlMc8l1xXQNA8FLXbL1AdLRmbQ==} engines: {node: '>=18'} + '@sd-jwt/jwt-status-list@0.9.1': + resolution: {integrity: sha512-vdk9LKmhZWNbR3K4MFPwJ/ms04UKpDpxNt+CgTuvP++xgcdgmMj8WPiqxZznNssaPPe5XI+THgFn0w1O7gXTSg==} + engines: {node: '>=18'} + '@sd-jwt/present@0.7.2': resolution: {integrity: sha512-mQV85u2+mLLy2VZ9Wx2zpaB6yTDnbhCfWkP7eeCrzJQHBKAAHko8GrylEFmLKewFIcajS/r4lT/zHOsCkp5pZw==} engines: {node: '>=18'} @@ -5927,6 +5934,10 @@ packages: resolution: {integrity: sha512-1NRKowiW0ZiB9SGLApLPBH4Xk8gDQJ+nA9NdZ+uy6MmJKLEwjuJxO7yTvRIv/jX/0/Ebh339S7Kq4RD2AiFuRg==} engines: {node: '>=18'} + '@sd-jwt/types@0.9.1': + resolution: {integrity: sha512-/Ylzbpre9HQdBrRSRS3bivFdYtwkLM99Ye6gEZEMsJCb231Z1LKdzvmN35H/335ZV4cFYsT5IVM9PurEl6GoLQ==} + engines: {node: '>=18'} + '@sd-jwt/utils@0.7.2': resolution: {integrity: sha512-aMPY7uHRMgyI5PlDvEiIc+eBFGC1EM8OCQRiEjJ8HGN0pajWMYj0qwSw7pS90A49/DsYU1a5Zpvb7nyjgGH0Yg==} engines: {node: '>=18'} @@ -6035,7 +6046,6 @@ packages: '@sphereon/kmp-mdoc-core@0.2.0-SNAPSHOT.26': resolution: {integrity: sha512-QXJ6R8ENiZV2rPMbn06cw5JKwqUYN1kzVRbYfONqE1PEXx1noQ4md7uxr2zSczi0ubKkNcbyYDNtIMTZIhGzmQ==} - bundledDependencies: [] '@sphereon/lto-did-ts@0.1.8-unstable.0': resolution: {integrity: sha512-3jzwwuYX/VYuze+T9/yg4PcsJ5iNNwAfTp4WfS4aSfPFBErDAfKXqn6kOb0wFYGkhejr3Jz+rljPC2iKZiHiGA==} @@ -16835,6 +16845,12 @@ snapshots: base64url: 3.0.1 pako: 2.1.0 + '@sd-jwt/jwt-status-list@0.9.1': + dependencies: + '@sd-jwt/types': 0.9.1 + base64url: 3.0.1 + pako: 2.1.0 + '@sd-jwt/present@0.7.2': dependencies: '@sd-jwt/decode': 0.7.2 @@ -16849,6 +16865,8 @@ snapshots: '@sd-jwt/types@0.7.2': {} + '@sd-jwt/types@0.9.1': {} + '@sd-jwt/utils@0.7.2': dependencies: '@sd-jwt/types': 0.7.2 From f928c224c22f213c1b7616802e1b8e6613f81963 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 10 Jan 2025 15:10:52 +0100 Subject: [PATCH 02/24] chore: prepare interface for OAuth2StatusList args --- packages/vc-status-list/src/types/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index 5930eb8a1..7799c922a 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -50,11 +50,8 @@ export type BaseCreateNewStatusListArgs = { export type UpdateStatusList2021Args = { issuer: string | IIssuer id: string - type?: StatusListType statusPurpose: StatusPurpose2021 encodedList: string - proofFormat?: ProofFormat - keyRef?: string } export type UpdateOAuth2StatusListArgs = { @@ -63,8 +60,12 @@ export type UpdateOAuth2StatusListArgs = { } export interface UpdateStatusListFromEncodedListArgs { + type?: StatusListType statusListIndex: number | string value: boolean + proofFormat?: ProofFormat + keyRef?: string + correlationId?: string statusList2021Args?: UpdateStatusList2021Args oauth2StatusListArgs?: UpdateOAuth2StatusListArgs } From 0e3b6e40496a034ff32bb41aff3ca0f23c3b1d9d Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 10 Jan 2025 15:31:33 +0100 Subject: [PATCH 03/24] chore: prepare interface for OAuth2StatusList args --- packages/ssi-types/src/types/w3c-vc.ts | 1 - packages/vc-status-list/src/types/index.ts | 26 +++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/ssi-types/src/types/w3c-vc.ts b/packages/ssi-types/src/types/w3c-vc.ts index 33b90b5f6..b05ff6f50 100644 --- a/packages/ssi-types/src/types/w3c-vc.ts +++ b/packages/ssi-types/src/types/w3c-vc.ts @@ -278,7 +278,6 @@ export interface IErrorDetails { export enum StatusListType { StatusList2021 = 'StatusList2021', - OAuth2StatusList = 'OAuth2StatusList', } export type StatusPurpose2021 = 'revocation' | 'suspension' | string diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index 7799c922a..77cd47111 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -9,7 +9,6 @@ import { StatusListCredentialIdMode, StatusListDriverType, StatusListIndexingDirection, - StatusListType, StatusPurpose2021, } from '@sphereon/ssi-types' import { @@ -23,6 +22,13 @@ import { } from '@veramo/core' import { DataSource } from 'typeorm' +export enum StatusListType { + StatusList2021 = 'StatusList2021', + OAuth2StatusList = 'OAuth2StatusList', +} + +export type StatusPurposeOAuth2 = 'active' | 'suspended' | 'revoked' | string + export type StatusList2021Args = { encodedList: string indexingDirection: StatusListIndexingDirection @@ -31,7 +37,7 @@ export type StatusList2021Args = { } export type OAuth2StatusListArgs = { - status: 'active' | 'suspended' | 'revoked' + statusPurpose: StatusPurposeOAuth2 expiresAt?: string } @@ -55,7 +61,7 @@ export type UpdateStatusList2021Args = { } export type UpdateOAuth2StatusListArgs = { - status: 'active' | 'suspended' | 'revoked' + statusPurpose: StatusPurposeOAuth2 expiresAt?: string } @@ -66,8 +72,8 @@ export interface UpdateStatusListFromEncodedListArgs { proofFormat?: ProofFormat keyRef?: string correlationId?: string - statusList2021Args?: UpdateStatusList2021Args - oauth2StatusListArgs?: UpdateOAuth2StatusListArgs + statusList2021?: UpdateStatusList2021Args + oauth2StatusList?: UpdateOAuth2StatusListArgs } export interface UpdateStatusListFromStatusListCredentialArgs { @@ -104,6 +110,16 @@ export interface StatusList2021EntryCredentialStatus extends ICredentialStatus { statusListCredential: string } +export interface StatusList2021ToVerifiableCredentialArgs { + issuer: string | IIssuer + id: string + type?: StatusListType + proofFormat?: ProofFormat + keyRef?: string + encodedList: string + statusPurpose: StatusPurpose2021 +} + /** * The interface definition for a plugin that can add statuslist info to a credential * From 321e6e873036ace0cb2108c0c25357f4f49d86be Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 10 Jan 2025 15:34:33 +0100 Subject: [PATCH 04/24] chore: prepare interface for OAuth2StatusList args --- packages/vc-status-list/src/types/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index 77cd47111..57c374487 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -49,8 +49,8 @@ export type BaseCreateNewStatusListArgs = { length?: number proofFormat?: ProofFormat keyRef?: string - statusList2021Args?: StatusList2021Args - oauth2StatusListArgs?: OAuth2StatusListArgs + statusList2021?: StatusList2021Args + oauth2StatusList?: OAuth2StatusListArgs } export type UpdateStatusList2021Args = { From b11f791d7e783e856c762eb767ae949d431da62e Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 10 Jan 2025 21:37:41 +0100 Subject: [PATCH 05/24] chore: implementing OAuthStatusList --- .../src/api-functions.ts | 5 +- packages/vc-status-list/package.json | 1 + packages/vc-status-list/src/functions.ts | 179 ++++------------- .../vc-status-list/src/impl/IStatusList.ts | 47 +++++ .../src/impl/OAuthStatusList.ts | 189 ++++++++++++++++++ .../vc-status-list/src/impl/StatusList2021.ts | 186 +++++++++++++++++ .../src/impl/StatusListFactory.ts | 39 ++++ packages/vc-status-list/src/types/index.ts | 27 +-- packages/vc-status-list/src/utils.ts | 31 +++ pnpm-lock.yaml | 4 + 10 files changed, 551 insertions(+), 157 deletions(-) create mode 100644 packages/vc-status-list/src/impl/IStatusList.ts create mode 100644 packages/vc-status-list/src/impl/OAuthStatusList.ts create mode 100644 packages/vc-status-list/src/impl/StatusList2021.ts create mode 100644 packages/vc-status-list/src/impl/StatusListFactory.ts create mode 100644 packages/vc-status-list/src/utils.ts diff --git a/packages/vc-status-list-issuer-rest-api/src/api-functions.ts b/packages/vc-status-list-issuer-rest-api/src/api-functions.ts index 9e8c7c862..8402d612f 100644 --- a/packages/vc-status-list-issuer-rest-api/src/api-functions.ts +++ b/packages/vc-status-list-issuer-rest-api/src/api-functions.ts @@ -175,7 +175,10 @@ export function updateW3CStatusEndpoint(router: Router, context: IRequiredContex await driver.updateStatusListEntry({ ...statusListEntry, statusListIndex, statusList, credentialId, value: value ? '1' : '0' }) // todo: optimize. We are now creating a new VC for every item passed in. Probably wise to look at DB as well - details = await updateStatusIndexFromStatusListCredential({ statusListCredential, statusListIndex, value, keyRef: opts.keyRef }, context) + details = await updateStatusIndexFromStatusListCredential( + { statusListCredential: statusListCredential, statusListIndex, value, keyRef: opts.keyRef }, + context, + ) details = await driver.updateStatusList({ statusListCredential: details.statusListCredential }) } diff --git a/packages/vc-status-list/package.json b/packages/vc-status-list/package.json index adeeb778c..794a02b27 100644 --- a/packages/vc-status-list/package.json +++ b/packages/vc-status-list/package.json @@ -13,6 +13,7 @@ "@sd-jwt/jwt-status-list": "^0.9.1", "@sphereon/ssi-sdk-ext.did-utils": "0.27.0", "@sphereon/ssi-sdk-ext.identifier-resolution": "0.27.0", + "@sphereon/ssi-sdk-ext.jwt-service": "0.27.0", "@sphereon/ssi-types": "workspace:*", "@sphereon/vc-status-list": "7.0.0-next.0", "@veramo/core": "4.2.0", diff --git a/packages/vc-status-list/src/functions.ts b/packages/vc-status-list/src/functions.ts index 3a8fd594d..db5377dc6 100644 --- a/packages/vc-status-list/src/functions.ts +++ b/packages/vc-status-list/src/functions.ts @@ -1,16 +1,8 @@ import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { - CredentialMapper, - DocumentFormat, - IIssuer, - OriginalVerifiableCredential, - StatusListDriverType, - StatusListType, - StatusPurpose2021, -} from '@sphereon/ssi-types' +import { CredentialMapper, OriginalVerifiableCredential, StatusListDriverType, StatusListType, StatusPurpose2021 } from '@sphereon/ssi-types' -import { checkStatus, StatusList } from '@sphereon/vc-status-list' -import { CredentialStatus, DIDDocument, IAgentContext, ICredentialPlugin, ProofFormat } from '@veramo/core' +import { checkStatus } from '@sphereon/vc-status-list' +import { CredentialStatus, DIDDocument, IAgentContext, ICredentialPlugin } from '@veramo/core' import { CredentialJwtOrJSON, StatusMethod } from 'credential-status' import { CreateNewStatusListFuncArgs, @@ -20,6 +12,8 @@ import { UpdateStatusListFromEncodedListArgs, UpdateStatusListFromStatusListCredentialArgs, } from './types' +import { getAssertedStatusListType, getAssertedValue, getAssertedValues } from './utils' +import { getStatusListImplementation } from './impl/StatusListFactory' export async function fetchStatusListCredential(args: { statusListCredential: string }): Promise { const url = getAssertedValue('statusListCredential', args.statusListCredential) @@ -147,80 +141,35 @@ export async function checkStatusIndexFromStatusListCredential(args: { statusListIndex: string | number }): Promise { const requestedType = getAssertedStatusListType(args.type?.replace('Entry', '') as StatusListType) - const uniform = CredentialMapper.toUniformCredential(args.statusListCredential) - const { issuer, type, credentialSubject, id } = uniform - getAssertedValue('issuer', issuer) // We are only checking the value here - getAssertedValue('credentialSubject', credentialSubject) - if (args.statusPurpose && 'statusPurpose' in credentialSubject) { - if (args.statusPurpose !== credentialSubject.statusPurpose) { - throw Error( - `Status purpose in StatusList credential with id ${id} and value ${credentialSubject.statusPurpose} does not match supplied purpose: ${args.statusPurpose}`, - ) - } - } else if (args.id && args.id !== id) { - throw Error(`Status list id ${id} did not match required supplied id: ${args.id}`) - } - if (!type || !(type.includes(requestedType) || type.includes(requestedType + 'Credential'))) { - throw Error(`Credential type ${JSON.stringify(type)} does not contain requested type ${requestedType}`) - } - // @ts-ignore - const encodedList = getAssertedValue('encodedList', credentialSubject['encodedList']) - - const statusList = await StatusList.decode({ encodedList }) - const status = statusList.getStatus(typeof args.statusListIndex === 'number' ? args.statusListIndex : Number.parseInt(args.statusListIndex)) - return status + const implementation = getStatusListImplementation(requestedType) + return implementation.checkStatusIndex(args) } export async function createNewStatusList( args: CreateNewStatusListFuncArgs, context: IAgentContext, ): Promise { - const length = args?.length ?? 250000 - const proofFormat = args?.proofFormat ?? 'lds' - const { issuer, type, id } = getAssertedValues(args) - const correlationId = getAssertedValue('correlationId', args.correlationId) - - const list = new StatusList({ length }) - const encodedList = await list.encode() - const statusPurpose = args.statusPurpose ?? 'revocation' - const statusListCredential = await statusList2021ToVerifiableCredential( - { - ...args, - type, - proofFormat, - encodedList, - }, - context, - ) - - return { - encodedList, - statusListCredential, - length, - type, - proofFormat, - id, - correlationId, - issuer, - statusPurpose, - indexingDirection: 'rightToLeft', - } as StatusListResult + const { type } = getAssertedValues(args) + const implementation = getStatusListImplementation(type) + return implementation.createNewStatusList(args, context) } export async function updateStatusIndexFromStatusListCredential( args: UpdateStatusListFromStatusListCredentialArgs, context: IAgentContext, ): Promise { - return updateStatusListIndexFromEncodedList( - { - ...(await statusListCredentialToDetails(args)), - statusListIndex: args.statusListIndex, - value: args.value, - }, - context, - ) + const credential = getAssertedValue('statusListCredential', args.statusListCredential) + const uniform = CredentialMapper.toUniformCredential(credential) + const type = uniform.type.find((t) => t.includes('StatusList2021') || t.includes('OAuth2StatusList')) + if (!type) { + throw new Error('Invalid status list credential type') + } + const statusListType = type.replace('Credential', '') as StatusListType + const implementation = getStatusListImplementation(statusListType) + return implementation.updateStatusListIndex(args, context) } +// Keeping helper function for backward compatibility export async function statusListCredentialToDetails(args: { statusListCredential: OriginalVerifiableCredential correlationId?: string @@ -228,63 +177,29 @@ export async function statusListCredentialToDetails(args: { }): Promise { const credential = getAssertedValue('statusListCredential', args.statusListCredential) const uniform = CredentialMapper.toUniformCredential(credential) - const { issuer, type, credentialSubject } = uniform - if (!type.includes('StatusList2021Credential')) { - throw Error('StatusList2021Credential type should be present in the Verifiable Credential') - } - const id = getAssertedValue('id', uniform.id) - // @ts-ignore - const { encodedList, statusPurpose } = credentialSubject - const proofFormat: ProofFormat = CredentialMapper.detectDocumentType(credential) === DocumentFormat.JWT ? 'jwt' : 'lds' - return { - id, - encodedList, - issuer, - type: StatusListType.StatusList2021, - proofFormat, - indexingDirection: 'rightToLeft', - length: (await StatusList.decode({ encodedList })).length, - statusPurpose, - statusListCredential: credential, - ...(args.correlationId && { correlationId: args.correlationId }), - ...(args.driverType && { driverType: args.driverType }), + const type = uniform.type.find((t) => t.includes('StatusList2021') || t.includes('OAuth2StatusList')) + if (!type) { + throw new Error('Invalid status list credential type') } + const statusListType = type.replace('Credential', '') as StatusListType + const implementation = getStatusListImplementation(statusListType) + return implementation.updateStatusListIndex( + { + statusListCredential: args.statusListCredential, + statusListIndex: 0, + value: false, + }, + {} as IAgentContext, + ) } export async function updateStatusListIndexFromEncodedList( args: UpdateStatusListFromEncodedListArgs, context: IAgentContext, ): Promise { - const { issuer, type, id } = getAssertedValues(args) - const proofFormat = args?.proofFormat ?? 'lds' - const origEncodedList = getAssertedValue('encodedList', args.encodedList) - const index = getAssertedValue('index', typeof args.statusListIndex === 'number' ? args.statusListIndex : Number.parseInt(args.statusListIndex)) - const value = getAssertedValue('value', args.value) - const statusPurpose = getAssertedValue('statusPurpose', args.statusPurpose) - - const statusList = await StatusList.decode({ encodedList: origEncodedList }) - statusList.setStatus(index, value) - const encodedList = await statusList.encode() - const statusListCredential = await statusList2021ToVerifiableCredential( - { - ...args, - type, - proofFormat, - encodedList, - }, - context, - ) - return { - encodedList, - statusListCredential, - length: statusList.length - 1, - type, - proofFormat, - id, - issuer, - statusPurpose, - indexingDirection: 'rightToLeft', - } + const { type } = getAssertedValue('type', args) + const implementation = getStatusListImplementation(type!) + return implementation.updateStatusListFromEncodedList(args, context) } export async function statusList2021ToVerifiableCredential( @@ -322,25 +237,3 @@ export async function statusList2021ToVerifiableCredential( return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as OriginalVerifiableCredential).original } - -function getAssertedStatusListType(type?: StatusListType) { - const assertedType = type ?? StatusListType.StatusList2021 - if (assertedType !== StatusListType.StatusList2021) { - throw Error(`StatusList type ${assertedType} is not supported (yet)`) - } - return assertedType -} - -function getAssertedValue(name: string, value: T): NonNullable { - if (value === undefined || value === null) { - throw Error(`Missing required ${name} value`) - } - return value -} - -function getAssertedValues(args: { issuer: string | IIssuer; id: string; type?: StatusListType }) { - const type = getAssertedStatusListType(args?.type) - const id = getAssertedValue('id', args.id) - const issuer = getAssertedValue('issuer', args.issuer) - return { id, issuer, type } -} diff --git a/packages/vc-status-list/src/impl/IStatusList.ts b/packages/vc-status-list/src/impl/IStatusList.ts new file mode 100644 index 000000000..ead330444 --- /dev/null +++ b/packages/vc-status-list/src/impl/IStatusList.ts @@ -0,0 +1,47 @@ +import { IAgentContext, ICredentialPlugin, ProofFormat } from '@veramo/core' +import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' +import { IIssuer, OriginalVerifiableCredential } from '@sphereon/ssi-types' +import { StatusListDetails, StatusListResult, UpdateStatusListFromEncodedListArgs } from '../types' + +export interface IStatusList { + /** + * Creates a new status list of the specific type + */ + createNewStatusList( + args: { + issuer: string | IIssuer + id: string + proofFormat?: ProofFormat + keyRef?: string + correlationId?: string + length?: number + }, + context: IAgentContext, + ): Promise + + /** + * Updates a status at the given index in the status list + */ + updateStatusListIndex( + args: { + statusListCredential: OriginalVerifiableCredential + keyRef?: string + statusListIndex: number | string + value: boolean + }, + context: IAgentContext, + ): Promise + + /** + * Updates a status list using a base64 encoded list of statuses + */ + updateStatusListFromEncodedList( + args: UpdateStatusListFromEncodedListArgs, + context: IAgentContext, + ): Promise + + /** + * Checks the status at a given index in the status list + */ + checkStatusIndex(args: { statusListCredential: OriginalVerifiableCredential; statusListIndex: string | number }): Promise +} diff --git a/packages/vc-status-list/src/impl/OAuthStatusList.ts b/packages/vc-status-list/src/impl/OAuthStatusList.ts new file mode 100644 index 000000000..deb911bbc --- /dev/null +++ b/packages/vc-status-list/src/impl/OAuthStatusList.ts @@ -0,0 +1,189 @@ +import { IAgentContext, ICredentialPlugin, ProofFormat } from '@veramo/core' +import { CompactJWT, IIssuer } from '@sphereon/ssi-types' +import { StatusListDetails, StatusListResult, StatusListType, UpdateStatusListFromEncodedListArgs } from '../types' +import { decodeStatusListJWT, getAssertedValue, getAssertedValues } from '../utils' +import { IStatusList } from './IStatusList' +import { createHeaderAndPayload, StatusList, StatusListJWTHeaderParameters, StatusListJWTPayload } from '@sd-jwt/jwt-status-list' +import { JWTPayload } from 'did-jwt' +import { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service' +import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' + +export class OAuthStatusListImplementation implements IStatusList { + async createNewStatusList( + args: { + issuer: string | IIssuer + id: string + proofFormat?: ProofFormat + keyRef?: string + correlationId?: string + expiresAt?: string + length?: number + }, + context: IAgentContext, + ): Promise { + const proofFormat = args?.proofFormat ?? 'jwt' + if (proofFormat !== 'jwt') { + throw new Error(`Invalid proof format '${proofFormat}' for OAuthStatusList`) + } + + const { issuer, id } = args + + const issuerString = typeof args.issuer === 'string' ? args.issuer : args.issuer.id + const identifier = await this.resolveIdentifier(context, issuerString, args.keyRef) + + const correlationId = getAssertedValue('correlationId', args.correlationId) + const length = args.length ?? 100 // Default length if not specified + + // Initialize a status list with the specified length, all set to 0 (not revoked) + const initialStatuses = new Array(length).fill(0) + const statusList = new StatusList(initialStatuses, 1) // TODO bits per status config + const encodedList = statusList.compressStatusList() + const payload: JWTPayload = { + iss: issuerString, + sub: id, + iat: Math.floor(new Date().getTime() / 1000), + } + const header: StatusListJWTHeaderParameters = { + alg: 'EdDSA', + typ: 'statuslist+jwt', + } + const values = createHeaderAndPayload(statusList, payload, header) + const signedPayload = await context.agent.jwtCreateJwsCompactSignature({ + issuer: { ...identifier, noIssPayloadUpdate: false }, + protectedHeader: values.header, + payload: values.payload, + }) + + return { + encodedList, + statusListCredential: signedPayload.jwt, + length, + type: StatusListType.OAuthStatusList, + proofFormat, + id, + correlationId, + issuer, + statusPurpose: 'active', + indexingDirection: 'rightToLeft', + } + } + + private async resolveIdentifier(context: IAgentContext, issuer: string, keyRef?: string) { + const identifier = keyRef + ? await context.agent.identifierManagedGetByKid({ + identifier: keyRef, + }) + : await context.agent.identifierManagedGet({ + identifier: issuer, + vmRelationship: 'assertionMethod', + offlineWhenNoDIDRegistered: true, + }) + return identifier + } + + async updateStatusListIndex( + args: { + statusListCredential: CompactJWT + keyRef?: string + statusListIndex: number | string + value: boolean + }, + context: IAgentContext, + ): Promise { + const { statusListCredential, value } = args + const sourcePayload = decodeStatusListJWT(statusListCredential) + if (!('iss' in sourcePayload)) { + throw new Error('issuer (iss) is missing in the status list JWT') + } + if (!('sub' in sourcePayload)) { + throw new Error('List id (sub) is missing in the status list JWT') + } + const { iss: issuer, sub: id } = sourcePayload as StatusListJWTPayload & { iss: string; sub: string } + + const statusListContainer = sourcePayload.status_list + const statusList = StatusList.decompressStatusList(statusListContainer.lst, statusListContainer.bits) + + 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 ? 1 : 0) + const updatedEncodedList = await statusList.compressStatusList() + const identifier = await this.resolveIdentifier(context, issuer, args.keyRef) + + const payload: JWTPayload = { + iss: issuer, + sub: id, + iat: Math.floor(new Date().getTime() / 1000), + } + const header: StatusListJWTHeaderParameters = { + alg: 'EdDSA', + typ: 'statuslist+jwt', + } + const values = createHeaderAndPayload(statusList, payload, header) + const signedPayload = await context.agent.jwtCreateJwsCompactSignature({ + issuer: { ...identifier, noIssPayloadUpdate: false }, + protectedHeader: values.header, + payload: values.payload, + }) + + // Return details without credential-specific fields + return { + encodedList: updatedEncodedList, + statusListCredential: signedPayload.jwt, + length: statusList.statusList.length, + type: StatusListType.OAuthStatusList, + proofFormat: 'jwt', + id, + issuer, + statusPurpose: 'active', + indexingDirection: 'rightToLeft', + } + } + + async updateStatusListFromEncodedList( + args: UpdateStatusListFromEncodedListArgs, + context: IAgentContext, + ): Promise { + if (!args.oauthStatusList) { + throw new Error('OAuthStatusList options are required for type OAuthStatusList') + } + + const { statusPurpose } = args.oauthStatusList + const { issuer, id } = getAssertedValues(args) + + const decodedList = StatusList.decompressStatusList(args.encodedList, 1) + const updatedEncodedList = await decodedList.compressStatusList() + + return { + encodedList: updatedEncodedList, + statusListCredential: { + encodedList: updatedEncodedList, + issuer, + id, + statusPurpose, + }, + length: decodedList.statusList.length, + type: StatusListType.OAuthStatusList, + proofFormat: args.proofFormat ?? 'jwt', + id, + issuer, + statusPurpose, + indexingDirection: 'rightToLeft', + } + } + + async checkStatusIndex(args: { statusListCredential: CompactJWT; statusListIndex: string | number }): Promise { + const { statusListCredential, statusListIndex } = args + + const statusList = StatusList.decompressStatusList(statusListCredential, 1) + 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) === 1 + } +} diff --git a/packages/vc-status-list/src/impl/StatusList2021.ts b/packages/vc-status-list/src/impl/StatusList2021.ts new file mode 100644 index 000000000..9692358aa --- /dev/null +++ b/packages/vc-status-list/src/impl/StatusList2021.ts @@ -0,0 +1,186 @@ +import { IAgentContext, ICredentialPlugin, ProofFormat } from '@veramo/core' +import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' +import { CredentialMapper, DocumentFormat, IIssuer, OriginalVerifiableCredential, StatusListType } from '@sphereon/ssi-types' +import { StatusList } from '@sphereon/vc-status-list' +import { IStatusList } from './IStatusList' +import { StatusListDetails, StatusListResult, UpdateStatusListFromEncodedListArgs } from '../types' +import { getAssertedValue, getAssertedValues } from '../utils' + +export class StatusList2021Implementation implements IStatusList { + async createNewStatusList( + args: { + issuer: string | IIssuer + id: string + proofFormat?: ProofFormat + keyRef?: string + correlationId?: string + length?: number + }, + context: IAgentContext, + ): Promise { + const length = args?.length ?? 250000 + const proofFormat: ProofFormat = args?.proofFormat ?? 'lds' + 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, + }, + context, + ) + + return { + encodedList, + statusListCredential: statusListCredential, + length, + type: StatusListType.StatusList2021, + proofFormat, + id, + correlationId, + issuer, + statusPurpose, + indexingDirection: 'rightToLeft', + } + } + + async updateStatusListIndex( + args: { + statusListCredential: OriginalVerifiableCredential + keyRef?: string + statusListIndex: number | string + value: boolean + }, + context: IAgentContext, + ): Promise { + const credential = args.statusListCredential + const uniform = CredentialMapper.toUniformCredential(credential) + const { issuer, credentialSubject } = uniform + const id = getAssertedValue('id', uniform.id) + // @ts-ignore + const origEncodedList = credentialSubject.encodedList + + const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) + const statusList = await StatusList.decode({ encodedList: origEncodedList }) + statusList.setStatus(index, args.value) + const encodedList = await statusList.encode() + + const updatedCredential = await this.createVerifiableCredential( + { + ...args, + id, + issuer, + encodedList, + proofFormat: CredentialMapper.detectDocumentType(credential) === DocumentFormat.JWT ? 'jwt' : 'lds', + }, + context, + ) + + return { + encodedList, + statusListCredential: updatedCredential, + length: statusList.length - 1, + type: StatusListType.StatusList2021, + proofFormat: CredentialMapper.detectDocumentType(credential) === DocumentFormat.JWT ? 'jwt' : 'lds', + id, + issuer, + // @ts-ignore + statusPurpose: credentialSubject.statusPurpose, + indexingDirection: 'rightToLeft', + } + } + + async updateStatusListFromEncodedList( + args: UpdateStatusListFromEncodedListArgs, + context: IAgentContext, + ): Promise { + if (!args.statusList2021) { + throw new Error('statusList2021 options required for type StatusList2021') + } + 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: args.proofFormat, + keyRef: args.keyRef, + }, + context, + ) + + return { + encodedList: newEncodedList, + statusListCredential: credential, + length: statusList.length, + type: StatusListType.StatusList2021, + proofFormat: args.proofFormat ?? 'lds', + id: id, + issuer: issuer, + statusPurpose: 'revocation', + indexingDirection: 'rightToLeft', + } + } + + async checkStatusIndex(args: { statusListCredential: OriginalVerifiableCredential; statusListIndex: string | number }): Promise { + const uniform = CredentialMapper.toUniformCredential(args.statusListCredential) + const { credentialSubject } = uniform + // @ts-ignore + const encodedList = getAssertedValue('encodedList', credentialSubject.encodedList) + + const statusList = await StatusList.decode({ encodedList }) + const status = statusList.getStatus(typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex)) + return status + } + + private async createVerifiableCredential( + args: { + id: string + issuer: string | IIssuer + encodedList: string + proofFormat?: ProofFormat + keyRef?: string + }, + context: IAgentContext, + ): Promise { + 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 ?? 'lds', + fetchRemoteContexts: true, + }) + + return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as OriginalVerifiableCredential).original + } +} diff --git a/packages/vc-status-list/src/impl/StatusListFactory.ts b/packages/vc-status-list/src/impl/StatusListFactory.ts new file mode 100644 index 000000000..6651367d1 --- /dev/null +++ b/packages/vc-status-list/src/impl/StatusListFactory.ts @@ -0,0 +1,39 @@ +import { IStatusList } from './IStatusList' +import { StatusList2021Implementation } from './StatusList2021' +import { OAuth2StatusListImplementation } from './OAuthStatusList' +import { StatusListType } from '../types' + +export class StatusListFactory { + private static instance: StatusListFactory + private implementations: Map + + private constructor() { + this.implementations = new Map() + this.implementations.set(StatusListType.StatusList2021, new StatusList2021Implementation()) + this.implementations.set(StatusListType.OAuthStatusList, new OAuth2StatusListImplementation()) + } + + public static getInstance(): StatusListFactory { + if (!StatusListFactory.instance) { + StatusListFactory.instance = new StatusListFactory() + } + return StatusListFactory.instance + } + + public getImplementation(type: StatusListType): IStatusList { + const implementation = this.implementations.get(type) + if (!implementation) { + throw new Error(`No implementation found for status list type: ${type}`) + } + return implementation + } + + // Optional: Method to register custom implementations if needed + public registerImplementation(type: StatusListType, implementation: IStatusList): void { + this.implementations.set(type, implementation) + } +} + +export function getStatusListImplementation(type: StatusListType): IStatusList { + return StatusListFactory.getInstance().getImplementation(type) +} diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index 57c374487..2100e8384 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -1,5 +1,6 @@ import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' import { + CompactJWT, ICredential, ICredentialStatus, IIssuer, @@ -24,10 +25,10 @@ import { DataSource } from 'typeorm' export enum StatusListType { StatusList2021 = 'StatusList2021', - OAuth2StatusList = 'OAuth2StatusList', + OAuthStatusList = 'OAuthStatusList', } -export type StatusPurposeOAuth2 = 'active' | 'suspended' | 'revoked' | string +export type StatusPurposeOAuth = 'active' | 'suspended' | 'revoked' | string export type StatusList2021Args = { encodedList: string @@ -36,8 +37,8 @@ export type StatusList2021Args = { // todo: validFrom and validUntil } -export type OAuth2StatusListArgs = { - statusPurpose: StatusPurposeOAuth2 +export type OAuthStatusListArgs = { + statusPurpose: StatusPurposeOAuth expiresAt?: string } @@ -50,18 +51,15 @@ export type BaseCreateNewStatusListArgs = { proofFormat?: ProofFormat keyRef?: string statusList2021?: StatusList2021Args - oauth2StatusList?: OAuth2StatusListArgs + oauthStatusList?: OAuthStatusListArgs } export type UpdateStatusList2021Args = { - issuer: string | IIssuer - id: string statusPurpose: StatusPurpose2021 - encodedList: string } -export type UpdateOAuth2StatusListArgs = { - statusPurpose: StatusPurposeOAuth2 +export type UpdateOAuthStatusListArgs = { + statusPurpose: StatusPurposeOAuth expiresAt?: string } @@ -72,12 +70,15 @@ export interface UpdateStatusListFromEncodedListArgs { proofFormat?: ProofFormat keyRef?: string correlationId?: string + encodedList: string + issuer: string | IIssuer + id: string statusList2021?: UpdateStatusList2021Args - oauth2StatusList?: UpdateOAuth2StatusListArgs + oauthStatusList?: UpdateOAuthStatusListArgs } export interface UpdateStatusListFromStatusListCredentialArgs { - statusListCredential: OriginalVerifiableCredential + statusListCredential: OriginalVerifiableCredential | CompactJWT keyRef?: string statusListIndex: number | string value: boolean @@ -88,7 +89,7 @@ export interface StatusListDetails { length: number type: StatusListType proofFormat: ProofFormat - statusPurpose: StatusPurpose2021 + statusPurpose?: StatusPurpose2021 | StatusPurposeOAuth id: string issuer: string | IIssuer indexingDirection: StatusListIndexingDirection diff --git a/packages/vc-status-list/src/utils.ts b/packages/vc-status-list/src/utils.ts new file mode 100644 index 000000000..2682e8c6e --- /dev/null +++ b/packages/vc-status-list/src/utils.ts @@ -0,0 +1,31 @@ +import { CompactJWT, IIssuer, StatusListType as StatusListTypeW3C } from '@sphereon/ssi-types' +import { StatusListType } from './types' +import { StatusListJWTPayload } from '@sd-jwt/jwt-status-list' +import { decodeBase64url } from 'did-jwt/lib/util' + +export function decodeStatusListJWT(jwt: CompactJWT): StatusListJWTPayload { + const parts = jwt.split('.') + return JSON.parse(decodeBase64url(parts[1])) +} + +export function getAssertedStatusListType(type?: StatusListType) { + const assertedType = type ?? StatusListType.StatusList2021 + if (assertedType !== StatusListType.StatusList2021) { + throw Error(`StatusList type ${assertedType} is not supported (yet)`) + } + return assertedType +} + +export function getAssertedValue(name: string, value: T): NonNullable { + if (value === undefined || value === null) { + throw Error(`Missing required ${name} value`) + } + return value +} + +export function getAssertedValues(args: { issuer: string | IIssuer; id: string; type?: StatusListTypeW3C | StatusListType }) { + const type = getAssertedStatusListType(args?.type) + const id = getAssertedValue('id', args.id) + const issuer = getAssertedValue('issuer', args.issuer) + return { id, issuer, type } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdf9bd7b0..dd4c13489 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3230,6 +3230,9 @@ importers: '@sphereon/ssi-sdk-ext.identifier-resolution': specifier: 0.27.0 version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) + '@sphereon/ssi-sdk-ext.jwt-service': + specifier: 0.27.0 + version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) '@sphereon/ssi-types': specifier: workspace:* version: link:../ssi-types @@ -6046,6 +6049,7 @@ packages: '@sphereon/kmp-mdoc-core@0.2.0-SNAPSHOT.26': resolution: {integrity: sha512-QXJ6R8ENiZV2rPMbn06cw5JKwqUYN1kzVRbYfONqE1PEXx1noQ4md7uxr2zSczi0ubKkNcbyYDNtIMTZIhGzmQ==} + bundledDependencies: [] '@sphereon/lto-did-ts@0.1.8-unstable.0': resolution: {integrity: sha512-3jzwwuYX/VYuze+T9/yg4PcsJ5iNNwAfTp4WfS4aSfPFBErDAfKXqn6kOb0wFYGkhejr3Jz+rljPC2iKZiHiGA==} From 7e70318adfa509fbf1f08e3b243be43c738c1606 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Mon, 13 Jan 2025 12:31:39 +0100 Subject: [PATCH 06/24] chore: implementing OAuthStatusList --- .../src/impl/OAuthStatusList.ts | 150 +++++++++--------- .../src/impl/StatusListFactory.ts | 4 +- packages/vc-status-list/src/types/index.ts | 11 +- 3 files changed, 80 insertions(+), 85 deletions(-) diff --git a/packages/vc-status-list/src/impl/OAuthStatusList.ts b/packages/vc-status-list/src/impl/OAuthStatusList.ts index deb911bbc..382b25389 100644 --- a/packages/vc-status-list/src/impl/OAuthStatusList.ts +++ b/packages/vc-status-list/src/impl/OAuthStatusList.ts @@ -7,6 +7,17 @@ import { createHeaderAndPayload, StatusList, StatusListJWTHeaderParameters, Stat import { JWTPayload } from 'did-jwt' import { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service' import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' +import { BitsPerStatus } from '@sd-jwt/jwt-status-list/dist' + +type IRequiredContext = IAgentContext + +export const BITS_PER_STATUS_DEFAULT = 1 +export const DEFAULT_LIST_LENGTH = 65536 +export const DEFAULT_PROOF_FORMAT = 'jwt' as const +export const STATUS_LIST_JWT_HEADER: StatusListJWTHeaderParameters = { + alg: 'EdDSA', + typ: 'statuslist+jwt', +} export class OAuthStatusListImplementation implements IStatusList { async createNewStatusList( @@ -18,45 +29,29 @@ export class OAuthStatusListImplementation implements IStatusList { correlationId?: string expiresAt?: string length?: number + bitsPerStatus?: BitsPerStatus }, - context: IAgentContext, + context: IRequiredContext, ): Promise { - const proofFormat = args?.proofFormat ?? 'jwt' - if (proofFormat !== 'jwt') { + const proofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT + if (proofFormat !== DEFAULT_PROOF_FORMAT) { throw new Error(`Invalid proof format '${proofFormat}' for OAuthStatusList`) } const { issuer, id } = args - - const issuerString = typeof args.issuer === 'string' ? args.issuer : args.issuer.id - const identifier = await this.resolveIdentifier(context, issuerString, args.keyRef) - + const length = args.length ?? DEFAULT_LIST_LENGTH + const bitsPerStatus = args.bitsPerStatus ?? BITS_PER_STATUS_DEFAULT + const issuerString = typeof issuer === 'string' ? issuer : issuer.id const correlationId = getAssertedValue('correlationId', args.correlationId) - const length = args.length ?? 100 // Default length if not specified - // Initialize a status list with the specified length, all set to 0 (not revoked) const initialStatuses = new Array(length).fill(0) - const statusList = new StatusList(initialStatuses, 1) // TODO bits per status config + const statusList = new StatusList(initialStatuses, bitsPerStatus) const encodedList = statusList.compressStatusList() - const payload: JWTPayload = { - iss: issuerString, - sub: id, - iat: Math.floor(new Date().getTime() / 1000), - } - const header: StatusListJWTHeaderParameters = { - alg: 'EdDSA', - typ: 'statuslist+jwt', - } - const values = createHeaderAndPayload(statusList, payload, header) - const signedPayload = await context.agent.jwtCreateJwsCompactSignature({ - issuer: { ...identifier, noIssPayloadUpdate: false }, - protectedHeader: values.header, - payload: values.payload, - }) + const { jwt } = await this.createSignedPayload(context, statusList, issuerString, id, args.keyRef) return { encodedList, - statusListCredential: signedPayload.jwt, + statusListCredential: jwt, length, type: StatusListType.OAuthStatusList, proofFormat, @@ -68,19 +63,6 @@ export class OAuthStatusListImplementation implements IStatusList { } } - private async resolveIdentifier(context: IAgentContext, issuer: string, keyRef?: string) { - const identifier = keyRef - ? await context.agent.identifierManagedGetByKid({ - identifier: keyRef, - }) - : await context.agent.identifierManagedGet({ - identifier: issuer, - vmRelationship: 'assertionMethod', - offlineWhenNoDIDRegistered: true, - }) - return identifier - } - async updateStatusListIndex( args: { statusListCredential: CompactJWT @@ -88,7 +70,7 @@ export class OAuthStatusListImplementation implements IStatusList { statusListIndex: number | string value: boolean }, - context: IAgentContext, + context: IRequiredContext, ): Promise { const { statusListCredential, value } = args const sourcePayload = decodeStatusListJWT(statusListCredential) @@ -109,32 +91,14 @@ export class OAuthStatusListImplementation implements IStatusList { } statusList.setStatus(index, value ? 1 : 0) - const updatedEncodedList = await statusList.compressStatusList() - const identifier = await this.resolveIdentifier(context, issuer, args.keyRef) + const { jwt, encodedList } = await this.createSignedPayload(context, statusList, issuer, id, args.keyRef) - const payload: JWTPayload = { - iss: issuer, - sub: id, - iat: Math.floor(new Date().getTime() / 1000), - } - const header: StatusListJWTHeaderParameters = { - alg: 'EdDSA', - typ: 'statuslist+jwt', - } - const values = createHeaderAndPayload(statusList, payload, header) - const signedPayload = await context.agent.jwtCreateJwsCompactSignature({ - issuer: { ...identifier, noIssPayloadUpdate: false }, - protectedHeader: values.header, - payload: values.payload, - }) - - // Return details without credential-specific fields return { - encodedList: updatedEncodedList, - statusListCredential: signedPayload.jwt, + encodedList, + statusListCredential: jwt, length: statusList.statusList.length, type: StatusListType.OAuthStatusList, - proofFormat: 'jwt', + proofFormat: DEFAULT_PROOF_FORMAT, id, issuer, statusPurpose: 'active', @@ -142,31 +106,28 @@ export class OAuthStatusListImplementation implements IStatusList { } } - async updateStatusListFromEncodedList( - args: UpdateStatusListFromEncodedListArgs, - context: IAgentContext, - ): Promise { + async updateStatusListFromEncodedList(args: UpdateStatusListFromEncodedListArgs, context: IRequiredContext): Promise { if (!args.oauthStatusList) { throw new Error('OAuthStatusList options are required for type OAuthStatusList') } const { statusPurpose } = args.oauthStatusList const { issuer, id } = getAssertedValues(args) + const bitsPerStatus = args.oauthStatusList.bitsPerStatus ?? BITS_PER_STATUS_DEFAULT + const issuerString = typeof issuer === 'string' ? issuer : issuer.id - const decodedList = StatusList.decompressStatusList(args.encodedList, 1) - const updatedEncodedList = await decodedList.compressStatusList() + const listToUpdate = StatusList.decompressStatusList(args.encodedList, bitsPerStatus) + const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) + listToUpdate.setStatus(index, args.value ? 1 : 0) + + const { jwt, encodedList } = await this.createSignedPayload(context, listToUpdate, issuerString, id, args.keyRef) return { - encodedList: updatedEncodedList, - statusListCredential: { - encodedList: updatedEncodedList, - issuer, - id, - statusPurpose, - }, - length: decodedList.statusList.length, + encodedList, + statusListCredential: jwt, + length: listToUpdate.statusList.length, type: StatusListType.OAuthStatusList, - proofFormat: args.proofFormat ?? 'jwt', + proofFormat: args.proofFormat ?? DEFAULT_PROOF_FORMAT, id, issuer, statusPurpose, @@ -176,8 +137,7 @@ export class OAuthStatusListImplementation implements IStatusList { async checkStatusIndex(args: { statusListCredential: CompactJWT; statusListIndex: string | number }): Promise { const { statusListCredential, statusListIndex } = args - - const statusList = StatusList.decompressStatusList(statusListCredential, 1) + const statusList = StatusList.decompressStatusList(statusListCredential, BITS_PER_STATUS_DEFAULT) const index = typeof statusListIndex === 'number' ? statusListIndex : parseInt(statusListIndex) if (index < 0 || index >= statusList.statusList.length) { @@ -186,4 +146,36 @@ export class OAuthStatusListImplementation implements IStatusList { return statusList.getStatus(index) === 1 } + + private async createSignedPayload(context: IRequiredContext, statusList: StatusList, issuerString: string, id: string, keyRef?: string) { + const identifier = await this.resolveIdentifier(context, issuerString, keyRef) + const payload: JWTPayload = { + iss: issuerString, + sub: id, + iat: Math.floor(new Date().getTime() / 1000), + } + const values = createHeaderAndPayload(statusList, payload, STATUS_LIST_JWT_HEADER) + const signedJwt = await context.agent.jwtCreateJwsCompactSignature({ + issuer: { ...identifier, noIssPayloadUpdate: false }, + protectedHeader: values.header, + payload: values.payload, + }) + + return { + jwt: signedJwt.jwt, + encodedList: (values.payload as StatusListJWTPayload).status_list.lst, + } + } + + private async resolveIdentifier(context: IRequiredContext, issuer: string, keyRef?: string) { + return keyRef + ? await context.agent.identifierManagedGetByKid({ + identifier: keyRef, + }) + : await context.agent.identifierManagedGet({ + identifier: issuer, + vmRelationship: 'assertionMethod', + offlineWhenNoDIDRegistered: true, + }) + } } diff --git a/packages/vc-status-list/src/impl/StatusListFactory.ts b/packages/vc-status-list/src/impl/StatusListFactory.ts index 6651367d1..be25a9673 100644 --- a/packages/vc-status-list/src/impl/StatusListFactory.ts +++ b/packages/vc-status-list/src/impl/StatusListFactory.ts @@ -1,6 +1,6 @@ import { IStatusList } from './IStatusList' import { StatusList2021Implementation } from './StatusList2021' -import { OAuth2StatusListImplementation } from './OAuthStatusList' +import { OAuthStatusListImplementation } from './OAuthStatusList' import { StatusListType } from '../types' export class StatusListFactory { @@ -10,7 +10,7 @@ export class StatusListFactory { private constructor() { this.implementations = new Map() this.implementations.set(StatusListType.StatusList2021, new StatusList2021Implementation()) - this.implementations.set(StatusListType.OAuthStatusList, new OAuth2StatusListImplementation()) + this.implementations.set(StatusListType.OAuthStatusList, new OAuthStatusListImplementation()) } public static getInstance(): StatusListFactory { diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index 2100e8384..cb93671c2 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -22,13 +22,14 @@ import { ProofFormat, } from '@veramo/core' import { DataSource } from 'typeorm' +import { BitsPerStatus } from '@sd-jwt/jwt-status-list/dist' export enum StatusListType { StatusList2021 = 'StatusList2021', OAuthStatusList = 'OAuthStatusList', } -export type StatusPurposeOAuth = 'active' | 'suspended' | 'revoked' | string +export type StatusOAuth = 'active' | 'suspended' | 'revoked' | string export type StatusList2021Args = { encodedList: string @@ -38,7 +39,7 @@ export type StatusList2021Args = { } export type OAuthStatusListArgs = { - statusPurpose: StatusPurposeOAuth + statusPurpose: StatusOAuth expiresAt?: string } @@ -48,6 +49,7 @@ export type BaseCreateNewStatusListArgs = { issuer: string | IIssuer correlationId?: string length?: number + bitsPerStatus?: BitsPerStatus proofFormat?: ProofFormat keyRef?: string statusList2021?: StatusList2021Args @@ -59,7 +61,8 @@ export type UpdateStatusList2021Args = { } export type UpdateOAuthStatusListArgs = { - statusPurpose: StatusPurposeOAuth + bitsPerStatus: BitsPerStatus + statusPurpose: StatusOAuth expiresAt?: string } @@ -89,7 +92,7 @@ export interface StatusListDetails { length: number type: StatusListType proofFormat: ProofFormat - statusPurpose?: StatusPurpose2021 | StatusPurposeOAuth + statusPurpose?: StatusPurpose2021 | StatusOAuth id: string issuer: string | IIssuer indexingDirection: StatusListIndexingDirection From ca080728e466dabbe8e3608d7bf171e12ca9cb98 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 15 Jan 2025 18:15:57 +0100 Subject: [PATCH 07/24] chore: saving work --- .../StatusList2021EntryEntity.ts | 2 +- .../StatusListEntities.ts} | 46 ++++- packages/data-store/src/index.ts | 4 +- .../src/statusList/IStatusListStore.ts | 2 +- .../src/statusList/StatusListStore.ts | 35 +++- .../src/types/statusList/statusList.ts | 12 +- packages/ssi-types/src/types/index.ts | 1 + packages/ssi-types/src/types/status-list.ts | 4 + packages/ssi-types/src/types/w3c-vc.ts | 2 + .../src/drivers.ts | 65 ++++-- .../src/status-list-adapters.ts | 38 ++++ .../src/types.ts | 15 +- .../__tests__/agent.ts | 4 +- .../package.json | 1 + .../src/api-functions.ts | 10 +- .../__tests__/status-list-vc-handling.test.ts | 186 +++++++++++++----- .../src/agent/StatusListPlugin.ts | 22 +-- .../vc-status-list-issuer/src/functions.ts | 7 +- .../__tests__/statuslist.test.ts | 181 +++++++++++++++++ packages/vc-status-list-tests/package.json | 53 +++++ packages/vc-status-list/package.json | 2 + packages/vc-status-list/src/functions.ts | 18 +- .../vc-status-list/src/impl/IStatusList.ts | 39 ++-- .../src/impl/OAuthStatusList.ts | 81 ++++---- .../vc-status-list/src/impl/StatusList2021.ts | 70 +++---- .../src/impl/StatusListFactory.ts | 2 +- packages/vc-status-list/src/types/index.ts | 71 +++++-- packages/vc-status-list/src/utils.ts | 16 +- pnpm-lock.yaml | 75 ++++++- 29 files changed, 822 insertions(+), 242 deletions(-) rename packages/data-store/src/entities/{statusList2021 => statusList}/StatusList2021EntryEntity.ts (95%) rename packages/data-store/src/entities/{statusList2021/StatusList2021Entity.ts => statusList/StatusListEntities.ts} (72%) create mode 100644 packages/ssi-types/src/types/status-list.ts create mode 100644 packages/vc-status-list-issuer-drivers/src/status-list-adapters.ts create mode 100644 packages/vc-status-list-tests/__tests__/statuslist.test.ts create mode 100644 packages/vc-status-list-tests/package.json diff --git a/packages/data-store/src/entities/statusList2021/StatusList2021EntryEntity.ts b/packages/data-store/src/entities/statusList/StatusList2021EntryEntity.ts similarity index 95% rename from packages/data-store/src/entities/statusList2021/StatusList2021EntryEntity.ts rename to packages/data-store/src/entities/statusList/StatusList2021EntryEntity.ts index 128419ded..50bf29dc6 100644 --- a/packages/data-store/src/entities/statusList2021/StatusList2021EntryEntity.ts +++ b/packages/data-store/src/entities/statusList/StatusList2021EntryEntity.ts @@ -1,7 +1,7 @@ import { Validate } from 'class-validator' import { BaseEntity, Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm' import { IsNonEmptyStringConstraint } from '../validators' -import { StatusListEntity } from './StatusList2021Entity' +import { StatusListEntity } from './StatusListEntities' @Entity('StatusListEntry') // @Unique('uq_credential_statuslist', ['statusList', 'credentialId']) // disabled because one prop can be null diff --git a/packages/data-store/src/entities/statusList2021/StatusList2021Entity.ts b/packages/data-store/src/entities/statusList/StatusListEntities.ts similarity index 72% rename from packages/data-store/src/entities/statusList2021/StatusList2021Entity.ts rename to packages/data-store/src/entities/statusList/StatusListEntities.ts index e3c5c5cde..3a99fc790 100644 --- a/packages/data-store/src/entities/statusList2021/StatusList2021Entity.ts +++ b/packages/data-store/src/entities/statusList/StatusListEntities.ts @@ -9,12 +9,13 @@ import { W3CVerifiableCredential, } from '@sphereon/ssi-types' import { ProofFormat } from '@veramo/core' -import { BaseEntity, Column, Entity, OneToMany, PrimaryColumn, Unique } from 'typeorm' +import { BaseEntity, ChildEntity, Column, Entity, OneToMany, PrimaryColumn, TableInheritance, Unique } from 'typeorm' import { StatusListEntryEntity } from './StatusList2021EntryEntity' @Entity('StatusList') @Unique('UQ_correlationId', ['correlationId']) -export class StatusListEntity extends BaseEntity { +@TableInheritance({ column: { type: 'varchar', name: 'type' } }) +export abstract class StatusListEntity extends BaseEntity { @PrimaryColumn({ name: 'id', type: 'varchar' }) id!: string @@ -46,10 +47,20 @@ export class StatusListEntity extends BaseEntity { }) issuer!: string | IIssuer - @Column('simple-enum', { name: 'type', enum: StatusListType, nullable: false, default: StatusListType.StatusList2021 }) + @Column('simple-enum', { + name: 'type', + enum: StatusListType, + nullable: false, + default: StatusListType.StatusList2021, + }) type!: StatusListType - @Column('simple-enum', { name: 'driverType', enum: StatusListDriverType, nullable: false, default: StatusListDriverType.AGENT_TYPEORM }) + @Column('simple-enum', { + name: 'driverType', + enum: StatusListDriverType, + nullable: false, + default: StatusListDriverType.AGENT_TYPEORM, + }) driverType!: StatusListDriverType @Column('simple-enum', { @@ -63,12 +74,6 @@ export class StatusListEntity extends BaseEntity { @Column({ type: 'varchar', name: 'proofFormat', enum: ['lds', 'jwt'], nullable: false, default: 'lds' }) proofFormat!: ProofFormat - @Column({ type: 'varchar', name: 'indexingDirection', enum: ['rightToLeft'], nullable: false, default: 'rightToLeft' }) - indexingDirection!: StatusListIndexingDirection - - @Column({ type: 'varchar', name: 'statusPurpose', nullable: false, default: 'revocation' }) - statusPurpose!: StatusPurpose2021 - @Column({ name: 'statusListCredential', type: 'text', @@ -94,3 +99,24 @@ export class StatusListEntity extends BaseEntity { @OneToMany((type) => StatusListEntryEntity, (entry) => entry.statusList) statusListEntries!: StatusListEntryEntity[] } + +@ChildEntity(StatusListType.StatusList2021) +export class StatusList2021Entity extends StatusListEntity { + @Column({ + type: 'varchar', + name: 'indexingDirection', + enum: ['rightToLeft'], + nullable: false, + default: 'rightToLeft', + }) + indexingDirection!: StatusListIndexingDirection + + @Column({ type: 'varchar', name: 'statusPurpose', nullable: false, default: 'revocation' }) + statusPurpose!: StatusPurpose2021 +} + +@ChildEntity(StatusListType.OAuthStatusList) +export class OAuthStatusListEntity extends StatusListEntity { + bitsPerStatus: number + expiresAt?: string +} diff --git a/packages/data-store/src/index.ts b/packages/data-store/src/index.ts index 1a5948488..afd0ddec8 100644 --- a/packages/data-store/src/index.ts +++ b/packages/data-store/src/index.ts @@ -16,8 +16,8 @@ import { ImageDimensionsEntity } from './entities/issuanceBranding/ImageDimensio import { IssuerLocaleBrandingEntity } from './entities/issuanceBranding/IssuerLocaleBrandingEntity' import { IssuerBrandingEntity } from './entities/issuanceBranding/IssuerBrandingEntity' import { TextAttributesEntity } from './entities/issuanceBranding/TextAttributesEntity' -import { StatusListEntity } from './entities/statusList2021/StatusList2021Entity' -import { StatusListEntryEntity } from './entities/statusList2021/StatusList2021EntryEntity' +import { StatusListEntity } from './entities/statusList/StatusListEntities' +import { StatusListEntryEntity } from './entities/statusList/StatusList2021EntryEntity' import { MachineStateInfoEntity } from './entities/machineState/MachineStateInfoEntity' import { IStatusListEntity, IStatusListEntryEntity } from './types' import { PartyRelationshipEntity } from './entities/contact/PartyRelationshipEntity' diff --git a/packages/data-store/src/statusList/IStatusListStore.ts b/packages/data-store/src/statusList/IStatusListStore.ts index 7081ff95e..da93ce426 100644 --- a/packages/data-store/src/statusList/IStatusListStore.ts +++ b/packages/data-store/src/statusList/IStatusListStore.ts @@ -1,4 +1,4 @@ -import { StatusListEntryEntity } from '../entities/statusList2021/StatusList2021EntryEntity' +import { StatusListEntryEntity } from '../entities/statusList/StatusList2021EntryEntity' import { IAddStatusListArgs, IAddStatusListEntryArgs, diff --git a/packages/data-store/src/statusList/StatusListStore.ts b/packages/data-store/src/statusList/StatusListStore.ts index 6cbb4db99..b4fd19180 100644 --- a/packages/data-store/src/statusList/StatusListStore.ts +++ b/packages/data-store/src/statusList/StatusListStore.ts @@ -1,8 +1,8 @@ import { OrPromise } from '@sphereon/ssi-types' import Debug from 'debug' import { DataSource, In, Repository } from 'typeorm' -import { StatusListEntity } from '../entities/statusList2021/StatusList2021Entity' -import { StatusListEntryEntity } from '../entities/statusList2021/StatusList2021EntryEntity' +import { OAuthStatusListEntity, StatusList2021Entity, StatusListEntity } from '../entities/statusList/StatusListEntities' +import { StatusListEntryEntity } from '../entities/statusList/StatusList2021EntryEntity' import { IAddStatusListArgs, IAddStatusListEntryArgs, @@ -11,11 +11,13 @@ import { IGetStatusListEntryByCredentialIdArgs, IGetStatusListEntryByIndexArgs, IGetStatusListsArgs, + IOAuthStatusListEntity, IRemoveStatusListArgs, - IStatusListEntryAvailableArgs, - IUpdateStatusListIndexArgs, + IStatusList2021Entity, IStatusListEntity, + IStatusListEntryAvailableArgs, IStatusListEntryEntity, + IUpdateStatusListIndexArgs, } from '../types' import { IStatusListStore } from './IStatusListStore' @@ -176,9 +178,15 @@ export class StatusListStore implements IStatusListStore { if (!result) { throw Error(`No status list found for id ${args.id}`) } - return result - } + if (result instanceof StatusList2021Entity) { + return result as IStatusList2021Entity + } else if (result instanceof OAuthStatusListEntity) { + return result as IOAuthStatusListEntity + } + + throw Error(`Invalid status list type ${result.type}`) + } async getStatusLists(args: IGetStatusListsArgs): Promise> { const result = await ( await this.getStatusListRepo() @@ -189,9 +197,16 @@ export class StatusListStore implements IStatusListStore { if (!result) { return [] } - return result - } + return result.map((entity) => { + if (entity instanceof StatusList2021Entity) { + return entity as IStatusList2021Entity + } else if (entity instanceof OAuthStatusListEntity) { + return entity as IOAuthStatusListEntity + } + throw Error(`Invalid status list type ${entity.type}`) + }) + } async addStatusList(args: IAddStatusListArgs): Promise { const { id, correlationId } = args @@ -206,7 +221,6 @@ export class StatusListStore implements IStatusListStore { debug('Adding status list ', id) const createdResult = await (await this.getStatusListRepo()).save(args) - return createdResult } @@ -227,7 +241,8 @@ export class StatusListStore implements IStatusListStore { } async getStatusListRepo(): Promise> { - return (await this.getDS()).getRepository(StatusListEntity) + const repo = (await this.getDS()).getRepository(StatusListEntity) + return repo } async getStatusListEntryRepo(): Promise> { diff --git a/packages/data-store/src/types/statusList/statusList.ts b/packages/data-store/src/types/statusList/statusList.ts index 815d7626c..e0f236944 100644 --- a/packages/data-store/src/types/statusList/statusList.ts +++ b/packages/data-store/src/types/statusList/statusList.ts @@ -8,7 +8,7 @@ import { StatusPurpose2021, } from '@sphereon/ssi-types' import { ProofFormat } from '@veramo/core' -import { StatusListEntity } from '../../entities/statusList2021/StatusList2021Entity' +import { StatusListEntity } from '../../entities/statusList/StatusListEntities' export interface IStatusListEntity { id: string @@ -19,9 +19,17 @@ export interface IStatusListEntity { issuer: string | IIssuer type: StatusListType proofFormat: ProofFormat + statusListCredential?: OriginalVerifiableCredential +} + +export interface IStatusList2021Entity extends IStatusListEntity { indexingDirection: StatusListIndexingDirection statusPurpose: StatusPurpose2021 - statusListCredential?: OriginalVerifiableCredential +} + +export interface IOAuthStatusListEntity extends IStatusListEntity { + bitsPerStatus: number + expiresAt?: string } export interface IStatusListEntryEntity { diff --git a/packages/ssi-types/src/types/index.ts b/packages/ssi-types/src/types/index.ts index 3b979b6e4..01a42c098 100644 --- a/packages/ssi-types/src/types/index.ts +++ b/packages/ssi-types/src/types/index.ts @@ -9,3 +9,4 @@ export * from './jose' export * from './cose' export * from './mso_mdoc' export * from './metadata-types' +export * from './status-list' diff --git a/packages/ssi-types/src/types/status-list.ts b/packages/ssi-types/src/types/status-list.ts new file mode 100644 index 000000000..c51f7e30f --- /dev/null +++ b/packages/ssi-types/src/types/status-list.ts @@ -0,0 +1,4 @@ +export enum StatusListType { + StatusList2021 = 'StatusList2021', + OAuthStatusList = 'OAuthStatusList', +} diff --git a/packages/ssi-types/src/types/w3c-vc.ts b/packages/ssi-types/src/types/w3c-vc.ts index b05ff6f50..2464e0eb0 100644 --- a/packages/ssi-types/src/types/w3c-vc.ts +++ b/packages/ssi-types/src/types/w3c-vc.ts @@ -276,9 +276,11 @@ export interface IErrorDetails { cause?: IError } +/* FIXME figurae out how to handle this, we can't have duplicates and we need one in ssi-types for the data store logic export enum StatusListType { StatusList2021 = 'StatusList2021', } +*/ export type StatusPurpose2021 = 'revocation' | 'suspension' | string diff --git a/packages/vc-status-list-issuer-drivers/src/drivers.ts b/packages/vc-status-list-issuer-drivers/src/drivers.ts index 01b8501ba..0b268b8f1 100644 --- a/packages/vc-status-list-issuer-drivers/src/drivers.ts +++ b/packages/vc-status-list-issuer-drivers/src/drivers.ts @@ -5,12 +5,20 @@ import { IGetStatusListEntryByIndexArgs, IStatusListEntity, IStatusListEntryEntity, + StatusListEntity, StatusListStore, } from '@sphereon/ssi-sdk.data-store' -import { StatusList2021EntryCredentialStatus, statusListCredentialToDetails, StatusListDetails } from '@sphereon/ssi-sdk.vc-status-list' -import { OriginalVerifiableCredential, StatusListCredentialIdMode, StatusListDriverType } from '@sphereon/ssi-types' +import { + StatusList2021EntryCredentialStatus, + statusListCredentialToDetails, + StatusListOAuthEntryCredentialStatus, + StatusListResult, +} from '@sphereon/ssi-sdk.vc-status-list' +import { OriginalVerifiableCredential, StatusListCredentialIdMode, StatusListDriverType, StatusListType } from '@sphereon/ssi-types' import { DataSource } from 'typeorm' import { IStatusListDriver } from './types' +import { statusListResultToEntity } from './status-list-adapters' +import { OAuthStatusListEntity, StatusList2021Entity } from '@sphereon/ssi-sdk.data-store/dist/entities/statusList/StatusListEntities' export interface StatusListManagementOptions { id?: string @@ -124,7 +132,7 @@ export class AgentDataSourceStatusListDriver implements IStatusListDriver { statusListCredential: OriginalVerifiableCredential correlationId?: string credentialIdMode?: StatusListCredentialIdMode - }): Promise { + }): Promise { const correlationId = args.correlationId ?? this.options.correlationId if (!correlationId) { throw Error('Either a correlationId needs to be set as an option, or it needs to be provided when creating a status list. None found') @@ -143,7 +151,7 @@ export class AgentDataSourceStatusListDriver implements IStatusListDriver { return details } - async updateStatusList(args: { statusListCredential: OriginalVerifiableCredential; correlationId: string }): Promise { + async updateStatusList(args: { statusListCredential: OriginalVerifiableCredential; correlationId: string }): Promise { const correlationId = args.correlationId ?? this.options.correlationId const details = await statusListCredentialToDetails({ ...args, correlationId, driverType: this.getType() }) const entity = await ( @@ -176,21 +184,50 @@ export class AgentDataSourceStatusListDriver implements IStatusListDriver { return Promise.resolve(true) } + private isStatusList2021Entity(statusList: StatusListEntity): statusList is StatusList2021Entity { + return statusList.type === StatusListType.StatusList2021 + } + + private isOAuthStatusListEntity(statusList: StatusListEntity): statusList is OAuthStatusListEntity { + return statusList.type === StatusListType.OAuthStatusList + } + async updateStatusListEntry(args: IAddStatusListEntryArgs): Promise<{ - credentialStatus: StatusList2021EntryCredentialStatus + credentialStatus: StatusList2021EntryCredentialStatus | StatusListOAuthEntryCredentialStatus statusListEntry: IStatusListEntryEntity }> { - const statusList = typeof args.statusList === 'string' ? await this.getStatusList() : args.statusList + const statusList: StatusListEntity = typeof args.statusList === 'string' ? statusListResultToEntity(await this.getStatusList()) : args.statusList const statusListEntry = await this.statusListStore.updateStatusListEntry({ ...args, statusList: statusList.id }) - const credentialStatus: StatusList2021EntryCredentialStatus = { - id: `${statusList.id}#${statusListEntry.statusListIndex}`, - type: 'StatusList2021Entry', - statusPurpose: statusList.statusPurpose ?? 'revocation', - statusListIndex: '' + statusListEntry.statusListIndex, - statusListCredential: statusList.id, + + if (this.isStatusList2021Entity(statusList)) { + return { + credentialStatus: { + id: `${statusList.id}#${statusListEntry.statusListIndex}`, + type: 'StatusList2021Entry', + statusPurpose: statusList.statusPurpose ?? 'revocation', + statusListIndex: '' + statusListEntry.statusListIndex, + statusListCredential: statusList.id, + }, + statusListEntry, + } + } + + if (this.isOAuthStatusListEntity(statusList)) { + return { + credentialStatus: { + id: `${statusList.id}#${statusListEntry.statusListIndex}`, + type: 'OAuthStatusListEntry', + bitsPerStatus: statusList.bitsPerStatus, + statusListIndex: '' + statusListEntry.statusListIndex, + statusListCredential: statusList.id, + expiresAt: statusList.expiresAt, + }, + statusListEntry, + } } - return { credentialStatus, statusListEntry } + + throw new Error(`Unsupported status list type: ${statusList.type}`) } async getStatusListEntryByCredentialId(args: IGetStatusListEntryByCredentialIdArgs): Promise { @@ -238,7 +275,7 @@ export class AgentDataSourceStatusListDriver implements IStatusListDriver { return this._statusListLength! } - async getStatusList(args?: { correlationId?: string }): Promise { + async getStatusList(args?: { correlationId?: string }): Promise { const id = this.options.id const correlationId = args?.correlationId ?? this.options.correlationId return await this.statusListStore diff --git a/packages/vc-status-list-issuer-drivers/src/status-list-adapters.ts b/packages/vc-status-list-issuer-drivers/src/status-list-adapters.ts new file mode 100644 index 000000000..6f309a3fe --- /dev/null +++ b/packages/vc-status-list-issuer-drivers/src/status-list-adapters.ts @@ -0,0 +1,38 @@ +import { StatusListType } from '@sphereon/ssi-types' +import { OAuthStatusListEntity, StatusList2021Entity } from '@sphereon/ssi-sdk.data-store/dist/entities/statusList/StatusListEntities' +import { StatusListResult } from '@sphereon/ssi-sdk.vc-status-list' + +export function statusListResultToEntity(result: StatusListResult): StatusList2021Entity | OAuthStatusListEntity { + const baseFields = { + id: result.id, + correlationId: result.correlationId, + driverType: result.driverType, + credentialIdMode: result.credentialIdMode, + length: result.length, + issuer: result.issuer, + type: result.type, + proofFormat: result.proofFormat, + statusListCredential: result.statusListCredential, + } + + if (result.type === StatusListType.StatusList2021) { + if (!result.statusList2021) { + throw new Error('Missing statusList2021 details') + } + return Object.assign(new StatusList2021Entity(), { + ...baseFields, + indexingDirection: result.statusList2021.indexingDirection, + statusPurpose: result.statusList2021.statusPurpose, + }) + } else if (result.type === StatusListType.OAuthStatusList) { + if (!result.oauthStatusList) { + throw new Error('Missing oauthStatusList details') + } + return Object.assign(new OAuthStatusListEntity(), { + ...baseFields, + bitsPerStatus: result.oauthStatusList.bitsPerStatus, + expiresAt: undefined, // Optional field + }) + } + throw new Error(`Unsupported status list type: ${result.type}`) +} diff --git a/packages/vc-status-list-issuer-drivers/src/types.ts b/packages/vc-status-list-issuer-drivers/src/types.ts index 6142c92be..0ed92bdd9 100644 --- a/packages/vc-status-list-issuer-drivers/src/types.ts +++ b/packages/vc-status-list-issuer-drivers/src/types.ts @@ -6,7 +6,12 @@ import { IStatusListEntryEntity, StatusListStore, } from '@sphereon/ssi-sdk.data-store' -import { IStatusListPlugin, StatusList2021EntryCredentialStatus, StatusListDetails } from '@sphereon/ssi-sdk.vc-status-list' +import { + IStatusListPlugin, + StatusList2021EntryCredentialStatus, + StatusListOAuthEntryCredentialStatus, + StatusListResult, +} from '@sphereon/ssi-sdk.vc-status-list' import { OriginalVerifiableCredential, StatusListDriverType } from '@sphereon/ssi-types' import { IAgentContext, @@ -40,12 +45,12 @@ export interface IStatusListDriver { getStatusListLength(args?: { correlationId?: string }): Promise - createStatusList(args: { statusListCredential: OriginalVerifiableCredential; correlationId?: string }): Promise + createStatusList(args: { statusListCredential: OriginalVerifiableCredential; correlationId?: string }): Promise - getStatusList(args?: { correlationId?: string }): Promise + getStatusList(args?: { correlationId?: string }): Promise updateStatusListEntry(args: IAddStatusListEntryArgs): Promise<{ - credentialStatus: StatusList2021EntryCredentialStatus + credentialStatus: StatusList2021EntryCredentialStatus | StatusListOAuthEntryCredentialStatus statusListEntry: IStatusListEntryEntity }> @@ -53,7 +58,7 @@ export interface IStatusListDriver { getStatusListEntryByIndex(args: IGetStatusListEntryByIndexArgs): Promise - updateStatusList(args: { statusListCredential: OriginalVerifiableCredential }): Promise + updateStatusList(args: { statusListCredential: OriginalVerifiableCredential }): Promise deleteStatusList(): Promise diff --git a/packages/vc-status-list-issuer-rest-api/__tests__/agent.ts b/packages/vc-status-list-issuer-rest-api/__tests__/agent.ts index 45b2a5e50..30044c041 100644 --- a/packages/vc-status-list-issuer-rest-api/__tests__/agent.ts +++ b/packages/vc-status-list-issuer-rest-api/__tests__/agent.ts @@ -27,6 +27,7 @@ import { Resolver } from 'did-resolver' import { StatuslistManagementApiServer } from '../src' import { IRequiredPlugins } from '@sphereon/ssi-sdk.vc-status-list-issuer-drivers' import { DB_CONNECTION_NAME_POSTGRES, DB_ENCRYPTION_KEY, postgresConfig } from './database' +import { IJwtService, JwtService } from '@sphereon/ssi-sdk-ext.jwt-service' const debug = Debug('sphereon:status-list-api') @@ -66,7 +67,7 @@ const dbConnection = DataSources.singleInstance() const privateKeyStore: PrivateKeyStore = new PrivateKeyStore(dbConnection, new SecretBox(DB_ENCRYPTION_KEY)) const agent: TAgent = createAgent< - IDIDManager & IKeyManager & IDataStoreORM & IResolver & ICredentialHandlerLDLocal & ICredentialPlugin & IIdentifierResolution + IDIDManager & IKeyManager & IDataStoreORM & IResolver & ICredentialHandlerLDLocal & ICredentialPlugin & IIdentifierResolution & IJwtService >({ plugins: [ new DataStore(dbConnection), @@ -86,6 +87,7 @@ const agent: TAgent = createAgent< resolver, }), new IdentifierResolution({ crypto: global.crypto }), + new JwtService(), new CredentialPlugin(), new CredentialHandlerLDLocal({ contextMaps: [LdDefaultContexts], diff --git a/packages/vc-status-list-issuer-rest-api/package.json b/packages/vc-status-list-issuer-rest-api/package.json index 270d9d62f..c6d43a81f 100644 --- a/packages/vc-status-list-issuer-rest-api/package.json +++ b/packages/vc-status-list-issuer-rest-api/package.json @@ -15,6 +15,7 @@ "@sphereon/ssi-express-support": "workspace:*", "@sphereon/ssi-sdk-ext.did-utils": "0.27.0", "@sphereon/ssi-sdk-ext.identifier-resolution": "0.27.0", + "@sphereon/ssi-sdk-ext.jwt-service": "0.27.0", "@sphereon/ssi-sdk.core": "workspace:*", "@sphereon/ssi-sdk.data-store": "workspace:*", "@sphereon/ssi-sdk.vc-status-list": "workspace:*", diff --git a/packages/vc-status-list-issuer-rest-api/src/api-functions.ts b/packages/vc-status-list-issuer-rest-api/src/api-functions.ts index 8402d612f..20528dd59 100644 --- a/packages/vc-status-list-issuer-rest-api/src/api-functions.ts +++ b/packages/vc-status-list-issuer-rest-api/src/api-functions.ts @@ -8,6 +8,7 @@ import { getDriver } from '@sphereon/ssi-sdk.vc-status-list-issuer-drivers' import Debug from 'debug' import { Request, Response, Router } from 'express' import { ICredentialStatusListEndpointOpts, IRequiredContext, IW3CredentialStatusEndpointOpts, UpdateCredentialStatusRequest } from './types' +import { StatusListType } from '@sphereon/ssi-types' const debug = Debug('sphereon:ssi-sdk:status-list') @@ -100,7 +101,14 @@ export function getStatusListCredentialIndexStatusEndpoint(router: Router, conte correlationId: details.correlationId, errorOnNotFound: false, }) - const status = await checkStatusIndexFromStatusListCredential({ ...details, statusListIndex }) + const type = details.type === StatusListType.StatusList2021 ? 'StatusList2021Entry' : details.type + const status = await checkStatusIndexFromStatusListCredential({ + statusListCredential: details.statusListCredential, + statusPurpose: details.statusList2021?.statusPurpose, + type, + id: details.id, + statusListIndex, + }) if (!entry) { // The fact we have nothing on it means the status is okay entry = { diff --git a/packages/vc-status-list-issuer/__tests__/status-list-vc-handling.test.ts b/packages/vc-status-list-issuer/__tests__/status-list-vc-handling.test.ts index 248650417..00d36c8c8 100644 --- a/packages/vc-status-list-issuer/__tests__/status-list-vc-handling.test.ts +++ b/packages/vc-status-list-issuer/__tests__/status-list-vc-handling.test.ts @@ -12,19 +12,9 @@ import { SphereonEd25519Signature2020, SphereonJsonWebSignature2020, } from '@sphereon/ssi-sdk.vc-handler-ld-local' -import { IStatusListPlugin } from '@sphereon/ssi-sdk.vc-status-list' -import { StatusListDriverType, StatusListType } from '@sphereon/ssi-types' -import { - createAgent, - CredentialPayload, - ICredentialPlugin, - IDataStoreORM, - IDIDManager, - IIdentifier, - IKeyManager, - IResolver, - TAgent, -} from '@veramo/core' +import { IStatusListPlugin, StatusListResult } from '@sphereon/ssi-sdk.vc-status-list' +import { IVerifiableCredential, StatusListDriverType, StatusListType } from '@sphereon/ssi-types' +import { createAgent, ICredentialPlugin, IDataStoreORM, IDIDManager, IKeyManager, IResolver, TAgent } from '@veramo/core' import { CredentialPlugin } from '@veramo/credential-w3c' import { DataStore, DataStoreORM, DIDStore, KeyStore, PrivateKeyStore } from '@veramo/data-store' import { DIDManager } from '@veramo/did-manager' @@ -34,7 +24,6 @@ import { KeyManager } from '@veramo/key-manager' import { KeyManagementSystem, SecretBox } from '@veramo/kms-local' import Debug from 'debug' import { Resolver } from 'did-resolver' -import { v4 } from 'uuid' import { StatusListPlugin } from '../src/agent/StatusListPlugin' import { DB_CONNECTION_NAME_POSTGRES, DB_ENCRYPTION_KEY, sqliteConfig } from './database' @@ -88,14 +77,19 @@ describe('JWT Verifiable Credential, should be', () => { let agent: TAgent // let agentContext: IAgentContext - let identifier: IIdentifier beforeAll(async () => { agent = createAgent({ plugins: [ new DataStore(dbConnection), new DataStoreORM(dbConnection), new StatusListPlugin({ - instances: [{ id: 'http://localhost/test/1', driverType: StatusListDriverType.AGENT_TYPEORM, dataSource: dbConnection }], + instances: [ + { + id: 'http://localhost/test/1', + driverType: StatusListDriverType.AGENT_TYPEORM, + dataSource: dbConnection, + }, + ], defaultInstanceId: 'http://localhost/test/1', allDataSources: DataSources.singleInstance(), }), @@ -134,7 +128,7 @@ describe('JWT Verifiable Credential, should be', () => { // agentContext = {...agent.context, agent}; await agent.dataStoreORMGetIdentifiers().then((ids) => ids.forEach((id) => console.log(JSON.stringify(id, null, 2)))) - identifier = await agent + await agent .didManagerCreate({ provider: 'did:jwk', alias: 'test', @@ -151,38 +145,132 @@ describe('JWT Verifiable Credential, should be', () => { }) }) - it('should add status list to credential', async () => { - // Just for this test we are creating the status list. Normally this has been pre-created of course - const sl = await agent.slCreateStatusList({ - id: 'http://localhost/test/1', - issuer: identifier.did, - type: StatusListType.StatusList2021, - proofFormat: 'jwt', - statusPurpose: 'revocation', - keyRef: identifier.keys[0].kid, - correlationId: '1', + describe('slCreateStatusList', () => { + it('should reject non-JWT proof formats like LD-Signatures when creating OAuth status list', async () => { + await expect( + agent.slCreateStatusList({ + type: StatusListType.OAuthStatusList, + issuer: 'did:example:123', + id: 'list123', + proofFormat: 'lds', + oauthStatusList: { + bitsPerStatus: 2, + }, + }), + ).rejects.toThrow("Invalid proof format 'lds' for OAuthStatusList") + }) + + it('should successfully create OAuth status list using JWT format with proper header and encoding', async () => { + const mockResult = { + type: StatusListType.OAuthStatusList, + proofFormat: 'jwt', + statusListCredential: 'ey_eyMockJWT', + encodedList: 'AAAA', + id: 'list123', + issuer: 'did:example:123', + length: 250000, + oauthStatusList: { + bitsPerStatus: 2, + }, + } satisfies StatusListResult + jest.spyOn(agent, 'slCreateStatusList').mockResolvedValue(mockResult) + + const result = await agent.slCreateStatusList({ + type: StatusListType.OAuthStatusList, + issuer: 'did:example:123', + id: 'list123', + proofFormat: 'jwt', + oauthStatusList: { + bitsPerStatus: 2, + }, + }) + + expect(result.type).toBe(StatusListType.OAuthStatusList) + expect(result.proofFormat).toBe('jwt') + expect(result.statusListCredential).toBe('ey_eyMockJWT') + }) + }) + + describe('slAddStatusToCredential', () => { + it('should inject a status to a credential', async () => { + const mockCredential: IVerifiableCredential = { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential'], + issuer: 'did:example:123', + issuanceDate: '2024-01-15T00:00:00Z', + credentialSubject: { + id: 'did:example:456', + }, + proof: { + type: 'Ed25519Signature2018', + created: '2024-01-15T00:00:00Z', + proofPurpose: 'assertionMethod', + verificationMethod: 'did:example:123#key-1', + }, + } + + const mockResultCredential: IVerifiableCredential = { + ...mockCredential, + credentialStatus: { + id: 'list123#0', + type: 'OAuth2StatusList', + statusListIndex: '0', + statusListCredential: 'eyMockJWT', + }, + } + + jest.spyOn(agent, 'slAddStatusToCredential').mockResolvedValue(mockResultCredential) + + const result = await agent.slAddStatusToCredential({ + credential: mockCredential, + statusListId: 'list123', + statusListIndex: 0, + }) + + expect(result.credentialStatus?.type).toBe('OAuth2StatusList') + expect(result.credentialStatus?.id).toBe('list123#0') + expect(result.credentialStatus?.statusListIndex).toBe('0') + expect(result.credentialStatus?.statusListCredential).toBe('eyMockJWT') + expect(result.issuer).toBe('did:example:123') + }) + }) + + describe('slGetStatusList', () => { + it('should retrieve an existing status list', async () => { + const mockResult: StatusListResult = { + type: StatusListType.OAuthStatusList, + proofFormat: 'jwt', + statusListCredential: 'ey_mockJWT', + encodedList: 'AAAA', + id: 'list123', + issuer: 'did:example:123', + length: 250000, + oauthStatusList: { + bitsPerStatus: 2, + }, + } + + jest.spyOn(agent, 'slGetStatusList').mockResolvedValue(mockResult) + + const result = await agent.slGetStatusList({ + id: 'list123', + }) + + expect(result.id).toBe('list123') + expect(result.type).toBe(StatusListType.OAuthStatusList) + expect(result.encodedList).toBe('AAAA') + expect(result.statusListCredential).toBe('ey_mockJWT') + expect(result.length).toBe(250000) + }) + + it('should throw when status list not found', async () => { + jest.spyOn(agent, 'slGetStatusList').mockRejectedValue(new Error('Status list not found')) + + await expect( + agent.slGetStatusList({ + id: 'nonexistent', + }), + ).rejects.toThrow('Status list not found') }) - console.log(JSON.stringify(sl, null, 2)) - - // @ts-ignore // We do not provide the credentialStatus id as the plugin should handle that - const vcPayload = { - issuer: identifier.did, - id: v4(), - credentialSubject: { - id: identifier.did, - example: 'value', - }, - - // Let's create a credentialStatus object, so that the status list handling code will assign an index automatically - credentialStatus: { - type: 'StatusList2021', - }, - } as CredentialPayload - const vc = await agent.createVerifiableCredentialLDLocal({ credential: vcPayload, keyRef: identifier.keys[0].kid }) - expect(vc).toBeDefined() - expect(vc.credentialStatus).toBeDefined() - expect(vc.credentialStatus?.statusListIndex).toBeDefined() - - console.log(JSON.stringify(vc, null, 2)) }) }) diff --git a/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts b/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts index 70ecd4f98..7d9a72591 100644 --- a/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts +++ b/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts @@ -8,7 +8,7 @@ import { IRequiredContext, IRequiredPlugins, IStatusListPlugin, - StatusListDetails, + StatusListResult, } from '@sphereon/ssi-sdk.vc-status-list' import { getDriver } from '@sphereon/ssi-sdk.vc-status-list-issuer-drivers' import { Loggers } from '@sphereon/ssi-types' @@ -47,7 +47,7 @@ export class StatusListPlugin implements IAgentPlugin { this.autoCreateInstances = opts.autoCreateInstances ?? true } - private async slGetStatusList(args: GetStatusListArgs, context: IAgentContext): Promise { + private async slGetStatusList(args: GetStatusListArgs, context: IAgentContext): Promise { const sl = this.instances.find((instance) => instance.id === args.id || instance.correlationId === args.correlationId) const dataSource = (sl?.dataSource ?? args?.dataSource) @@ -74,7 +74,7 @@ export class StatusListPlugin implements IAgentPlugin { private async slCreateStatusList( args: CreateNewStatusListArgs, context: IAgentContext, - ): Promise { + ): Promise { const sl = await createNewStatusList(args, context) const dataSource = args?.dataSource ? await args.dataSource @@ -86,29 +86,29 @@ export class StatusListPlugin implements IAgentPlugin { correlationId: sl.correlationId, dataSource, }) - let statusListDetails: StatusListDetails | undefined = undefined + let statusListResponse: StatusListResult | undefined = undefined try { - statusListDetails = await this.slGetStatusList(args, context) + statusListResponse = await this.slGetStatusList(args, context) } catch (e) { // That is fine if there is no status list yet } - if (statusListDetails && this.instances.find((sl) => sl.id === args.id || sl.correlationId === args.correlationId)) { + if (statusListResponse && this.instances.find((sl) => sl.id === args.id || sl.correlationId === args.correlationId)) { return Promise.reject(Error(`Status list with id ${args.id} or correlation id ${args.correlationId} already exists`)) } else { - statusListDetails = await driver.createStatusList({ + statusListResponse = await driver.createStatusList({ statusListCredential: sl.statusListCredential, correlationId: sl.correlationId, }) this.instances.push({ - correlationId: statusListDetails.correlationId, - id: statusListDetails.id, + correlationId: statusListResponse.correlationId, + id: statusListResponse.id, dataSource, - driverType: statusListDetails.driverType!, + driverType: statusListResponse.driverType!, driverOptions: driver.getOptions(), }) } - return statusListDetails + return statusListResponse } private async slAddStatusToCredential(args: IAddStatusToCredentialArgs, context: IRequiredContext): Promise { diff --git a/packages/vc-status-list-issuer/src/functions.ts b/packages/vc-status-list-issuer/src/functions.ts index e26897e51..2e90fa253 100644 --- a/packages/vc-status-list-issuer/src/functions.ts +++ b/packages/vc-status-list-issuer/src/functions.ts @@ -4,7 +4,7 @@ import { IIssueCredentialStatusOpts, IRequiredPlugins, IStatusListPlugin, - StatusListDetails, + StatusListResult, } from '@sphereon/ssi-sdk.vc-status-list' import { getDriver, IStatusListDriver } from '@sphereon/ssi-sdk.vc-status-list-issuer-drivers' import { StatusListCredentialIdMode, StatusListType, StatusPurpose2021 } from '@sphereon/ssi-types' @@ -17,7 +17,7 @@ export const createStatusListFromInstance = async ( instance: StatusListInstance & { issuer: string; type?: StatusListType; statusPurpose?: StatusPurpose2021 } }, context: IAgentContext, -): Promise => { +): Promise => { const instance = { ...args.instance, dataSource: args.instance.dataSource ? await args.instance.dataSource : undefined, @@ -25,7 +25,7 @@ export const createStatusListFromInstance = async ( statusPurpose: args.instance.statusPurpose ?? 'revocation', correlationId: args.instance.correlationId ?? args.instance.id, } - let sl: StatusListDetails + let sl: StatusListResult try { sl = await context.agent.slGetStatusList(instance) } catch (e) { @@ -68,6 +68,7 @@ export const handleCredentialStatus = async ( 'No credential.id was provided in the credential, whilst the issuer is configured to persist credentialIds. Please adjust your input credential to contain an id', ) } + let existingEntry: IStatusListEntryEntity | undefined = undefined // Search whether there is an existing status list entry for this credential first if (credentialId) { diff --git a/packages/vc-status-list-tests/__tests__/statuslist.test.ts b/packages/vc-status-list-tests/__tests__/statuslist.test.ts new file mode 100644 index 000000000..7fdd3812c --- /dev/null +++ b/packages/vc-status-list-tests/__tests__/statuslist.test.ts @@ -0,0 +1,181 @@ +import { IdentifierResolution, IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' +import { createAgent, ICredentialPlugin, IDIDManager, IIdentifier, IKeyManager, IResolver, TAgent } from '@veramo/core' +import { CredentialPlugin, ICredentialIssuer } from '@veramo/credential-w3c' +import { DIDManager, MemoryDIDStore } from '@veramo/did-manager' +import { getDidKeyResolver, SphereonKeyDidProvider } from '@sphereon/ssi-sdk-ext.did-provider-key' +import { DIDResolverPlugin } from '@veramo/did-resolver' +import { SphereonKeyManager } from '@sphereon/ssi-sdk-ext.key-manager' +import { SphereonKeyManagementSystem } from '@sphereon/ssi-sdk-ext.kms-local' +import { MemoryKeyStore, MemoryPrivateKeyStore } from '@veramo/key-manager' +import { Resolver } from 'did-resolver' +import { + checkStatusIndexFromStatusListCredential, + createNewStatusList, + updateStatusIndexFromStatusListCredential, +} from '@sphereon/ssi-sdk.vc-status-list' +import { + CredentialHandlerLDLocal, + ICredentialHandlerLDLocal, + LdDefaultContexts, + MethodNames, + SphereonEcdsaSecp256k1RecoverySignature2020, + SphereonEd25519Signature2018, + SphereonEd25519Signature2020, +} from '@sphereon/ssi-sdk.vc-handler-ld-local' +// @ts-ignore +import nock from 'nock' +import { StatusListType } from '@sphereon/ssi-types' +import { JwtService } from '@sphereon/ssi-sdk-ext.jwt-service' + +jest.setTimeout(100000) + +describe('Status list', () => { + let didKeyIdentifier: IIdentifier + let agent: TAgent + + // jest.setTimeout(1000000) + beforeAll(async () => { + agent = createAgent({ + plugins: [ + new SphereonKeyManager({ + store: new MemoryKeyStore(), + kms: { + local: new SphereonKeyManagementSystem(new MemoryPrivateKeyStore()), + }, + }), + new DIDManager({ + providers: { + 'did:key': new SphereonKeyDidProvider({ defaultKms: 'local' }), + }, + store: new MemoryDIDStore(), + defaultProvider: 'did:key', + }), + new IdentifierResolution({ crypto: global.crypto }), + new JwtService(), + new DIDResolverPlugin({ + resolver: new Resolver({ + ...getDidKeyResolver(), + }), + }), + new CredentialPlugin(), + new CredentialHandlerLDLocal({ + contextMaps: [LdDefaultContexts], + suites: [new SphereonEd25519Signature2018(), new SphereonEd25519Signature2020(), new SphereonEcdsaSecp256k1RecoverySignature2020()], + bindingOverrides: new Map([ + // Bindings to test overrides of credential-ld plugin methods + ['createVerifiableCredentialLD', MethodNames.createVerifiableCredentialLDLocal], + ['createVerifiablePresentationLD', MethodNames.createVerifiablePresentationLDLocal], + // We test the verify methods by using the LDLocal versions directly in the tests + ]), + }), + ], + }) + didKeyIdentifier = await agent.didManagerCreate() + }) + + describe('StatusList2021', () => { + it('should create and update using LD-Signatures', async () => { + const statusList = await createNewStatusList( + { + type: StatusListType.StatusList2021, + proofFormat: 'lds', + id: 'http://localhost:9543/list1', + issuer: didKeyIdentifier.did, + length: 99999, + correlationId: 'test-1-' + Date.now(), + statusList2021: { + indexingDirection: 'rightToLeft', + }, + }, + { agent }, + ) + expect(statusList.type).toBe(StatusListType.StatusList2021) + expect(statusList.proofFormat).toBe('lds') + expect(statusList.statusList2021?.indexingDirection).toBe('rightToLeft') + + const updated = await updateStatusIndexFromStatusListCredential( + { statusListCredential: statusList.statusListCredential, statusListIndex: 2, value: true }, + { agent }, + ) + const status = await checkStatusIndexFromStatusListCredential({ + statusListCredential: updated.statusListCredential, + statusListIndex: '2', + }) + expect(status).toBe(1) + }) + + it('should create and update using JWT format', async () => { + const statusList = await createNewStatusList( + { + type: StatusListType.StatusList2021, + proofFormat: 'jwt', + id: 'http://localhost:9543/list2', + issuer: didKeyIdentifier.did, + length: 99999, + correlationId: 'test-2-' + Date.now(), + statusList2021: { + indexingDirection: 'rightToLeft', + }, + }, + { agent }, + ) + + const updated = await updateStatusIndexFromStatusListCredential( + { statusListCredential: statusList.statusListCredential, statusListIndex: 3, value: true }, + { agent }, + ) + const status = await checkStatusIndexFromStatusListCredential({ + statusListCredential: updated.statusListCredential, + statusListIndex: '3', + }) + expect(status).toBe(1) + }) + }) + + describe('OAuthStatusList', () => { + it('should create and update using JWT format', async () => { + const statusList = await createNewStatusList( + { + type: StatusListType.OAuthStatusList, + proofFormat: 'jwt', + id: 'http://localhost:9543/oauth1', + issuer: didKeyIdentifier.did, + length: 99999, + correlationId: 'test-3-' + Date.now(), + oauthStatusList: { + bitsPerStatus: 2, + }, + }, + { agent }, + ) + + const updated = await updateStatusIndexFromStatusListCredential( + { statusListCredential: statusList.statusListCredential, statusListIndex: 4, value: true }, + { agent }, + ) + const status = await checkStatusIndexFromStatusListCredential({ + statusListCredential: updated.statusListCredential, + statusListIndex: '4', + }) + expect(status).toBe(1) + }) + + it('should reject LD-Signatures format', async () => { + await expect( + createNewStatusList( + { + type: StatusListType.OAuthStatusList, + proofFormat: 'lds', + id: 'http://localhost:9543/oauth2', + issuer: didKeyIdentifier.did, + length: 99999, + oauthStatusList: { + bitsPerStatus: 2, + }, + }, + { agent }, + ), + ).rejects.toThrow("Invalid proof format 'lds' for OAuthStatusList") + }) + }) +}) diff --git a/packages/vc-status-list-tests/package.json b/packages/vc-status-list-tests/package.json new file mode 100644 index 000000000..caa852384 --- /dev/null +++ b/packages/vc-status-list-tests/package.json @@ -0,0 +1,53 @@ +{ + "name": "@sphereon/vc-status-list-tests", + "version": "0.32.0", + "scripts": { + "build": "tsc", + "build:clean": "tsc --build --clean && tsc --build" + }, + "dependencies": { + }, + "devDependencies": { + "@types/jest": "^27.5.2", + "@types/node": "^20.17.1", + "@sphereon/ssi-sdk.vc-handler-ld-local": "workspace:*", + "@sphereon/ssi-sdk.vc-status-list": "workspace:*", + "@sphereon/ssi-types": "workspace:*", + "@sphereon/ssi-sdk-ext.identifier-resolution": "0.27.0", + "@sphereon/ssi-sdk-ext.jwt-service": "0.27.0", + "@sphereon/ssi-sdk-ext.key-manager": "0.27.0", + "@sphereon/ssi-sdk-ext.kms-local": "0.27.0", + "@sphereon/ssi-sdk-ext.did-provider-key": "0.27.0", + "did-resolver": "^4.1.0", + "@veramo/core": "4.2.0", + "@veramo/credential-w3c": "4.2.0", + "@veramo/did-manager": "4.2.0", + "@veramo/key-manager": "4.2.0", + "@veramo/did-resolver": "4.2.0", + "nock": "^13.5.4", + "ts-node": "^10.9.2", + "typescript": "5.6.3" + }, + "files": [ + ], + "private": true, + "publishConfig": { + "access": "public" + }, + "repository": "git@github.com:Sphereon-Opensource/SSI-SDK.git", + "author": "Sphereon ", + "license": "Apache-2.0", + "keywords": [ + "Sphereon", + "SSI", + "Veramo", + "W3C", + "Verifiable Credentials", + "Verifiable Presentations", + "JsonLd" + ], + "peerDependencies": { + "react-native-securerandom": "^1.0.1" + }, + "nx": {} +} diff --git a/packages/vc-status-list/package.json b/packages/vc-status-list/package.json index 794a02b27..625c7b9e2 100644 --- a/packages/vc-status-list/package.json +++ b/packages/vc-status-list/package.json @@ -18,6 +18,7 @@ "@sphereon/vc-status-list": "7.0.0-next.0", "@veramo/core": "4.2.0", "@veramo/credential-status": "4.2.0", + "base64url": "^3.0.1", "credential-status": "^2.0.6", "debug": "^4.3.5", "typeorm": "^0.3.20", @@ -28,6 +29,7 @@ "@babel/core": "^7.24.9", "@babel/preset-env": "^7.24.8", "@babel/preset-typescript": "^7.24.7", + "@veramo/key-manager": "4.2.0", "typescript": "5.4.2" }, "files": [ diff --git a/packages/vc-status-list/src/functions.ts b/packages/vc-status-list/src/functions.ts index db5377dc6..b38646bb3 100644 --- a/packages/vc-status-list/src/functions.ts +++ b/packages/vc-status-list/src/functions.ts @@ -6,9 +6,10 @@ import { CredentialStatus, DIDDocument, IAgentContext, ICredentialPlugin } from import { CredentialJwtOrJSON, StatusMethod } from 'credential-status' import { CreateNewStatusListFuncArgs, + Status2021, StatusList2021ToVerifiableCredentialArgs, - StatusListDetails, StatusListResult, + StatusOAuth, UpdateStatusListFromEncodedListArgs, UpdateStatusListFromStatusListCredentialArgs, } from './types' @@ -20,8 +21,7 @@ export async function fetchStatusListCredential(args: { statusListCredential: st try { const response = await fetch(url) if (!response.ok) { - const error = `Fetching status list ${url} resulted in an error: ${response.status} : ${response.statusText}` - throw Error(error) + throw Error(`Fetching status list ${url} resulted in an error: ${response.status} : ${response.statusText}`) } const responseAsText = await response.text() if (responseAsText.trim().startsWith('{')) { @@ -29,7 +29,7 @@ export async function fetchStatusListCredential(args: { statusListCredential: st } return responseAsText as OriginalVerifiableCredential } catch (error) { - console.log(`Fetching status list ${url} resulted in an unexpected error: ${error instanceof Error ? error.message : JSON.stringify(error)}`) + console.error(`Fetching status list ${url} resulted in an unexpected error: ${error instanceof Error ? error.message : JSON.stringify(error)}`) throw error } } @@ -126,7 +126,7 @@ export async function simpleCheckStatusFromStatusListUrl(args: { type?: StatusListType | 'StatusList2021Entry' id?: string statusListIndex: string -}): Promise { +}): Promise { return checkStatusIndexFromStatusListCredential({ ...args, statusListCredential: await fetchStatusListCredential(args), @@ -139,7 +139,7 @@ export async function checkStatusIndexFromStatusListCredential(args: { type?: StatusListType | 'StatusList2021Entry' id?: string statusListIndex: string | number -}): Promise { +}): Promise { const requestedType = getAssertedStatusListType(args.type?.replace('Entry', '') as StatusListType) const implementation = getStatusListImplementation(requestedType) return implementation.checkStatusIndex(args) @@ -157,7 +157,7 @@ export async function createNewStatusList( export async function updateStatusIndexFromStatusListCredential( args: UpdateStatusListFromStatusListCredentialArgs, context: IAgentContext, -): Promise { +): Promise { const credential = getAssertedValue('statusListCredential', args.statusListCredential) const uniform = CredentialMapper.toUniformCredential(credential) const type = uniform.type.find((t) => t.includes('StatusList2021') || t.includes('OAuth2StatusList')) @@ -174,7 +174,7 @@ export async function statusListCredentialToDetails(args: { statusListCredential: OriginalVerifiableCredential correlationId?: string driverType?: StatusListDriverType -}): Promise { +}): Promise { const credential = getAssertedValue('statusListCredential', args.statusListCredential) const uniform = CredentialMapper.toUniformCredential(credential) const type = uniform.type.find((t) => t.includes('StatusList2021') || t.includes('OAuth2StatusList')) @@ -196,7 +196,7 @@ export async function statusListCredentialToDetails(args: { export async function updateStatusListIndexFromEncodedList( args: UpdateStatusListFromEncodedListArgs, context: IAgentContext, -): Promise { +): Promise { const { type } = getAssertedValue('type', args) const implementation = getStatusListImplementation(type!) return implementation.updateStatusListFromEncodedList(args, context) diff --git a/packages/vc-status-list/src/impl/IStatusList.ts b/packages/vc-status-list/src/impl/IStatusList.ts index ead330444..727f90bee 100644 --- a/packages/vc-status-list/src/impl/IStatusList.ts +++ b/packages/vc-status-list/src/impl/IStatusList.ts @@ -1,36 +1,25 @@ -import { IAgentContext, ICredentialPlugin, ProofFormat } from '@veramo/core' +import { IAgentContext, ICredentialPlugin } from '@veramo/core' import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { IIssuer, OriginalVerifiableCredential } from '@sphereon/ssi-types' -import { StatusListDetails, StatusListResult, UpdateStatusListFromEncodedListArgs } from '../types' +import { + CheckStatusIndexArgs, + CreateStatusListArgs, + Status2021, + StatusListResult, + StatusOAuth, + UpdateStatusListFromEncodedListArgs, + UpdateStatusListIndexArgs, +} from '../types' export interface IStatusList { /** * Creates a new status list of the specific type */ - createNewStatusList( - args: { - issuer: string | IIssuer - id: string - proofFormat?: ProofFormat - keyRef?: string - correlationId?: string - length?: number - }, - context: IAgentContext, - ): Promise + createNewStatusList(args: CreateStatusListArgs, context: IAgentContext): Promise /** * Updates a status at the given index in the status list */ - updateStatusListIndex( - args: { - statusListCredential: OriginalVerifiableCredential - keyRef?: string - statusListIndex: number | string - value: boolean - }, - context: IAgentContext, - ): Promise + updateStatusListIndex(args: UpdateStatusListIndexArgs, context: IAgentContext): Promise /** * Updates a status list using a base64 encoded list of statuses @@ -38,10 +27,10 @@ export interface IStatusList { updateStatusListFromEncodedList( args: UpdateStatusListFromEncodedListArgs, context: IAgentContext, - ): Promise + ): Promise /** * Checks the status at a given index in the status list */ - checkStatusIndex(args: { statusListCredential: OriginalVerifiableCredential; statusListIndex: string | number }): Promise + checkStatusIndex(args: CheckStatusIndexArgs): Promise } diff --git a/packages/vc-status-list/src/impl/OAuthStatusList.ts b/packages/vc-status-list/src/impl/OAuthStatusList.ts index 382b25389..4b4ab0b03 100644 --- a/packages/vc-status-list/src/impl/OAuthStatusList.ts +++ b/packages/vc-status-list/src/impl/OAuthStatusList.ts @@ -1,18 +1,24 @@ -import { IAgentContext, ICredentialPlugin, ProofFormat } from '@veramo/core' -import { CompactJWT, IIssuer } from '@sphereon/ssi-types' -import { StatusListDetails, StatusListResult, StatusListType, UpdateStatusListFromEncodedListArgs } from '../types' +import { IAgentContext, ICredentialPlugin } from '@veramo/core' +import { CredentialMapper, StatusListType } from '@sphereon/ssi-types' +import { + CheckStatusIndexArgs, + CreateStatusListArgs, + StatusListResult, + StatusOAuth, + UpdateStatusListFromEncodedListArgs, + UpdateStatusListIndexArgs, +} from '../types' import { decodeStatusListJWT, getAssertedValue, getAssertedValues } from '../utils' import { IStatusList } from './IStatusList' import { createHeaderAndPayload, StatusList, StatusListJWTHeaderParameters, StatusListJWTPayload } from '@sd-jwt/jwt-status-list' import { JWTPayload } from 'did-jwt' import { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service' import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { BitsPerStatus } from '@sd-jwt/jwt-status-list/dist' type IRequiredContext = IAgentContext -export const BITS_PER_STATUS_DEFAULT = 1 -export const DEFAULT_LIST_LENGTH = 65536 +export const BITS_PER_STATUS_DEFAULT = 2 // 2 bits are sufficient for 0x00 - "VALID" 0x01 - "INVALID" & 0x02 - "SUSPENDED" +export const DEFAULT_LIST_LENGTH = 250000 export const DEFAULT_PROOF_FORMAT = 'jwt' as const export const STATUS_LIST_JWT_HEADER: StatusListJWTHeaderParameters = { alg: 'EdDSA', @@ -20,19 +26,11 @@ export const STATUS_LIST_JWT_HEADER: StatusListJWTHeaderParameters = { } export class OAuthStatusListImplementation implements IStatusList { - async createNewStatusList( - args: { - issuer: string | IIssuer - id: string - proofFormat?: ProofFormat - keyRef?: string - correlationId?: string - expiresAt?: string - length?: number - bitsPerStatus?: BitsPerStatus - }, - context: IRequiredContext, - ): Promise { + async createNewStatusList(args: CreateStatusListArgs, context: IRequiredContext): Promise { + if (!args.oauthStatusList) { + throw new Error('OAuthStatusList options are required for type OAuthStatusList') + } + const proofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT if (proofFormat !== DEFAULT_PROOF_FORMAT) { throw new Error(`Invalid proof format '${proofFormat}' for OAuthStatusList`) @@ -40,7 +38,7 @@ export class OAuthStatusListImplementation implements IStatusList { const { issuer, id } = args const length = args.length ?? DEFAULT_LIST_LENGTH - const bitsPerStatus = args.bitsPerStatus ?? BITS_PER_STATUS_DEFAULT + const bitsPerStatus = args.oauthStatusList.bitsPerStatus ?? BITS_PER_STATUS_DEFAULT const issuerString = typeof issuer === 'string' ? issuer : issuer.id const correlationId = getAssertedValue('correlationId', args.correlationId) @@ -52,27 +50,23 @@ export class OAuthStatusListImplementation implements IStatusList { return { encodedList, statusListCredential: jwt, + oauthStatusList: { + bitsPerStatus, + }, length, type: StatusListType.OAuthStatusList, proofFormat, id, correlationId, issuer, - statusPurpose: 'active', - indexingDirection: 'rightToLeft', } } - async updateStatusListIndex( - args: { - statusListCredential: CompactJWT - keyRef?: string - statusListIndex: number | string - value: boolean - }, - context: IRequiredContext, - ): Promise { + async updateStatusListIndex(args: UpdateStatusListIndexArgs, context: IRequiredContext): Promise { const { statusListCredential, value } = args + if (!CredentialMapper.isJwtEncoded(statusListCredential) && !CredentialMapper.isMsoMdocOid4VPEncoded(statusListCredential)) { + return Promise.reject(new Error('statusListCredential is neither a JWT nor an MDOC document')) + } const sourcePayload = decodeStatusListJWT(statusListCredential) if (!('iss' in sourcePayload)) { throw new Error('issuer (iss) is missing in the status list JWT') @@ -96,22 +90,22 @@ export class OAuthStatusListImplementation implements IStatusList { return { encodedList, statusListCredential: jwt, + oauthStatusList: { + bitsPerStatus: statusListContainer.bits, + }, length: statusList.statusList.length, type: StatusListType.OAuthStatusList, proofFormat: DEFAULT_PROOF_FORMAT, id, issuer, - statusPurpose: 'active', - indexingDirection: 'rightToLeft', } } - async updateStatusListFromEncodedList(args: UpdateStatusListFromEncodedListArgs, context: IRequiredContext): Promise { + async updateStatusListFromEncodedList(args: UpdateStatusListFromEncodedListArgs, context: IRequiredContext): Promise { if (!args.oauthStatusList) { throw new Error('OAuthStatusList options are required for type OAuthStatusList') } - const { statusPurpose } = args.oauthStatusList const { issuer, id } = getAssertedValues(args) const bitsPerStatus = args.oauthStatusList.bitsPerStatus ?? BITS_PER_STATUS_DEFAULT const issuerString = typeof issuer === 'string' ? issuer : issuer.id @@ -125,26 +119,33 @@ export class OAuthStatusListImplementation implements IStatusList { return { encodedList, statusListCredential: jwt, + oauthStatusList: { + bitsPerStatus, + }, length: listToUpdate.statusList.length, type: StatusListType.OAuthStatusList, proofFormat: args.proofFormat ?? DEFAULT_PROOF_FORMAT, id, issuer, - statusPurpose, - indexingDirection: 'rightToLeft', } } - async checkStatusIndex(args: { statusListCredential: CompactJWT; statusListIndex: string | number }): Promise { + async checkStatusIndex(args: CheckStatusIndexArgs): Promise { const { statusListCredential, statusListIndex } = args - const statusList = StatusList.decompressStatusList(statusListCredential, BITS_PER_STATUS_DEFAULT) + if (!CredentialMapper.isJwtEncoded(statusListCredential) && !CredentialMapper.isMsoMdocOid4VPEncoded(statusListCredential)) { + return Promise.reject(new Error('statusListCredential is neither a JWT nor an MDOC document')) + } + const sourcePayload = decodeStatusListJWT(statusListCredential) + const statusListContainer = sourcePayload.status_list + const statusList = StatusList.decompressStatusList(statusListContainer.lst, statusListContainer.bits) + 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) === 1 + return statusList.getStatus(index) } private async createSignedPayload(context: IRequiredContext, statusList: StatusList, issuerString: string, id: string, keyRef?: string) { diff --git a/packages/vc-status-list/src/impl/StatusList2021.ts b/packages/vc-status-list/src/impl/StatusList2021.ts index 9692358aa..529bde2bd 100644 --- a/packages/vc-status-list/src/impl/StatusList2021.ts +++ b/packages/vc-status-list/src/impl/StatusList2021.ts @@ -3,22 +3,24 @@ import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resoluti import { CredentialMapper, DocumentFormat, IIssuer, OriginalVerifiableCredential, StatusListType } from '@sphereon/ssi-types' import { StatusList } from '@sphereon/vc-status-list' import { IStatusList } from './IStatusList' -import { StatusListDetails, StatusListResult, UpdateStatusListFromEncodedListArgs } from '../types' -import { getAssertedValue, getAssertedValues } from '../utils' +import { + CheckStatusIndexArgs, + CreateStatusListArgs, + Status2021, + StatusListResult, + UpdateStatusListFromEncodedListArgs, + UpdateStatusListIndexArgs, +} from '../types' +import { getAssertedProperty, getAssertedValue, getAssertedValues } from '../utils' + +export const DEFAULT_LIST_LENGTH = 250000 export class StatusList2021Implementation implements IStatusList { async createNewStatusList( - args: { - issuer: string | IIssuer - id: string - proofFormat?: ProofFormat - keyRef?: string - correlationId?: string - length?: number - }, + args: CreateStatusListArgs, context: IAgentContext, ): Promise { - const length = args?.length ?? 250000 + const length = args?.length ?? DEFAULT_LIST_LENGTH const proofFormat: ProofFormat = args?.proofFormat ?? 'lds' const { issuer, id } = args const correlationId = getAssertedValue('correlationId', args.correlationId) @@ -39,32 +41,28 @@ export class StatusList2021Implementation implements IStatusList { return { encodedList, statusListCredential: statusListCredential, + statusList2021: { + statusPurpose, + indexingDirection: 'rightToLeft', + }, length, type: StatusListType.StatusList2021, proofFormat, id, correlationId, issuer, - statusPurpose, - indexingDirection: 'rightToLeft', } } async updateStatusListIndex( - args: { - statusListCredential: OriginalVerifiableCredential - keyRef?: string - statusListIndex: number | string - value: boolean - }, + args: UpdateStatusListIndexArgs, context: IAgentContext, - ): Promise { + ): Promise { const credential = args.statusListCredential const uniform = CredentialMapper.toUniformCredential(credential) const { issuer, credentialSubject } = uniform const id = getAssertedValue('id', uniform.id) - // @ts-ignore - const origEncodedList = credentialSubject.encodedList + const origEncodedList = getAssertedProperty('encodedList', credentialSubject) const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) const statusList = await StatusList.decode({ encodedList: origEncodedList }) @@ -83,23 +81,24 @@ export class StatusList2021Implementation implements IStatusList { ) return { - encodedList, statusListCredential: updatedCredential, + encodedList, + statusList2021: { + ...('statusPurpose' in credentialSubject ? { statusPurpose: credentialSubject.statusPurpose } : {}), + indexingDirection: 'rightToLeft', + }, length: statusList.length - 1, type: StatusListType.StatusList2021, proofFormat: CredentialMapper.detectDocumentType(credential) === DocumentFormat.JWT ? 'jwt' : 'lds', id, issuer, - // @ts-ignore - statusPurpose: credentialSubject.statusPurpose, - indexingDirection: 'rightToLeft', } } async updateStatusListFromEncodedList( args: UpdateStatusListFromEncodedListArgs, context: IAgentContext, - ): Promise { + ): Promise { if (!args.statusList2021) { throw new Error('statusList2021 options required for type StatusList2021') } @@ -122,27 +121,28 @@ export class StatusList2021Implementation implements IStatusList { ) return { - encodedList: newEncodedList, + type: StatusListType.StatusList2021, statusListCredential: credential, + encodedList: newEncodedList, + statusList2021: { + statusPurpose: args.statusList2021.statusPurpose, + indexingDirection: 'rightToLeft', + }, length: statusList.length, - type: StatusListType.StatusList2021, proofFormat: args.proofFormat ?? 'lds', id: id, issuer: issuer, - statusPurpose: 'revocation', - indexingDirection: 'rightToLeft', } } - async checkStatusIndex(args: { statusListCredential: OriginalVerifiableCredential; statusListIndex: string | number }): Promise { + async checkStatusIndex(args: CheckStatusIndexArgs): Promise { const uniform = CredentialMapper.toUniformCredential(args.statusListCredential) const { credentialSubject } = uniform - // @ts-ignore - const encodedList = getAssertedValue('encodedList', credentialSubject.encodedList) + 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 + return status ? Status2021.Invalid : Status2021.Valid } private async createVerifiableCredential( diff --git a/packages/vc-status-list/src/impl/StatusListFactory.ts b/packages/vc-status-list/src/impl/StatusListFactory.ts index be25a9673..b1379f3b9 100644 --- a/packages/vc-status-list/src/impl/StatusListFactory.ts +++ b/packages/vc-status-list/src/impl/StatusListFactory.ts @@ -1,7 +1,7 @@ import { IStatusList } from './IStatusList' import { StatusList2021Implementation } from './StatusList2021' import { OAuthStatusListImplementation } from './OAuthStatusList' -import { StatusListType } from '../types' +import { StatusListType } from '@sphereon/ssi-types' export class StatusListFactory { private static instance: StatusListFactory diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index cb93671c2..b37532b03 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -10,6 +10,7 @@ import { StatusListCredentialIdMode, StatusListDriverType, StatusListIndexingDirection, + StatusListType, StatusPurpose2021, } from '@sphereon/ssi-types' import { @@ -24,22 +25,25 @@ import { import { DataSource } from 'typeorm' import { BitsPerStatus } from '@sd-jwt/jwt-status-list/dist' -export enum StatusListType { - StatusList2021 = 'StatusList2021', - OAuthStatusList = 'OAuthStatusList', +export enum StatusOAuth { + Valid = 0, + Invalid = 1, + Suspended = 2, } -export type StatusOAuth = 'active' | 'suspended' | 'revoked' | string +export enum Status2021 { + Valid = 0, + Invalid = 1, +} export type StatusList2021Args = { - encodedList: string indexingDirection: StatusListIndexingDirection statusPurpose?: StatusPurpose2021 // todo: validFrom and validUntil } export type OAuthStatusListArgs = { - statusPurpose: StatusOAuth + bitsPerStatus?: BitsPerStatus expiresAt?: string } @@ -49,7 +53,6 @@ export type BaseCreateNewStatusListArgs = { issuer: string | IIssuer correlationId?: string length?: number - bitsPerStatus?: BitsPerStatus proofFormat?: ProofFormat keyRef?: string statusList2021?: StatusList2021Args @@ -62,7 +65,6 @@ export type UpdateStatusList2021Args = { export type UpdateOAuthStatusListArgs = { bitsPerStatus: BitsPerStatus - statusPurpose: StatusOAuth expiresAt?: string } @@ -87,24 +89,30 @@ export interface UpdateStatusListFromStatusListCredentialArgs { value: boolean } -export interface StatusListDetails { +export interface StatusListResult { encodedList: string + statusListCredential: OriginalVerifiableCredential | CompactJWT length: number type: StatusListType proofFormat: ProofFormat - statusPurpose?: StatusPurpose2021 | StatusOAuth id: string issuer: string | IIssuer - indexingDirection: StatusListIndexingDirection - statusListCredential: OriginalVerifiableCredential + statusList2021?: StatusList2021Details + oauthStatusList?: OAuthStatusDetails + // These cannot be deduced from the VC, so they are present when callers pass in these values as params correlationId?: string driverType?: StatusListDriverType credentialIdMode?: StatusListCredentialIdMode } -export interface StatusListResult extends StatusListDetails { - statusListCredential: OriginalVerifiableCredential +interface StatusList2021Details { + indexingDirection: StatusListIndexingDirection + statusPurpose?: StatusPurpose2021 +} + +interface OAuthStatusDetails { + bitsPerStatus?: BitsPerStatus } export interface StatusList2021EntryCredentialStatus extends ICredentialStatus { @@ -114,6 +122,14 @@ export interface StatusList2021EntryCredentialStatus extends ICredentialStatus { statusListCredential: string } +export interface StatusListOAuthEntryCredentialStatus extends ICredentialStatus { + type: 'OAuthStatusListEntry' + bitsPerStatus: number + statusListIndex: string + statusListCredential: string + expiresAt?: string +} + export interface StatusList2021ToVerifiableCredentialArgs { issuer: string | IIssuer id: string @@ -124,6 +140,29 @@ export interface StatusList2021ToVerifiableCredentialArgs { statusPurpose: StatusPurpose2021 } +export interface CreateStatusListArgs { + issuer: string | IIssuer + id: string + proofFormat?: ProofFormat + keyRef?: string + correlationId?: string + length?: number + statusList2021?: StatusList2021Args + oauthStatusList?: OAuthStatusListArgs +} + +export interface UpdateStatusListIndexArgs { + statusListCredential: OriginalVerifiableCredential | CompactJWT + keyRef?: string + statusListIndex: number | string + value: boolean +} + +export interface CheckStatusIndexArgs { + statusListCredential: OriginalVerifiableCredential | CompactJWT + statusListIndex: string | number +} + /** * The interface definition for a plugin that can add statuslist info to a credential * @@ -140,7 +179,7 @@ export interface IStatusListPlugin extends IPluginMethodMap { * * @returns - The details of the newly created status list */ - slCreateStatusList(args: CreateNewStatusListArgs, context: IRequiredContext): Promise + slCreateStatusList(args: CreateNewStatusListArgs, context: IRequiredContext): Promise /** * Ensures status list info like index and list id is added to a credential @@ -159,7 +198,7 @@ export interface IStatusListPlugin extends IPluginMethodMap { * @param args * @param context */ - slGetStatusList(args: GetStatusListArgs, context: IRequiredContext): Promise + slGetStatusList(args: GetStatusListArgs, context: IRequiredContext): Promise } export type CreateNewStatusListFuncArgs = BaseCreateNewStatusListArgs diff --git a/packages/vc-status-list/src/utils.ts b/packages/vc-status-list/src/utils.ts index 2682e8c6e..a9d856f02 100644 --- a/packages/vc-status-list/src/utils.ts +++ b/packages/vc-status-list/src/utils.ts @@ -1,16 +1,15 @@ -import { CompactJWT, IIssuer, StatusListType as StatusListTypeW3C } from '@sphereon/ssi-types' -import { StatusListType } from './types' +import { CompactJWT, IIssuer, StatusListType, StatusListType as StatusListTypeW3C } from '@sphereon/ssi-types' import { StatusListJWTPayload } from '@sd-jwt/jwt-status-list' -import { decodeBase64url } from 'did-jwt/lib/util' +import base64url from 'base64url' export function decodeStatusListJWT(jwt: CompactJWT): StatusListJWTPayload { const parts = jwt.split('.') - return JSON.parse(decodeBase64url(parts[1])) + return JSON.parse(base64url.decode(parts[1])) } export function getAssertedStatusListType(type?: StatusListType) { const assertedType = type ?? StatusListType.StatusList2021 - if (assertedType !== StatusListType.StatusList2021) { + if (![StatusListType.StatusList2021, StatusListType.OAuthStatusList].includes(assertedType)) { throw Error(`StatusList type ${assertedType} is not supported (yet)`) } return assertedType @@ -29,3 +28,10 @@ export function getAssertedValues(args: { issuer: string | IIssuer; id: string; const issuer = getAssertedValue('issuer', args.issuer) return { id, issuer, type } } + +export function getAssertedProperty(propertyName: string, obj: T): NonNullable { + if (!(propertyName in obj)) { + throw Error(`The input object does not contain required property: ${propertyName}`) + } + return getAssertedValue(propertyName, (obj as any)[propertyName]) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd4c13489..02bafbc0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3245,6 +3245,9 @@ importers: '@veramo/credential-status': specifier: 4.2.0 version: 4.2.0(encoding@0.1.13) + base64url: + specifier: ^3.0.1 + version: 3.0.1 credential-status: specifier: ^2.0.6 version: 2.0.6 @@ -3270,6 +3273,9 @@ importers: '@babel/preset-typescript': specifier: ^7.24.7 version: 7.26.0(@babel/core@7.26.0) + '@veramo/key-manager': + specifier: 4.2.0 + version: 4.2.0 typescript: specifier: 5.6.3 version: 5.6.3 @@ -3470,6 +3476,9 @@ importers: '@sphereon/ssi-sdk-ext.identifier-resolution': specifier: 0.27.0 version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) + '@sphereon/ssi-sdk-ext.jwt-service': + specifier: 0.27.0 + version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) '@sphereon/ssi-sdk.core': specifier: workspace:* version: link:../ssi-sdk-core @@ -3595,6 +3604,70 @@ importers: specifier: 5.6.3 version: 5.6.3 + packages/vc-status-list-tests: + dependencies: + react-native-securerandom: + specifier: ^1.0.1 + version: 1.0.1(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(encoding@0.1.13)(react@18.3.1)) + devDependencies: + '@sphereon/ssi-sdk-ext.did-provider-key': + specifier: 0.27.0 + version: 0.27.0(encoding@0.1.13)(expo@52.0.11(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(encoding@0.1.13)(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + '@sphereon/ssi-sdk-ext.identifier-resolution': + specifier: 0.27.0 + version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) + '@sphereon/ssi-sdk-ext.jwt-service': + specifier: 0.27.0 + version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) + '@sphereon/ssi-sdk-ext.key-manager': + specifier: 0.27.0 + version: 0.27.0 + '@sphereon/ssi-sdk-ext.kms-local': + specifier: 0.27.0 + version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) + '@sphereon/ssi-sdk.vc-handler-ld-local': + specifier: workspace:* + version: link:../vc-handler-ld-local + '@sphereon/ssi-sdk.vc-status-list': + specifier: workspace:* + version: link:../vc-status-list + '@sphereon/ssi-types': + specifier: workspace:* + version: link:../ssi-types + '@types/jest': + specifier: ^27.5.2 + version: 27.5.2 + '@types/node': + specifier: ^20.17.1 + version: 20.17.9 + '@veramo/core': + specifier: 4.2.0 + version: 4.2.0(patch_hash=c5oempznsz4br5w3tcuk2i2mau) + '@veramo/credential-w3c': + specifier: 4.2.0 + version: 4.2.0(patch_hash=wuhizuafnrz3uzah2wlqaevbmi)(encoding@0.1.13)(expo@52.0.11(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(encoding@0.1.13)(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + '@veramo/did-manager': + specifier: 4.2.0 + version: 4.2.0 + '@veramo/did-resolver': + specifier: 4.2.0 + version: 4.2.0(encoding@0.1.13) + '@veramo/key-manager': + specifier: 4.2.0 + version: 4.2.0 + did-resolver: + specifier: ^4.1.0 + version: 4.1.0 + nock: + specifier: ^13.5.4 + version: 13.5.6 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.17.9)(typescript@5.6.3) + typescript: + specifier: 5.6.3 + version: 5.6.3 + packages/w3c-vc-api: dependencies: '@sphereon/did-auth-siop': @@ -18655,7 +18728,7 @@ snapshots: '@types/testing-library__jest-dom@5.14.9': dependencies: - '@types/jest': 27.5.2 + '@types/jest': 29.5.14 '@types/through@0.0.33': dependencies: From 227bf10767879325f717898b8d33c5210b6e2981 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 15 Jan 2025 18:23:16 +0100 Subject: [PATCH 08/24] chore: saving work --- packages/vc-status-list-issuer-rest-api/src/api-functions.ts | 2 +- packages/vc-status-list/src/functions.ts | 3 ++- packages/vc-status-list/src/impl/OAuthStatusList.ts | 2 +- packages/vc-status-list/src/impl/StatusList2021.ts | 2 +- packages/vc-status-list/src/types/index.ts | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/vc-status-list-issuer-rest-api/src/api-functions.ts b/packages/vc-status-list-issuer-rest-api/src/api-functions.ts index 20528dd59..9136ec507 100644 --- a/packages/vc-status-list-issuer-rest-api/src/api-functions.ts +++ b/packages/vc-status-list-issuer-rest-api/src/api-functions.ts @@ -178,7 +178,7 @@ export function updateW3CStatusEndpoint(router: Router, context: IRequiredContex `Required 'status' value was missing in the credentialStatus array for credentialId ${credentialId}`, ) } - const value = updateItem.status === '0' || updateItem.status.toLowerCase() === 'false' ? false : true + const value = updateItem.status === '0' || updateItem.status.toLowerCase() === 'false' ? 0 : 1 const statusList = statusListId ?? statusListEntry.statusList await driver.updateStatusListEntry({ ...statusListEntry, statusListIndex, statusList, credentialId, value: value ? '1' : '0' }) diff --git a/packages/vc-status-list/src/functions.ts b/packages/vc-status-list/src/functions.ts index b38646bb3..0e6a177c2 100644 --- a/packages/vc-status-list/src/functions.ts +++ b/packages/vc-status-list/src/functions.ts @@ -159,7 +159,8 @@ export async function updateStatusIndexFromStatusListCredential( context: IAgentContext, ): Promise { const credential = getAssertedValue('statusListCredential', args.statusListCredential) - const uniform = CredentialMapper.toUniformCredential(credential) + + const uniform = CredentialMapper.toUniformCredential(credential) // This is not correct, we can't run a OAuthSTatusList through CredentialMapper and we can't see the type const type = uniform.type.find((t) => t.includes('StatusList2021') || t.includes('OAuth2StatusList')) if (!type) { throw new Error('Invalid status list credential type') diff --git a/packages/vc-status-list/src/impl/OAuthStatusList.ts b/packages/vc-status-list/src/impl/OAuthStatusList.ts index 4b4ab0b03..c02fdc1b7 100644 --- a/packages/vc-status-list/src/impl/OAuthStatusList.ts +++ b/packages/vc-status-list/src/impl/OAuthStatusList.ts @@ -84,7 +84,7 @@ export class OAuthStatusListImplementation implements IStatusList { throw new Error('Status list index out of bounds') } - statusList.setStatus(index, value ? 1 : 0) + statusList.setStatus(index, value) const { jwt, encodedList } = await this.createSignedPayload(context, statusList, issuer, id, args.keyRef) return { diff --git a/packages/vc-status-list/src/impl/StatusList2021.ts b/packages/vc-status-list/src/impl/StatusList2021.ts index 529bde2bd..9487245b0 100644 --- a/packages/vc-status-list/src/impl/StatusList2021.ts +++ b/packages/vc-status-list/src/impl/StatusList2021.ts @@ -66,7 +66,7 @@ export class StatusList2021Implementation implements IStatusList { const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) const statusList = await StatusList.decode({ encodedList: origEncodedList }) - statusList.setStatus(index, args.value) + statusList.setStatus(index, args.value != 0) const encodedList = await statusList.encode() const updatedCredential = await this.createVerifiableCredential( diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index b37532b03..4456aa82c 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -86,7 +86,7 @@ export interface UpdateStatusListFromStatusListCredentialArgs { statusListCredential: OriginalVerifiableCredential | CompactJWT keyRef?: string statusListIndex: number | string - value: boolean + value: number | Status2021 | StatusOAuth } export interface StatusListResult { @@ -155,7 +155,7 @@ export interface UpdateStatusListIndexArgs { statusListCredential: OriginalVerifiableCredential | CompactJWT keyRef?: string statusListIndex: number | string - value: boolean + value: number | Status2021 | StatusOAuth } export interface CheckStatusIndexArgs { From 10b3d156b480de5172a0cf83c155983f982f645d Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Thu, 16 Jan 2025 15:59:11 +0100 Subject: [PATCH 09/24] chore: tests passing --- .../src/__tests__/statusList.entities.test.ts | 215 ++++++++++++++++ .../src/__tests__/statusList.store.test.ts | 232 ++++++++++++++++++ .../entities/statusList/StatusListEntities.ts | 23 +- packages/data-store/src/index.ts | 4 +- .../1693866470001-CreateStatusList.ts | 55 ++++- .../sqlite/1693866470000-CreateStatusList.ts | 52 +++- .../src/statusList/StatusListStore.ts | 93 ++++--- .../statusList/IAbstractStatusListStore.ts | 4 +- .../src/types/statusList/statusList.ts | 4 +- .../src/utils/statusList/MappingUtils.ts | 84 +++++++ packages/ssi-types/src/types/status-list.ts | 5 + packages/tsconfig.json | 4 +- .../src/__tests__/statuslist.test.ts | 148 ----------- .../src/drivers.ts | 18 +- .../src/types.ts | 6 +- packages/vc-status-list-tests/README.md | 43 ++++ .../__tests__/statuslist.test.ts | 144 ++++++++++- packages/vc-status-list/package.json | 1 + packages/vc-status-list/src/functions.ts | 62 +++-- .../vc-status-list/src/impl/StatusList2021.ts | 7 +- packages/vc-status-list/src/types/index.ts | 11 +- pnpm-lock.yaml | 3 + 22 files changed, 938 insertions(+), 280 deletions(-) create mode 100644 packages/data-store/src/__tests__/statusList.entities.test.ts create mode 100644 packages/data-store/src/__tests__/statusList.store.test.ts create mode 100644 packages/data-store/src/utils/statusList/MappingUtils.ts delete mode 100644 packages/vc-handler-ld-local/src/__tests__/statuslist.test.ts create mode 100644 packages/vc-status-list-tests/README.md diff --git a/packages/data-store/src/__tests__/statusList.entities.test.ts b/packages/data-store/src/__tests__/statusList.entities.test.ts new file mode 100644 index 000000000..7296c575c --- /dev/null +++ b/packages/data-store/src/__tests__/statusList.entities.test.ts @@ -0,0 +1,215 @@ +import { DataSource } from 'typeorm' +import { DataSources } from '@sphereon/ssi-sdk.agent-config' +import { DataStoreStatusListEntities, StatusListEntryEntity } from '../index' +import { DataStoreStatusListMigrations } from '../migrations' +import { OAuthStatusListEntity, StatusList2021Entity } from '../entities/statusList/StatusListEntities' +import { StatusListCredentialIdMode, StatusListDriverType } from '@sphereon/ssi-types' + +describe('Status list entities tests', () => { + let dbConnection: DataSource + + beforeEach(async () => { + DataSources.singleInstance().defaultDbType = 'sqlite' + dbConnection = await new DataSource({ + type: 'sqlite', + database: ':memory:', + migrationsRun: false, + migrations: DataStoreStatusListMigrations, + synchronize: false, + entities: [...DataStoreStatusListEntities], + }).initialize() + await dbConnection.runMigrations() + expect(await dbConnection.showMigrations()).toBeFalsy() + }) + + afterEach(async () => { + await dbConnection.destroy() + }) + + it('should save status list to database', async () => { + const statusList = new StatusList2021Entity() + statusList.id = 'test-list-1' + statusList.correlationId = 'correlation-1' + statusList.driverType = StatusListDriverType.AGENT_TYPEORM + statusList.length = 100000 + statusList.credentialIdMode = StatusListCredentialIdMode.ISSUANCE + statusList.proofFormat = 'jwt' + statusList.statusPurpose = 'revocation' + statusList.indexingDirection = 'rightToLeft' + statusList.issuer = 'did:example:123' + + const fromDb = await dbConnection.getRepository(StatusList2021Entity).save(statusList) + expect(fromDb).toBeDefined() + expect(fromDb.id).toEqual(statusList.id) + expect(fromDb.correlationId).toEqual(statusList.correlationId) + expect(fromDb.length).toEqual(statusList.length) + expect(fromDb.credentialIdMode).toEqual(statusList.credentialIdMode) + expect(fromDb.statusPurpose).toEqual(statusList.statusPurpose) + expect(fromDb.indexingDirection).toEqual(statusList.indexingDirection) + expect(fromDb.issuer).toEqual(statusList.issuer) + }) + + it('should save status list entry to database', async () => { + const statusList = new StatusList2021Entity() + statusList.id = 'test-list-1' + statusList.correlationId = 'correlation-1' + statusList.driverType = StatusListDriverType.AGENT_TYPEORM + statusList.length = 100000 + statusList.credentialIdMode = StatusListCredentialIdMode.ISSUANCE + statusList.proofFormat = 'jwt' + statusList.statusPurpose = 'revocation' + statusList.indexingDirection = 'rightToLeft' + statusList.issuer = 'did:example:123' + + await dbConnection.getRepository(StatusList2021Entity).save(statusList) + + const entry = new StatusListEntryEntity() + entry.statusList = statusList + entry.statusListIndex = 1 + entry.credentialId = 'credential-1' + entry.credentialHash = 'hash-1' + entry.correlationId = 'correlation-1' + entry.value = '1' + + const fromDb = await dbConnection.getRepository(StatusListEntryEntity).save(entry) + expect(fromDb).toBeDefined() + expect(fromDb.statusListIndex).toEqual(entry.statusListIndex) + expect(fromDb.credentialId).toEqual(entry.credentialId) + expect(fromDb.credentialHash).toEqual(entry.credentialHash) + expect(fromDb.correlationId).toEqual(entry.correlationId) + expect(fromDb.value).toEqual(entry.value) + }) + + it('should handle complex issuer object', async () => { + const statusList = new StatusList2021Entity() + statusList.id = 'test-list-1' + statusList.correlationId = 'correlation-1' + statusList.driverType = StatusListDriverType.AGENT_TYPEORM + statusList.length = 100000 + statusList.credentialIdMode = StatusListCredentialIdMode.ISSUANCE + statusList.proofFormat = 'jwt' + statusList.statusPurpose = 'revocation' + statusList.indexingDirection = 'rightToLeft' + statusList.issuer = { id: 'did:example:123', name: 'Test Issuer' } + + const fromDb = await dbConnection.getRepository(StatusList2021Entity).save(statusList) + expect(fromDb).toBeDefined() + expect(fromDb.issuer).toEqual(statusList.issuer) + expect(typeof fromDb.issuer).toEqual('object') + expect((fromDb.issuer as any).id).toEqual('did:example:123') + expect((fromDb.issuer as any).name).toEqual('Test Issuer') + }) + + it('should save OAuth status list to database', async () => { + const statusList = new OAuthStatusListEntity() + statusList.id = 'oauth-list-1' + statusList.correlationId = 'correlation-oauth-1' + statusList.driverType = StatusListDriverType.AGENT_TYPEORM + statusList.length = 100000 + statusList.credentialIdMode = StatusListCredentialIdMode.ISSUANCE + statusList.proofFormat = 'jwt' + statusList.bitsPerStatus = 1 + statusList.expiresAt = '2025-01-01T00:00:00Z' + statusList.issuer = 'did:example:123' + + const fromDb = await dbConnection.getRepository(OAuthStatusListEntity).save(statusList) + expect(fromDb).toBeDefined() + expect(fromDb.id).toEqual(statusList.id) + expect(fromDb.correlationId).toEqual(statusList.correlationId) + expect(fromDb.length).toEqual(statusList.length) + expect(fromDb.credentialIdMode).toEqual(statusList.credentialIdMode) + expect(fromDb.bitsPerStatus).toEqual(statusList.bitsPerStatus) + expect(fromDb.expiresAt).toEqual(statusList.expiresAt) + expect(fromDb.issuer).toEqual(statusList.issuer) + }) + + it('should handle both status list types having entries', async () => { + const statusList2021 = new StatusList2021Entity() + statusList2021.id = 'test-list-1' + statusList2021.correlationId = 'correlation-1' + statusList2021.driverType = StatusListDriverType.AGENT_TYPEORM + statusList2021.length = 100000 + statusList2021.credentialIdMode = StatusListCredentialIdMode.ISSUANCE + statusList2021.proofFormat = 'jwt' + statusList2021.statusPurpose = 'revocation' + statusList2021.indexingDirection = 'rightToLeft' + statusList2021.issuer = 'did:example:123' + await dbConnection.getRepository(StatusList2021Entity).save(statusList2021) + + const oauthStatusList = new OAuthStatusListEntity() + oauthStatusList.id = 'oauth-list-1' + oauthStatusList.correlationId = 'correlation-oauth-1' + oauthStatusList.driverType = StatusListDriverType.AGENT_TYPEORM + oauthStatusList.length = 100000 + oauthStatusList.credentialIdMode = StatusListCredentialIdMode.ISSUANCE + oauthStatusList.proofFormat = 'jwt' + oauthStatusList.bitsPerStatus = 1 + oauthStatusList.issuer = 'did:example:456' + await dbConnection.getRepository(OAuthStatusListEntity).save(oauthStatusList) + + const entry2021 = new StatusListEntryEntity() + entry2021.statusList = statusList2021 + entry2021.statusListIndex = 1 + entry2021.credentialId = 'credential-1' + entry2021.credentialHash = 'hash-1' + entry2021.value = '1' + await dbConnection.getRepository(StatusListEntryEntity).save(entry2021) + + const entryOAuth = new StatusListEntryEntity() + entryOAuth.statusList = oauthStatusList + entryOAuth.statusListIndex = 1 + entryOAuth.credentialId = 'credential-2' + entryOAuth.credentialHash = 'hash-2' + entryOAuth.value = '1' + await dbConnection.getRepository(StatusListEntryEntity).save(entryOAuth) + + const found2021Entry = await dbConnection.getRepository(StatusListEntryEntity).findOne({ + where: { statusList: statusList2021.id, statusListIndex: 1 }, + }) + const foundOAuthEntry = await dbConnection.getRepository(StatusListEntryEntity).findOne({ + where: { statusList: oauthStatusList.id, statusListIndex: 1 }, + }) + + expect(found2021Entry).toBeDefined() + expect(found2021Entry?.credentialId).toEqual('credential-1') + expect(foundOAuthEntry).toBeDefined() + expect(foundOAuthEntry?.credentialId).toEqual('credential-2') + }) + + it('should cascade delete entries when status list is deleted', async () => { + const statusList = new StatusList2021Entity() + statusList.id = 'test-list-1' + statusList.correlationId = 'correlation-1' + statusList.driverType = StatusListDriverType.AGENT_TYPEORM + statusList.length = 100000 + statusList.credentialIdMode = StatusListCredentialIdMode.ISSUANCE + statusList.proofFormat = 'jwt' + statusList.statusPurpose = 'revocation' + statusList.indexingDirection = 'rightToLeft' + statusList.issuer = 'did:example:123' + + const savedStatusList = await dbConnection.getRepository(StatusList2021Entity).save(statusList) + + const entry = new StatusListEntryEntity() + entry.statusList = statusList + entry.statusListIndex = 1 + entry.credentialId = 'credential-1' + entry.credentialHash = 'hash-1' + entry.correlationId = 'correlation-1' + entry.value = '1' + + await dbConnection.getRepository(StatusListEntryEntity).save(entry) + + // First delete entry, otherwise constraint fails + await dbConnection.getRepository(StatusListEntryEntity).delete({ statusList: savedStatusList.id }) + await dbConnection.getRepository(StatusList2021Entity).remove(savedStatusList) + + const foundEntry = await dbConnection.getRepository(StatusListEntryEntity).findOne({ + where: { + statusList: statusList.id, + statusListIndex: entry.statusListIndex, + }, + }) + expect(foundEntry).toBeNull() + }) +}) diff --git a/packages/data-store/src/__tests__/statusList.store.test.ts b/packages/data-store/src/__tests__/statusList.store.test.ts new file mode 100644 index 000000000..0eaac0f69 --- /dev/null +++ b/packages/data-store/src/__tests__/statusList.store.test.ts @@ -0,0 +1,232 @@ +import { DataSource } from 'typeorm' +import { DataSources } from '@sphereon/ssi-sdk.agent-config' +import { DataStoreStatusListEntities } from '../index' +import { DataStoreStatusListMigrations } from '../migrations' +import { StatusListStore } from '../statusList/StatusListStore' +import { IStatusList2021Entity, IStatusListEntryEntity, IOAuthStatusListEntity } from '../types' +import { StatusListCredentialIdMode, StatusListDriverType, StatusListType } from '@sphereon/ssi-types' + +describe('Status list store tests', () => { + let dbConnection: DataSource + let statusListStore: StatusListStore + + beforeEach(async () => { + DataSources.singleInstance().defaultDbType = 'sqlite' + dbConnection = await new DataSource({ + type: 'sqlite', + database: ':memory:', + migrationsRun: false, + migrations: DataStoreStatusListMigrations, + synchronize: false, + entities: DataStoreStatusListEntities, + }).initialize() + await dbConnection.runMigrations() + expect(await dbConnection.showMigrations()).toBeFalsy() + statusListStore = new StatusListStore(dbConnection) + }) + + afterEach(async () => { + await dbConnection.destroy() + }) + + it('should store status list', async () => { + const statusList: IStatusList2021Entity = { + id: 'test-list-1', + correlationId: 'correlation-1', + driverType: StatusListDriverType.AGENT_TYPEORM, + length: 100000, + credentialIdMode: StatusListCredentialIdMode.ISSUANCE, + type: StatusListType.StatusList2021, + proofFormat: 'jwt', + statusPurpose: 'revocation', + indexingDirection: 'rightToLeft', + issuer: 'did:example:123', + } + + const result = await statusListStore.addStatusList(statusList) + expect(result).toBeDefined() + expect(result.id).toEqual(statusList.id) + expect(result.correlationId).toEqual(statusList.correlationId) + }) + + it('should store status list entry', async () => { + const statusList: IStatusList2021Entity = { + id: 'test-list-1', + correlationId: 'correlation-1', + driverType: StatusListDriverType.AGENT_TYPEORM, + length: 100000, + credentialIdMode: StatusListCredentialIdMode.ISSUANCE, + type: StatusListType.StatusList2021, + proofFormat: 'jwt', + statusPurpose: 'revocation', + indexingDirection: 'rightToLeft', + issuer: 'did:example:123', + } + + await statusListStore.addStatusList(statusList) + + const entry: IStatusListEntryEntity = { + statusList: statusList.id, + statusListIndex: 1, + credentialId: 'credential-1', + credentialHash: 'hash-1', + correlationId: 'correlation-1', + value: '1', + } + + const result = await statusListStore.addStatusListEntry(entry) + expect(result).toBeDefined() + expect(result.statusListIndex).toEqual(entry.statusListIndex) + expect(result.credentialId).toEqual(entry.credentialId) + }) + + it('should store OAuth status list', async () => { + const statusList: IOAuthStatusListEntity = { + id: 'oauth-list-1', + correlationId: 'correlation-oauth-1', + driverType: StatusListDriverType.AGENT_TYPEORM, + length: 100000, + credentialIdMode: StatusListCredentialIdMode.ISSUANCE, + type: StatusListType.OAuthStatusList, + proofFormat: 'jwt', + bitsPerStatus: 1, + expiresAt: '2025-01-01T00:00:00Z', + issuer: 'did:example:123', + } + + const result = (await statusListStore.addStatusList(statusList)) as IOAuthStatusListEntity + expect(result).toBeDefined() + expect(result.id).toEqual(statusList.id) + expect(result.correlationId).toEqual(statusList.correlationId) + expect(result.bitsPerStatus).toEqual(statusList.bitsPerStatus) + expect(result.expiresAt).toEqual(statusList.expiresAt) + }) + + it('should store and retrieve both types of status lists', async () => { + const statusList2021: IStatusList2021Entity = { + id: 'test-list-1', + correlationId: 'correlation-1', + driverType: StatusListDriverType.AGENT_TYPEORM, + length: 100000, + credentialIdMode: StatusListCredentialIdMode.ISSUANCE, + type: StatusListType.StatusList2021, + proofFormat: 'jwt', + statusPurpose: 'revocation', + indexingDirection: 'rightToLeft', + issuer: 'did:example:123', + } + + const oauthStatusList: IOAuthStatusListEntity = { + id: 'oauth-list-1', + correlationId: 'correlation-oauth-1', + driverType: StatusListDriverType.AGENT_TYPEORM, + length: 100000, + credentialIdMode: StatusListCredentialIdMode.ISSUANCE, + type: StatusListType.OAuthStatusList, + proofFormat: 'jwt', + bitsPerStatus: 1, + issuer: 'did:example:456', + } + + await statusListStore.addStatusList(statusList2021) + await statusListStore.addStatusList(oauthStatusList) + + const found2021 = (await statusListStore.getStatusList({ id: statusList2021.id })) as IStatusList2021Entity + const foundOAuth = (await statusListStore.getStatusList({ id: oauthStatusList.id })) as IOAuthStatusListEntity + + expect(found2021.type).toEqual(StatusListType.StatusList2021) + expect(found2021.statusPurpose).toEqual('revocation') + expect(foundOAuth.type).toEqual(StatusListType.OAuthStatusList) + expect((foundOAuth as IOAuthStatusListEntity).bitsPerStatus).toEqual(1) + }) + + it('should get status list by id', async () => { + const statusList: IStatusList2021Entity = { + id: 'test-list-1', + correlationId: 'correlation-1', + driverType: StatusListDriverType.AGENT_TYPEORM, + length: 100000, + credentialIdMode: StatusListCredentialIdMode.ISSUANCE, + type: StatusListType.StatusList2021, + proofFormat: 'jwt', + statusPurpose: 'revocation', + indexingDirection: 'rightToLeft', + issuer: 'did:example:123', + } + + await statusListStore.addStatusList(statusList) + + const result = await statusListStore.getStatusList({ id: statusList.id }) + expect(result).toBeDefined() + expect(result.id).toEqual(statusList.id) + }) + + it('should get status lists with filter', async () => { + const statusList1: IStatusList2021Entity = { + id: 'test-list-1', + correlationId: 'correlation-1', + driverType: StatusListDriverType.AGENT_TYPEORM, + length: 100000, + credentialIdMode: StatusListCredentialIdMode.ISSUANCE, + type: StatusListType.StatusList2021, + proofFormat: 'jwt', + statusPurpose: 'revocation', + indexingDirection: 'rightToLeft', + issuer: 'did:example:123', + } + + const statusList2: IStatusList2021Entity = { + id: 'test-list-2', + correlationId: 'correlation-2', + driverType: StatusListDriverType.AGENT_TYPEORM, + length: 100000, + credentialIdMode: StatusListCredentialIdMode.ISSUANCE, + type: StatusListType.StatusList2021, + proofFormat: 'jwt', + statusPurpose: 'suspension', + indexingDirection: 'rightToLeft', + issuer: 'did:example:456', + } + + await statusListStore.addStatusList(statusList1) + await statusListStore.addStatusList(statusList2) + + const result = await statusListStore.getStatusLists({ + filter: [{ statusPurpose: 'revocation' }], + }) + + expect(result.length).toEqual(1) + expect(result[0].id).toEqual(statusList1.id) + }) + + it('should delete status list', async () => { + const statusList: IStatusList2021Entity = { + id: 'test-list-1', + correlationId: 'correlation-1', + driverType: StatusListDriverType.AGENT_TYPEORM, + length: 100000, + credentialIdMode: StatusListCredentialIdMode.ISSUANCE, + type: StatusListType.StatusList2021, + proofFormat: 'jwt', + statusPurpose: 'revocation', + indexingDirection: 'rightToLeft', + issuer: 'did:example:123', + } + + await statusListStore.addStatusList(statusList) + const entry: IStatusListEntryEntity = { + statusList: statusList.id, + statusListIndex: 1, + credentialId: 'credential-1', + credentialHash: 'hash-1', + correlationId: 'correlation-1', + value: '1', + } + await statusListStore.addStatusListEntry(entry) + + const result = await statusListStore.removeStatusList({ id: statusList.id }) + expect(result).toEqual(true) + + await expect(statusListStore.getStatusList({ id: statusList.id })).rejects.toThrow(`No status list found for id: ${statusList.id}`) + }) +}) diff --git a/packages/data-store/src/entities/statusList/StatusListEntities.ts b/packages/data-store/src/entities/statusList/StatusListEntities.ts index 3a99fc790..e0b3e2ccb 100644 --- a/packages/data-store/src/entities/statusList/StatusListEntities.ts +++ b/packages/data-store/src/entities/statusList/StatusListEntities.ts @@ -1,12 +1,11 @@ import { IIssuer, - JwtDecodedVerifiableCredential, + StatusListVerifiableCredential, StatusListCredentialIdMode, StatusListDriverType, StatusListIndexingDirection, StatusListType, StatusPurpose2021, - W3CVerifiableCredential, } from '@sphereon/ssi-types' import { ProofFormat } from '@veramo/core' import { BaseEntity, ChildEntity, Column, Entity, OneToMany, PrimaryColumn, TableInheritance, Unique } from 'typeorm' @@ -14,7 +13,7 @@ import { StatusListEntryEntity } from './StatusList2021EntryEntity' @Entity('StatusList') @Unique('UQ_correlationId', ['correlationId']) -@TableInheritance({ column: { type: 'varchar', name: 'type' } }) +@TableInheritance({ column: { type: 'simple-enum', name: 'type', enum: StatusListType } }) export abstract class StatusListEntity extends BaseEntity { @PrimaryColumn({ name: 'id', type: 'varchar' }) id!: string @@ -47,14 +46,6 @@ export abstract class StatusListEntity extends BaseEntity { }) issuer!: string | IIssuer - @Column('simple-enum', { - name: 'type', - enum: StatusListType, - nullable: false, - default: StatusListType.StatusList2021, - }) - type!: StatusListType - @Column('simple-enum', { name: 'driverType', enum: StatusListDriverType, @@ -80,13 +71,13 @@ export abstract class StatusListEntity extends BaseEntity { nullable: true, unique: false, transformer: { - from(value: string): W3CVerifiableCredential | JwtDecodedVerifiableCredential { + from(value: string): StatusListVerifiableCredential { if (value?.startsWith('ey')) { return value } return JSON.parse(value) }, - to(value: W3CVerifiableCredential | JwtDecodedVerifiableCredential): string { + to(value: StatusListVerifiableCredential): string { if (typeof value === 'string') { return value } @@ -94,7 +85,7 @@ export abstract class StatusListEntity extends BaseEntity { }, }, }) - statusListCredential?: W3CVerifiableCredential | JwtDecodedVerifiableCredential + statusListCredential?: StatusListVerifiableCredential @OneToMany((type) => StatusListEntryEntity, (entry) => entry.statusList) statusListEntries!: StatusListEntryEntity[] @@ -117,6 +108,8 @@ export class StatusList2021Entity extends StatusListEntity { @ChildEntity(StatusListType.OAuthStatusList) export class OAuthStatusListEntity extends StatusListEntity { - bitsPerStatus: number + @Column({ type: 'integer', name: 'bitsPerStatus', nullable: false }) + bitsPerStatus!: number + @Column({ type: 'varchar', name: 'expiresAt', nullable: true }) expiresAt?: string } diff --git a/packages/data-store/src/index.ts b/packages/data-store/src/index.ts index afd0ddec8..02a7be796 100644 --- a/packages/data-store/src/index.ts +++ b/packages/data-store/src/index.ts @@ -16,7 +16,7 @@ import { ImageDimensionsEntity } from './entities/issuanceBranding/ImageDimensio import { IssuerLocaleBrandingEntity } from './entities/issuanceBranding/IssuerLocaleBrandingEntity' import { IssuerBrandingEntity } from './entities/issuanceBranding/IssuerBrandingEntity' import { TextAttributesEntity } from './entities/issuanceBranding/TextAttributesEntity' -import { StatusListEntity } from './entities/statusList/StatusListEntities' +import { OAuthStatusListEntity, StatusList2021Entity, StatusListEntity } from './entities/statusList/StatusListEntities' import { StatusListEntryEntity } from './entities/statusList/StatusList2021EntryEntity' import { MachineStateInfoEntity } from './entities/machineState/MachineStateInfoEntity' import { IStatusListEntity, IStatusListEntryEntity } from './types' @@ -96,7 +96,7 @@ export const DataStoreIssuanceBrandingEntities = [ export const DataStorePresentationDefinitionEntities = [PresentationDefinitionItemEntity] -export const DataStoreStatusListEntities = [StatusListEntity, StatusListEntryEntity] +export const DataStoreStatusListEntities = [StatusListEntity, StatusList2021Entity, OAuthStatusListEntity, StatusListEntryEntity] export const DataStoreEventLoggerEntities = [AuditEventEntity] diff --git a/packages/data-store/src/migrations/postgres/1693866470001-CreateStatusList.ts b/packages/data-store/src/migrations/postgres/1693866470001-CreateStatusList.ts index 7561304da..b32d20897 100644 --- a/packages/data-store/src/migrations/postgres/1693866470001-CreateStatusList.ts +++ b/packages/data-store/src/migrations/postgres/1693866470001-CreateStatusList.ts @@ -1,24 +1,61 @@ +// noinspection SqlPostgresDialect SqlNoDataSourceInspection import { MigrationInterface, QueryRunner } from 'typeorm' export class CreateStatusList1693866470001 implements MigrationInterface { name = 'CreateStatusList1693866470001' public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "StatusList_type_enum" AS ENUM('StatusList2021', 'OAuthStatusList')`) + await queryRunner.query(`CREATE TYPE "StatusList_drivertype_enum" AS ENUM('agent_typeorm', 'agent_kv_store', 'github', 'agent_filesystem')`) + await queryRunner.query(`CREATE TYPE "StatusList_credentialidmode_enum" AS ENUM('ISSUANCE', 'PERSISTENCE', 'NEVER')`) + await queryRunner.query( - `CREATE TABLE "StatusListEntry" ("statusListId" character varying NOT NULL, "statusListIndex" integer NOT NULL, "credentialId" character varying, "credentialHash" character varying(128), "correlationId" character varying(255), "value" character varying(50), CONSTRAINT "PK_68704d2d13857360c6b44a3d1d0" PRIMARY KEY ("statusListId", "statusListIndex"))`, - ) - await queryRunner.query(`CREATE TYPE "public"."StatusList_type_enum" AS ENUM('StatusList2021')`) - await queryRunner.query( - `CREATE TYPE "public"."StatusList_drivertype_enum" AS ENUM('agent_typeorm', 'agent_kv_store', 'github', 'agent_filesystem')`, + `CREATE TABLE "StatusList" + ( + "id" varchar NOT NULL, + "correlationId" varchar NOT NULL, + "length" integer NOT NULL, + "issuer" text NOT NULL, + "type" "StatusList_type_enum" NOT NULL DEFAULT 'StatusList2021', + "driverType" "StatusList_drivertype_enum" NOT NULL DEFAULT 'agent_typeorm', + "credentialIdMode" "StatusList_credentialidmode_enum" NOT NULL DEFAULT 'ISSUANCE', + "proofFormat" varchar NOT NULL DEFAULT 'lds', + "statusListCredential" text, + "indexingDirection" varchar, + "statusPurpose" varchar, + "bitsPerStatus" integer, + "expiresAt" varchar, + CONSTRAINT "UQ_correlationId" UNIQUE ("correlationId"), + CONSTRAINT "PK_StatusList_Id" PRIMARY KEY ("id") + )`, ) - await queryRunner.query(`CREATE TYPE "public"."StatusList_credentialidmode_enum" AS ENUM('ISSUANCE', 'PERSISTENCE', 'NEVER')`) + await queryRunner.query( - `CREATE TABLE "StatusList" ("id" character varying NOT NULL, "correlationId" character varying NOT NULL, "length" integer NOT NULL, "issuer" text NOT NULL, "type" "public"."StatusList_type_enum" NOT NULL DEFAULT 'StatusList2021', "driverType" "public"."StatusList_drivertype_enum" NOT NULL DEFAULT 'agent_typeorm', "credentialIdMode" "public"."StatusList_credentialidmode_enum" NOT NULL DEFAULT 'ISSUANCE', "proofFormat" character varying NOT NULL DEFAULT 'lds', "indexingDirection" character varying NOT NULL DEFAULT 'rightToLeft', "statusPurpose" character varying NOT NULL DEFAULT 'revocation', "statusListCredential" text, CONSTRAINT "UQ_correlationId" UNIQUE ("correlationId"), CONSTRAINT "PK_StatusList_Id" PRIMARY KEY ("id"))`, + `CREATE TABLE "StatusListEntry" + ( + "statusListId" varchar NOT NULL, + "statusListIndex" integer NOT NULL, + "credentialId" varchar, + "credentialHash" varchar(128), + "correlationId" varchar(255), + "value" varchar(50), + CONSTRAINT "PK_68704d2d13857360c6b44a3d1d0" PRIMARY KEY ("statusListId", "statusListIndex") + )`, ) + await queryRunner.query( - `ALTER TABLE "StatusListEntry" ADD CONSTRAINT "FK_statusListEntry_statusListId" FOREIGN KEY ("statusListId") REFERENCES "StatusList"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + `ALTER TABLE "StatusListEntry" + ADD CONSTRAINT "FK_statusListEntry_statusListId" FOREIGN KEY ("statusListId") REFERENCES "StatusList" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, ) } - public async down(queryRunner: QueryRunner): Promise {} + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "StatusListEntry" + DROP CONSTRAINT "FK_statusListEntry_statusListId"`) + await queryRunner.query(`DROP TABLE "StatusListEntry"`) + await queryRunner.query(`DROP TABLE "StatusList"`) + await queryRunner.query(`DROP TYPE "StatusList_credentialidmode_enum"`) + await queryRunner.query(`DROP TYPE "StatusList_drivertype_enum"`) + await queryRunner.query(`DROP TYPE "StatusList_type_enum"`) + } } diff --git a/packages/data-store/src/migrations/sqlite/1693866470000-CreateStatusList.ts b/packages/data-store/src/migrations/sqlite/1693866470000-CreateStatusList.ts index 4e5d8182b..c55fd50ff 100644 --- a/packages/data-store/src/migrations/sqlite/1693866470000-CreateStatusList.ts +++ b/packages/data-store/src/migrations/sqlite/1693866470000-CreateStatusList.ts @@ -5,20 +5,62 @@ export class CreateStatusList1693866470002 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `CREATE TABLE "StatusListEntry" ("statusListId" varchar NOT NULL, "statusListIndex" integer NOT NULL, "credentialId" varchar, "credentialHash" varchar(128), "correlationId" varchar(255), "value" varchar(50), PRIMARY KEY ("statusListId", "statusListIndex"))`, + `CREATE TABLE "StatusListEntry" + ( + "statusListId" varchar NOT NULL, + "statusListIndex" integer NOT NULL, + "credentialId" varchar, + "credentialHash" varchar(128), + "correlationId" varchar(255), + "value" varchar(50), + PRIMARY KEY ("statusListId", "statusListIndex") + )`, ) await queryRunner.query( - `CREATE TABLE "StatusList" ("id" varchar PRIMARY KEY NOT NULL, "correlationId" varchar NOT NULL, "length" integer NOT NULL, "issuer" text NOT NULL, "type" varchar CHECK( "type" IN ('StatusList2021') ) NOT NULL DEFAULT ('StatusList2021'), "driverType" varchar CHECK( "driverType" IN ('agent_typeorm','agent_kv_store','github','agent_filesystem') ) NOT NULL DEFAULT ('agent_typeorm'), "credentialIdMode" varchar CHECK( "credentialIdMode" IN ('ISSUANCE','PERSISTENCE','NEVER') ) NOT NULL DEFAULT ('ISSUANCE'), "proofFormat" varchar CHECK( "proofFormat" IN ('lds','jwt') ) NOT NULL DEFAULT ('lds'), "indexingDirection" varchar CHECK( "indexingDirection" IN ('rightToLeft') ) NOT NULL DEFAULT ('rightToLeft'), "statusPurpose" varchar NOT NULL DEFAULT ('revocation'), "statusListCredential" text, CONSTRAINT "UQ_correlationId" UNIQUE ("correlationId"))`, + `CREATE TABLE "StatusList" + ( + "id" varchar PRIMARY KEY NOT NULL, + "correlationId" varchar NOT NULL, + "length" integer NOT NULL, + "issuer" text NOT NULL, + "type" varchar CHECK ( "type" IN ('StatusList2021', 'OAuthStatusList') ) NOT NULL DEFAULT ('StatusList2021'), + "driverType" varchar CHECK ( "driverType" IN ('agent_typeorm', 'agent_kv_store', 'github', + 'agent_filesystem') ) NOT NULL DEFAULT ('agent_typeorm'), + "credentialIdMode" varchar CHECK ( "credentialIdMode" IN ('ISSUANCE', 'PERSISTENCE', 'NEVER') ) NOT NULL DEFAULT ('ISSUANCE'), + "proofFormat" varchar CHECK ( "proofFormat" IN ('lds', 'jwt') ) NOT NULL DEFAULT ('lds'), + "indexingDirection" varchar CHECK ( "indexingDirection" IN ('rightToLeft') ), + "statusPurpose" varchar, + "statusListCredential" text, + "bitsPerStatus" integer, + "expiresAt" varchar, + CONSTRAINT "UQ_correlationId" UNIQUE ("correlationId") + )`, ) await queryRunner.query( - `CREATE TABLE "temporary_StatusListEntry" ("statusListId" varchar NOT NULL, "statusListIndex" integer NOT NULL, "credentialId" varchar, "credentialHash" varchar(128), "correlationId" varchar(255), "value" varchar(50), CONSTRAINT "FK_statusListEntry_statusListId" FOREIGN KEY ("statusListId") REFERENCES "StatusList" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, PRIMARY KEY ("statusListId", "statusListIndex"))`, + `CREATE TABLE "temporary_StatusListEntry" + ( + "statusListId" varchar NOT NULL, + "statusListIndex" integer NOT NULL, + "credentialId" varchar, + "credentialHash" varchar(128), + "correlationId" varchar(255), + "value" varchar(50), + CONSTRAINT "FK_statusListEntry_statusListId" FOREIGN KEY ("statusListId") REFERENCES "StatusList" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, + PRIMARY KEY ("statusListId", "statusListIndex") + )`, ) await queryRunner.query( - `INSERT INTO "temporary_StatusListEntry"("statusListId", "statusListIndex", "credentialId", "credentialHash", "correlationId", "value") SELECT "statusListId", "statusListIndex", "credentialId", "credentialHash", "correlationId", "value" FROM "StatusListEntry"`, + `INSERT INTO "temporary_StatusListEntry"("statusListId", "statusListIndex", "credentialId", + "credentialHash", "correlationId", "value") + SELECT "statusListId", "statusListIndex", "credentialId", "credentialHash", "correlationId", "value" + FROM "StatusListEntry"`, ) await queryRunner.query(`DROP TABLE "StatusListEntry"`) await queryRunner.query(`ALTER TABLE "temporary_StatusListEntry" RENAME TO "StatusListEntry"`) } - public async down(queryRunner: QueryRunner): Promise {} + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "StatusListEntry"`) + await queryRunner.query(`DROP TABLE "StatusList"`) + } } diff --git a/packages/data-store/src/statusList/StatusListStore.ts b/packages/data-store/src/statusList/StatusListStore.ts index b4fd19180..54ecfb47a 100644 --- a/packages/data-store/src/statusList/StatusListStore.ts +++ b/packages/data-store/src/statusList/StatusListStore.ts @@ -1,4 +1,4 @@ -import { OrPromise } from '@sphereon/ssi-types' +import { OrPromise, StatusListType } from '@sphereon/ssi-types' import Debug from 'debug' import { DataSource, In, Repository } from 'typeorm' import { OAuthStatusListEntity, StatusList2021Entity, StatusListEntity } from '../entities/statusList/StatusListEntities' @@ -11,15 +11,14 @@ import { IGetStatusListEntryByCredentialIdArgs, IGetStatusListEntryByIndexArgs, IGetStatusListsArgs, - IOAuthStatusListEntity, IRemoveStatusListArgs, - IStatusList2021Entity, IStatusListEntity, IStatusListEntryAvailableArgs, IStatusListEntryEntity, IUpdateStatusListIndexArgs, } from '../types' import { IStatusListStore } from './IStatusListStore' +import { statusListEntityFrom, statusListFrom } from '../utils/statusList/MappingUtils' const debug = Debug('sphereon:ssi-sdk:data-store:status-list') @@ -98,28 +97,6 @@ export class StatusListStore implements IStatusListStore { return result ?? undefined } - async removeStatusListEntryByIndex(args: IGetStatusListEntryByIndexArgs): Promise { - let error = false - try { - await this.getStatusListEntryByIndex(args) // only used to check it exists - } catch (error) { - error = true - } - if (error) { - console.log(`Could not delete statusList ${args.statusListId} entry by index ${args.statusListIndex}`) - } else { - const result = await ( - await this.getStatusListEntryRepo() - ).delete({ - ...(args.statusListId && { statusList: args.statusListId }), - ...(args.correlationId && { correlationId: args.correlationId }), - statusListIndex: args.statusListIndex, - }) - error = !result.affected || result.affected !== 1 - } - return !error - } - async getStatusListEntryByCredentialId(args: IGetStatusListEntryByCredentialIdArgs): Promise { const credentialId = args.credentialId if (!credentialId) { @@ -160,6 +137,28 @@ export class StatusListStore implements IStatusListStore { return !error } + async removeStatusListEntryByIndex(args: IGetStatusListEntryByIndexArgs): Promise { + let error = false + try { + await this.getStatusListEntryByIndex(args) + } catch (error) { + error = true + } + if (error) { + console.log(`Could not delete statusList ${args.statusListId} entry by index ${args.statusListIndex}`) + } else { + const result = await ( + await this.getStatusListEntryRepo() + ).delete({ + ...(args.statusListId && { statusList: args.statusListId }), + ...(args.correlationId && { correlationId: args.correlationId }), + statusListIndex: args.statusListIndex, + }) + error = !result.affected || result.affected !== 1 + } + return !error + } + async getStatusListEntries(args: IGetStatusListEntriesArgs): Promise { return (await this.getStatusListEntryRepo()).find({ where: { ...args?.filter, statusList: args.statusListId } }) } @@ -179,14 +178,9 @@ export class StatusListStore implements IStatusListStore { throw Error(`No status list found for id ${args.id}`) } - if (result instanceof StatusList2021Entity) { - return result as IStatusList2021Entity - } else if (result instanceof OAuthStatusListEntity) { - return result as IOAuthStatusListEntity - } - - throw Error(`Invalid status list type ${result.type}`) + return statusListFrom(result) } + async getStatusLists(args: IGetStatusListsArgs): Promise> { const result = await ( await this.getStatusListRepo() @@ -198,15 +192,9 @@ export class StatusListStore implements IStatusListStore { return [] } - return result.map((entity) => { - if (entity instanceof StatusList2021Entity) { - return entity as IStatusList2021Entity - } else if (entity instanceof OAuthStatusListEntity) { - return entity as IOAuthStatusListEntity - } - throw Error(`Invalid status list type ${entity.type}`) - }) + return result.map((entity) => statusListFrom(entity)) } + async addStatusList(args: IAddStatusListArgs): Promise { const { id, correlationId } = args @@ -220,29 +208,38 @@ export class StatusListStore implements IStatusListStore { } debug('Adding status list ', id) - const createdResult = await (await this.getStatusListRepo()).save(args) - return createdResult + const entity = statusListEntityFrom(args) + const createdResult = await (await this.getStatusListRepo(args.type)).save(entity) + return statusListFrom(createdResult) } async updateStatusList(args: IUpdateStatusListIndexArgs): Promise { const result = await this.getStatusList(args) debug('Updating status list', result) - const updatedResult = await (await this.getStatusListRepo()).save(args, { transaction: true }) - return updatedResult + const entity = statusListEntityFrom(args) + const updatedResult = await (await this.getStatusListRepo(args.type)).save(entity, { transaction: true }) + return statusListFrom(updatedResult) } async removeStatusList(args: IRemoveStatusListArgs): Promise { const result = await this.getStatusList(args) - await (await this.getStatusListRepo()).delete(result) + await (await this.getStatusListRepo(result.type)).delete(result) } private async getDS(): Promise { return this._dbConnection } - async getStatusListRepo(): Promise> { - const repo = (await this.getDS()).getRepository(StatusListEntity) - return repo + async getStatusListRepo(type?: StatusListType): Promise> { + const dataSource = await this.getDS() + switch (type) { + case StatusListType.StatusList2021: + return dataSource.getRepository(StatusList2021Entity) + case StatusListType.OAuthStatusList: + return dataSource.getRepository(OAuthStatusListEntity) + default: + return dataSource.getRepository(StatusListEntity) + } } async getStatusListEntryRepo(): Promise> { diff --git a/packages/data-store/src/types/statusList/IAbstractStatusListStore.ts b/packages/data-store/src/types/statusList/IAbstractStatusListStore.ts index 6c48bd34f..b1a00b337 100644 --- a/packages/data-store/src/types/statusList/IAbstractStatusListStore.ts +++ b/packages/data-store/src/types/statusList/IAbstractStatusListStore.ts @@ -1,7 +1,7 @@ import { FindOptionsWhere } from 'typeorm' -import { IStatusListEntity, IStatusListEntryEntity } from './statusList' +import { IOAuthStatusListEntity, IStatusList2021Entity, IStatusListEntity, IStatusListEntryEntity } from './statusList' -export type FindStatusListArgs = FindOptionsWhere[] +export type FindStatusListArgs = FindOptionsWhere[] export type FindStatusListEntryArgs = FindOptionsWhere[] | FindOptionsWhere export interface IStatusListEntryAvailableArgs { diff --git a/packages/data-store/src/types/statusList/statusList.ts b/packages/data-store/src/types/statusList/statusList.ts index e0f236944..f401f9adf 100644 --- a/packages/data-store/src/types/statusList/statusList.ts +++ b/packages/data-store/src/types/statusList/statusList.ts @@ -1,6 +1,6 @@ import { IIssuer, - OriginalVerifiableCredential, + StatusListVerifiableCredential, StatusListCredentialIdMode, StatusListDriverType, StatusListIndexingDirection, @@ -19,7 +19,7 @@ export interface IStatusListEntity { issuer: string | IIssuer type: StatusListType proofFormat: ProofFormat - statusListCredential?: OriginalVerifiableCredential + statusListCredential?: StatusListVerifiableCredential } export interface IStatusList2021Entity extends IStatusListEntity { diff --git a/packages/data-store/src/utils/statusList/MappingUtils.ts b/packages/data-store/src/utils/statusList/MappingUtils.ts new file mode 100644 index 000000000..3afb78a24 --- /dev/null +++ b/packages/data-store/src/utils/statusList/MappingUtils.ts @@ -0,0 +1,84 @@ +import { IOAuthStatusListEntity, IStatusList2021Entity, IStatusListEntity } from '../../types' +import { OAuthStatusListEntity, StatusList2021Entity, StatusListEntity } from '../../entities/statusList/StatusListEntities' +import { StatusListType } from '@sphereon/ssi-types' +import { replaceNullWithUndefined } from '../FormattingUtils' + +export const statusListEntityFrom = (args: IStatusListEntity): StatusListEntity => { + if (args.type === StatusListType.StatusList2021) { + const entity = new StatusList2021Entity() + const sl2021 = args as IStatusList2021Entity + entity.indexingDirection = sl2021.indexingDirection + entity.statusPurpose = sl2021.statusPurpose + setBaseFields(entity, args) + Object.defineProperty(entity, 'type', { + value: StatusListType.StatusList2021, + enumerable: true, + configurable: true, + }) + return entity + } + + if (args.type === StatusListType.OAuthStatusList) { + const entity = new OAuthStatusListEntity() + const oauthSl = args as IOAuthStatusListEntity + entity.bitsPerStatus = oauthSl.bitsPerStatus + entity.expiresAt = oauthSl.expiresAt + setBaseFields(entity, args) + Object.defineProperty(entity, 'type', { + value: StatusListType.OAuthStatusList, + enumerable: true, + configurable: true, + }) + return entity + } + + throw new Error(`Invalid status list type ${args.type}`) +} + +export const statusListFrom = (entity: StatusListEntity): IStatusListEntity => { + if (entity instanceof StatusList2021Entity) { + const result: IStatusList2021Entity = { + ...getBaseFields(entity), + type: StatusListType.StatusList2021, + indexingDirection: entity.indexingDirection, + statusPurpose: entity.statusPurpose, + } + return replaceNullWithUndefined(result) + } + + if (entity instanceof OAuthStatusListEntity) { + const result: IOAuthStatusListEntity = { + ...getBaseFields(entity), + type: StatusListType.OAuthStatusList, + bitsPerStatus: entity.bitsPerStatus, + expiresAt: entity.expiresAt, + } + return replaceNullWithUndefined(result) + } + + throw new Error(`Invalid status list type ${typeof entity}`) +} + +function setBaseFields(entity: StatusListEntity, args: IStatusListEntity) { + entity.id = args.id + entity.correlationId = args.correlationId + entity.length = args.length + entity.issuer = args.issuer + entity.driverType = args.driverType + entity.credentialIdMode = args.credentialIdMode + entity.proofFormat = args.proofFormat + entity.statusListCredential = args.statusListCredential +} + +function getBaseFields(entity: StatusListEntity): Omit { + return { + id: entity.id, + correlationId: entity.correlationId, + length: entity.length, + issuer: entity.issuer, + driverType: entity.driverType, + credentialIdMode: entity.credentialIdMode, + proofFormat: entity.proofFormat, + statusListCredential: entity.statusListCredential, + } +} diff --git a/packages/ssi-types/src/types/status-list.ts b/packages/ssi-types/src/types/status-list.ts index c51f7e30f..07cfc71a1 100644 --- a/packages/ssi-types/src/types/status-list.ts +++ b/packages/ssi-types/src/types/status-list.ts @@ -1,4 +1,9 @@ +import { W3CVerifiableCredential } from './w3c-vc' +import { MdocDocument } from './mso_mdoc' + export enum StatusListType { StatusList2021 = 'StatusList2021', OAuthStatusList = 'OAuthStatusList', } + +export type StatusListVerifiableCredential = W3CVerifiableCredential | MdocDocument diff --git a/packages/tsconfig.json b/packages/tsconfig.json index c927c8ce5..7d7233795 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -41,7 +41,7 @@ { "path": "wellknown-did-issuer" }, { "path": "wellknown-did-verifier" }, { "path": "issuance-branding" }, - { "path": "headless-web3-provider" }, + { "path": "web3-provider-headless" }, { "path": "event-logger" }, { "path": "remote-server-rest-api" }, { "path": "sd-jwt" }, @@ -49,7 +49,7 @@ { "path": "public-key-hosting" }, { "path": "resource-resolver" }, { "path": "oidf-client" }, - { "path": "oidf-metatdata-server" }, + { "path": "oidf-metadata-server" }, { "path": "anomaly-detection" }, { "path": "geolocation-store" } ] diff --git a/packages/vc-handler-ld-local/src/__tests__/statuslist.test.ts b/packages/vc-handler-ld-local/src/__tests__/statuslist.test.ts deleted file mode 100644 index 9b96b265b..000000000 --- a/packages/vc-handler-ld-local/src/__tests__/statuslist.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { IdentifierResolution, IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { createAgent, ICredentialPlugin, IDIDManager, IIdentifier, IKeyManager, IResolver, TAgent } from '@veramo/core' -import { CredentialPlugin, ICredentialIssuer } from '@veramo/credential-w3c' -import { DIDManager, MemoryDIDStore } from '@veramo/did-manager' -import { getDidKeyResolver, SphereonKeyDidProvider } from '@sphereon/ssi-sdk-ext.did-provider-key' -import { DIDResolverPlugin } from '@veramo/did-resolver' -import { SphereonKeyManager } from '@sphereon/ssi-sdk-ext.key-manager' -import { SphereonKeyManagementSystem } from '@sphereon/ssi-sdk-ext.kms-local' -import { MemoryKeyStore, MemoryPrivateKeyStore } from '@veramo/key-manager' -import { Resolver } from 'did-resolver' -// @ts-ignore -import nock from 'nock' -import { - createNewStatusList, - checkStatusIndexFromStatusListCredential, - updateStatusIndexFromStatusListCredential, -} from '@sphereon/ssi-sdk.vc-status-list' -import { CredentialHandlerLDLocal } from '../agent' -import { LdDefaultContexts } from '../ld-default-contexts' -import { SphereonEcdsaSecp256k1RecoverySignature2020, SphereonEd25519Signature2018, SphereonEd25519Signature2020 } from '../suites' -import { ICredentialHandlerLDLocal, MethodNames } from '../types' - -jest.setTimeout(100000) - -describe('Status list', () => { - let didKeyIdentifier: IIdentifier - let agent: TAgent - - // jest.setTimeout(1000000) - beforeAll(async () => { - agent = createAgent({ - plugins: [ - new SphereonKeyManager({ - store: new MemoryKeyStore(), - kms: { - local: new SphereonKeyManagementSystem(new MemoryPrivateKeyStore()), - }, - }), - new DIDManager({ - providers: { - 'did:key': new SphereonKeyDidProvider({ defaultKms: 'local' }), - }, - store: new MemoryDIDStore(), - defaultProvider: 'did:key', - }), - new IdentifierResolution({ crypto: global.crypto }), - new DIDResolverPlugin({ - resolver: new Resolver({ - ...getDidKeyResolver(), - }), - }), - new CredentialPlugin(), - new CredentialHandlerLDLocal({ - contextMaps: [LdDefaultContexts], - suites: [new SphereonEd25519Signature2018(), new SphereonEd25519Signature2020(), new SphereonEcdsaSecp256k1RecoverySignature2020()], - bindingOverrides: new Map([ - // Bindings to test overrides of credential-ld plugin methods - ['createVerifiableCredentialLD', MethodNames.createVerifiableCredentialLDLocal], - ['createVerifiablePresentationLD', MethodNames.createVerifiablePresentationLDLocal], - // We test the verify methods by using the LDLocal versions directly in the tests - ]), - }), - ], - }) - didKeyIdentifier = await agent.didManagerCreate() - }) - - it('create a new status list', async () => { - const statusList = await createNewStatusList( - { - statusPurpose: 'revocation', - proofFormat: 'lds', - id: 'http://localhost:9543/list1', - issuer: didKeyIdentifier.did, - length: 99999, - correlationId: '' + new Date().toISOString(), - }, - { agent }, - ) - expect(statusList).toBeDefined() - expect(statusList.id).toEqual('http://localhost:9543/list1') - expect(statusList.encodedList).toBeDefined() - expect(statusList.issuer).toEqual(didKeyIdentifier.did) - expect(statusList.length).toEqual(99999) - expect(statusList.indexingDirection).toEqual('rightToLeft') - expect(statusList.proofFormat).toEqual('lds') - expect(statusList.statusListCredential).toBeDefined() - }) - - it('Update a status list', async () => { - const initialList = await createNewStatusList( - { - statusPurpose: 'revocation', - proofFormat: 'lds', - id: 'http://localhost:9543/list2', - issuer: didKeyIdentifier.did, - length: 99999, - correlationId: '' + new Date().toISOString(), - }, - { agent }, - ) - expect(initialList).toBeDefined() - - let statusList = await updateStatusIndexFromStatusListCredential( - { statusListCredential: initialList.statusListCredential, statusListIndex: 2, value: true }, - { agent }, - ) - statusList = await updateStatusIndexFromStatusListCredential( - { statusListCredential: statusList.statusListCredential, statusListIndex: 4, value: true }, - { agent }, - ) - - expect(statusList.id).toEqual('http://localhost:9543/list2') - expect(statusList.encodedList).toBeDefined() - expect(statusList.issuer).toEqual(didKeyIdentifier.did) - expect(statusList.length).toEqual(99999) - expect(statusList.indexingDirection).toEqual('rightToLeft') - expect(statusList.proofFormat).toEqual('lds') - expect(statusList.statusListCredential).toBeDefined() - expect(statusList.statusListCredential).not.toEqual(initialList.statusListCredential) - - const result2 = await checkStatusIndexFromStatusListCredential({ - statusListCredential: statusList.statusListCredential, - statusListIndex: '2', - }) - expect(result2).toEqual(true) - const result3 = await checkStatusIndexFromStatusListCredential({ - statusListCredential: statusList.statusListCredential, - statusListIndex: '3', - }) - expect(result3).toEqual(false) - const result4 = await checkStatusIndexFromStatusListCredential({ - statusListCredential: statusList.statusListCredential, - statusListIndex: '4', - }) - expect(result4).toEqual(true) - - statusList = await updateStatusIndexFromStatusListCredential( - { statusListCredential: statusList.statusListCredential, statusListIndex: 4, value: false }, - { agent }, - ) - const result4Updated = await checkStatusIndexFromStatusListCredential({ - statusListCredential: statusList.statusListCredential, - statusListIndex: '4', - }) - expect(result4Updated).toEqual(false) - }) -}) diff --git a/packages/vc-status-list-issuer-drivers/src/drivers.ts b/packages/vc-status-list-issuer-drivers/src/drivers.ts index 0b268b8f1..85edb4b84 100644 --- a/packages/vc-status-list-issuer-drivers/src/drivers.ts +++ b/packages/vc-status-list-issuer-drivers/src/drivers.ts @@ -14,7 +14,7 @@ import { StatusListOAuthEntryCredentialStatus, StatusListResult, } from '@sphereon/ssi-sdk.vc-status-list' -import { OriginalVerifiableCredential, StatusListCredentialIdMode, StatusListDriverType, StatusListType } from '@sphereon/ssi-types' +import { StatusListCredentialIdMode, StatusListDriverType, StatusListType, StatusListVerifiableCredential } from '@sphereon/ssi-types' import { DataSource } from 'typeorm' import { IStatusListDriver } from './types' import { statusListResultToEntity } from './status-list-adapters' @@ -129,7 +129,7 @@ export class AgentDataSourceStatusListDriver implements IStatusListDriver { } async createStatusList(args: { - statusListCredential: OriginalVerifiableCredential + statusListCredential: StatusListVerifiableCredential correlationId?: string credentialIdMode?: StatusListCredentialIdMode }): Promise { @@ -151,11 +151,15 @@ export class AgentDataSourceStatusListDriver implements IStatusListDriver { return details } - async updateStatusList(args: { statusListCredential: OriginalVerifiableCredential; correlationId: string }): Promise { + async updateStatusList(args: { + statusListCredential: StatusListVerifiableCredential + correlationId: string + type: StatusListType + }): Promise { const correlationId = args.correlationId ?? this.options.correlationId const details = await statusListCredentialToDetails({ ...args, correlationId, driverType: this.getType() }) const entity = await ( - await this.statusListStore.getStatusListRepo() + await this.statusListStore.getStatusListRepo(args.type) ).findOne({ where: [ { @@ -185,11 +189,11 @@ export class AgentDataSourceStatusListDriver implements IStatusListDriver { } private isStatusList2021Entity(statusList: StatusListEntity): statusList is StatusList2021Entity { - return statusList.type === StatusListType.StatusList2021 + return statusList instanceof StatusList2021Entity } private isOAuthStatusListEntity(statusList: StatusListEntity): statusList is OAuthStatusListEntity { - return statusList.type === StatusListType.OAuthStatusList + return statusList instanceof OAuthStatusListEntity } async updateStatusListEntry(args: IAddStatusListEntryArgs): Promise<{ @@ -227,7 +231,7 @@ export class AgentDataSourceStatusListDriver implements IStatusListDriver { } } - throw new Error(`Unsupported status list type: ${statusList.type}`) + throw new Error(`Unsupported status list type: ${typeof statusList}`) } async getStatusListEntryByCredentialId(args: IGetStatusListEntryByCredentialIdArgs): Promise { diff --git a/packages/vc-status-list-issuer-drivers/src/types.ts b/packages/vc-status-list-issuer-drivers/src/types.ts index 0ed92bdd9..3cc610c7b 100644 --- a/packages/vc-status-list-issuer-drivers/src/types.ts +++ b/packages/vc-status-list-issuer-drivers/src/types.ts @@ -12,7 +12,7 @@ import { StatusListOAuthEntryCredentialStatus, StatusListResult, } from '@sphereon/ssi-sdk.vc-status-list' -import { OriginalVerifiableCredential, StatusListDriverType } from '@sphereon/ssi-types' +import { StatusListVerifiableCredential, StatusListDriverType } from '@sphereon/ssi-types' import { IAgentContext, ICredentialIssuer, @@ -45,7 +45,7 @@ export interface IStatusListDriver { getStatusListLength(args?: { correlationId?: string }): Promise - createStatusList(args: { statusListCredential: OriginalVerifiableCredential; correlationId?: string }): Promise + createStatusList(args: { statusListCredential: StatusListVerifiableCredential; correlationId?: string }): Promise getStatusList(args?: { correlationId?: string }): Promise @@ -58,7 +58,7 @@ export interface IStatusListDriver { getStatusListEntryByIndex(args: IGetStatusListEntryByIndexArgs): Promise - updateStatusList(args: { statusListCredential: OriginalVerifiableCredential }): Promise + updateStatusList(args: { statusListCredential: StatusListVerifiableCredential }): Promise deleteStatusList(): Promise diff --git a/packages/vc-status-list-tests/README.md b/packages/vc-status-list-tests/README.md new file mode 100644 index 000000000..4cca41dc5 --- /dev/null +++ b/packages/vc-status-list-tests/README.md @@ -0,0 +1,43 @@ +# README.md + +## Overview + +This package, `@sphereon/vc-status-list-tests`, contains a comprehensive suite of tests for verifying the functionality of the `@sphereon/ssi-sdk.vc-status-list` library. The tests ensure correctness and robustness when working with status lists, including `StatusList2021` and `OAuthStatusList`, used for managing verifiable credentials. The tests also cover various scenarios involving JWT and Linked Data Signatures (LD-Signatures) proofs. + +## Test Features + +### StatusList2021 Tests +- **Create and Update Using LD-Signatures**: Validates creation and modification of `StatusList2021` credentials using Linked Data Signatures. +- **Create and Update Using JWT Format**: Ensures correct behavior when using JWT-based proofs. +- **Update Status Using Encoded List**: Verifies updates to `StatusList2021` credentials using pre-encoded status lists. +- **Conversion to Verifiable Credential**: Tests the conversion of a `StatusList2021` to a verifiable credential in both string and object issuer formats, ensuring all required fields are correctly handled. + +### OAuthStatusList Tests +- **Create and Update Using JWT Format**: Confirms proper creation and modification of `OAuthStatusList` credentials using JWT-based proofs. +- **Invalid Proof Format Rejection**: Tests that invalid proof formats (e.g., LD-Signatures) are correctly rejected for `OAuthStatusList`. + +### Utility Tests +- **Updating Status Indices**: Validates the ability to update status indices within status lists for both `StatusList2021` and `OAuthStatusList`. +- **Error Handling**: Ensures missing or invalid fields throw appropriate errors during credential creation or updates. + +## Purpose of a Separate Package + +This package is maintained as a separate testing module to: +1. **Avoid Cyclic Dependencies**: Prevent cyclic dependency issues with `@sphereon/ssi-sdk.vc-handler-ld-local` and other related packages. +2. **More Flexible With Other Dependencies**: Allows imports from additional packages without adding them to `vc-handler-ld-local`, where some of these tests originated. +## Dependencies + +This package leverages several dependencies, including: +- `@veramo/core` and related plugins for DID management and credential handling. +- `@sphereon` SDK extensions for enhanced functionality like key management and identifier resolution. +- `jest` for running the test suite. + +## How to Use + +1. **Install Dependencies**: + Run `pnpm install` to install the necessary dependencies. Ensure all workspace links are correctly resolved. + +2. **Run Tests**: + Execute the test suite using: + ```bash + pnpm test diff --git a/packages/vc-status-list-tests/__tests__/statuslist.test.ts b/packages/vc-status-list-tests/__tests__/statuslist.test.ts index 7fdd3812c..39444fe88 100644 --- a/packages/vc-status-list-tests/__tests__/statuslist.test.ts +++ b/packages/vc-status-list-tests/__tests__/statuslist.test.ts @@ -11,7 +11,11 @@ import { Resolver } from 'did-resolver' import { checkStatusIndexFromStatusListCredential, createNewStatusList, + Status2021, + statusList2021ToVerifiableCredential, + StatusOAuth, updateStatusIndexFromStatusListCredential, + updateStatusListIndexFromEncodedList, } from '@sphereon/ssi-sdk.vc-status-list' import { CredentialHandlerLDLocal, @@ -94,14 +98,14 @@ describe('Status list', () => { expect(statusList.statusList2021?.indexingDirection).toBe('rightToLeft') const updated = await updateStatusIndexFromStatusListCredential( - { statusListCredential: statusList.statusListCredential, statusListIndex: 2, value: true }, + { statusListCredential: statusList.statusListCredential, statusListIndex: 2, value: Status2021.Invalid }, { agent }, ) const status = await checkStatusIndexFromStatusListCredential({ statusListCredential: updated.statusListCredential, statusListIndex: '2', }) - expect(status).toBe(1) + expect(status).toBe(Status2021.Invalid) }) it('should create and update using JWT format', async () => { @@ -121,14 +125,14 @@ describe('Status list', () => { ) const updated = await updateStatusIndexFromStatusListCredential( - { statusListCredential: statusList.statusListCredential, statusListIndex: 3, value: true }, + { statusListCredential: statusList.statusListCredential, statusListIndex: 3, value: Status2021.Invalid }, { agent }, ) const status = await checkStatusIndexFromStatusListCredential({ statusListCredential: updated.statusListCredential, statusListIndex: '3', }) - expect(status).toBe(1) + expect(status).toBe(Status2021.Invalid) }) }) @@ -150,14 +154,14 @@ describe('Status list', () => { ) const updated = await updateStatusIndexFromStatusListCredential( - { statusListCredential: statusList.statusListCredential, statusListIndex: 4, value: true }, + { statusListCredential: statusList.statusListCredential, statusListIndex: 4, value: StatusOAuth.Invalid }, { agent }, ) const status = await checkStatusIndexFromStatusListCredential({ statusListCredential: updated.statusListCredential, statusListIndex: '4', }) - expect(status).toBe(1) + expect(status).toBe(StatusOAuth.Invalid) }) it('should reject LD-Signatures format', async () => { @@ -178,4 +182,132 @@ describe('Status list', () => { ).rejects.toThrow("Invalid proof format 'lds' for OAuthStatusList") }) }) + + describe('updateStatusListIndexFromEncodedList', () => { + it('should update StatusList2021 using encoded list', async () => { + // First create a status list to get valid encoded list + const initialList = await createNewStatusList( + { + type: StatusListType.StatusList2021, + proofFormat: 'jwt', + id: 'http://localhost:9543/encoded1', + correlationId: 'test-4-' + Date.now(), + issuer: didKeyIdentifier.did, + length: 1000, + statusList2021: { + indexingDirection: 'rightToLeft', + }, + }, + { agent }, + ) + + const result = await updateStatusListIndexFromEncodedList( + { + type: StatusListType.StatusList2021, + statusListIndex: 1, + value: true, + proofFormat: 'jwt', + issuer: didKeyIdentifier.did, + id: 'http://localhost:9543/encoded1', + encodedList: initialList.encodedList, + statusList2021: { + statusPurpose: 'revocation', + }, + }, + { agent }, + ) + + expect(result.type).toBe(StatusListType.StatusList2021) + expect(result.encodedList).toBeDefined() + expect(result.statusListCredential).toBeDefined() + }) + + it('should update OAuthStatusList using encoded list', async () => { + const initialList = await createNewStatusList( + { + type: StatusListType.OAuthStatusList, + proofFormat: 'jwt', + id: 'http://localhost:9543/encoded2', + correlationId: 'test-5-' + Date.now(), + issuer: didKeyIdentifier.did, + length: 1000, + oauthStatusList: { + bitsPerStatus: 2, + }, + }, + { agent }, + ) + + const result = await updateStatusListIndexFromEncodedList( + { + type: StatusListType.OAuthStatusList, + statusListIndex: 1, + value: true, + proofFormat: 'jwt', + issuer: didKeyIdentifier.did, + id: 'http://localhost:9543/encoded2', + encodedList: initialList.encodedList, + oauthStatusList: { + bitsPerStatus: 2, + }, + }, + { agent }, + ) + + expect(result.type).toBe(StatusListType.OAuthStatusList) + expect(result.oauthStatusList?.bitsPerStatus).toBe(2) + }) + }) + + describe('statusList2021ToVerifiableCredential', () => { + it('should create VC with string issuer', async () => { + const result = await statusList2021ToVerifiableCredential( + { + issuer: didKeyIdentifier.did, + id: 'http://localhost:9543/sl1', + encodedList: 'H4sIAAAAAAAAA2NgwA8YgYARiEFEMxBzAbEMEEsAsQAQswExIxADAHPnBI8QAAAA', + statusPurpose: 'revocation', + type: StatusListType.StatusList2021, + proofFormat: 'jwt', + }, + { agent }, + ) + + expect(result).toBeDefined() + expect(typeof result === 'string' || 'proof' in result).toBeTruthy() + }) + + it('should create VC with issuer object', async () => { + const result = await statusList2021ToVerifiableCredential( + { + issuer: { id: didKeyIdentifier.did }, + id: 'http://localhost:9543/sl2', + encodedList: 'H4sIAAAAAAAAA2NgwA8YgYARiEFEMxBzAbEMEEsAsQAQswExIxADAHPnBI8QAAAA', + statusPurpose: 'revocation', + type: StatusListType.StatusList2021, + proofFormat: 'lds', + }, + { agent }, + ) + + if (typeof result === 'string') { + expect(result).toMatch(/^ey/) // JWT format starts with 'ey' + } else { + expect(result).toHaveProperty('proof') + } + }) + + it('should throw error for missing required fields', async () => { + await expect( + statusList2021ToVerifiableCredential( + { + issuer: didKeyIdentifier.did, + id: 'test', + encodedList: 'test', + } as any, + { agent }, + ), + ).rejects.toThrow() + }) + }) }) diff --git a/packages/vc-status-list/package.json b/packages/vc-status-list/package.json index 625c7b9e2..64566c4d5 100644 --- a/packages/vc-status-list/package.json +++ b/packages/vc-status-list/package.json @@ -20,6 +20,7 @@ "@veramo/credential-status": "4.2.0", "base64url": "^3.0.1", "credential-status": "^2.0.6", + "jwt-decode": "^4.0.0", "debug": "^4.3.5", "typeorm": "^0.3.20", "uint8arrays": "^3.1.1" diff --git a/packages/vc-status-list/src/functions.ts b/packages/vc-status-list/src/functions.ts index 0e6a177c2..b01f2f68e 100644 --- a/packages/vc-status-list/src/functions.ts +++ b/packages/vc-status-list/src/functions.ts @@ -1,5 +1,5 @@ import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { CredentialMapper, OriginalVerifiableCredential, StatusListDriverType, StatusListType, StatusPurpose2021 } from '@sphereon/ssi-types' +import { CredentialMapper, StatusListVerifiableCredential, StatusListDriverType, StatusListType, StatusPurpose2021 } from '@sphereon/ssi-types' import { checkStatus } from '@sphereon/vc-status-list' import { CredentialStatus, DIDDocument, IAgentContext, ICredentialPlugin } from '@veramo/core' @@ -13,10 +13,11 @@ import { UpdateStatusListFromEncodedListArgs, UpdateStatusListFromStatusListCredentialArgs, } from './types' -import { getAssertedStatusListType, getAssertedValue, getAssertedValues } from './utils' +import { getAssertedValue, getAssertedValues } from './utils' import { getStatusListImplementation } from './impl/StatusListFactory' +import { jwtDecode } from 'jwt-decode' -export async function fetchStatusListCredential(args: { statusListCredential: string }): Promise { +export async function fetchStatusListCredential(args: { statusListCredential: string }): Promise { const url = getAssertedValue('statusListCredential', args.statusListCredential) try { const response = await fetch(url) @@ -25,9 +26,9 @@ export async function fetchStatusListCredential(args: { statusListCredential: st } const responseAsText = await response.text() if (responseAsText.trim().startsWith('{')) { - return JSON.parse(responseAsText) as OriginalVerifiableCredential + return JSON.parse(responseAsText) as StatusListVerifiableCredential } - return responseAsText as OriginalVerifiableCredential + return responseAsText as StatusListVerifiableCredential } catch (error) { console.error(`Fetching status list ${url} resulted in an unexpected error: ${error instanceof Error ? error.message : JSON.stringify(error)}`) throw error @@ -46,7 +47,7 @@ export function statusPluginStatusFunction(args: { const result = await checkStatusForCredential({ ...args, documentLoader: args.documentLoader, - credential: credential as OriginalVerifiableCredential, + credential: credential as StatusListVerifiableCredential, errorUnknownListType: args.errorUnknownListType, }) @@ -69,7 +70,7 @@ export function vcLibCheckStatusFunction(args: { }) { const { mandatoryCredentialStatus, verifyStatusListCredential, verifyMatchingIssuers, errorUnknownListType } = args return (args: { - credential: OriginalVerifiableCredential + credential: StatusListVerifiableCredential documentLoader: any suite: any }): Promise<{ @@ -87,7 +88,7 @@ export function vcLibCheckStatusFunction(args: { } export async function checkStatusForCredential(args: { - credential: OriginalVerifiableCredential + credential: StatusListVerifiableCredential documentLoader: any suite: any mandatoryCredentialStatus?: boolean @@ -134,14 +135,14 @@ export async function simpleCheckStatusFromStatusListUrl(args: { } export async function checkStatusIndexFromStatusListCredential(args: { - statusListCredential: OriginalVerifiableCredential + statusListCredential: StatusListVerifiableCredential statusPurpose?: StatusPurpose2021 type?: StatusListType | 'StatusList2021Entry' id?: string statusListIndex: string | number }): Promise { - const requestedType = getAssertedStatusListType(args.type?.replace('Entry', '') as StatusListType) - const implementation = getStatusListImplementation(requestedType) + const statusListType: StatusListType = determineStatusListType(args.statusListCredential) + const implementation = getStatusListImplementation(statusListType) return implementation.checkStatusIndex(args) } @@ -154,25 +155,41 @@ export async function createNewStatusList( return implementation.createNewStatusList(args, context) } +function determineStatusListType(credential: StatusListVerifiableCredential): StatusListType { + if (CredentialMapper.isJwtEncoded(credential)) { + const payload: StatusListVerifiableCredential = jwtDecode(credential as string) + if (!CredentialMapper.isCredential(payload) && 'status_list' in payload) { + return StatusListType.OAuthStatusList + } + } + + if (CredentialMapper.isCredential(credential)) { + const uniform = CredentialMapper.toUniformCredential(credential) + const type = uniform.type.find((t) => { + return Object.values(StatusListType).some((statusType) => t.includes(statusType)) + }) + if (!type) { + throw new Error('Invalid status list credential type') + } + return type.replace('Credential', '') as StatusListType + } + + throw new Error('Cannot decode credential payload') +} + export async function updateStatusIndexFromStatusListCredential( args: UpdateStatusListFromStatusListCredentialArgs, context: IAgentContext, ): Promise { const credential = getAssertedValue('statusListCredential', args.statusListCredential) - - const uniform = CredentialMapper.toUniformCredential(credential) // This is not correct, we can't run a OAuthSTatusList through CredentialMapper and we can't see the type - const type = uniform.type.find((t) => t.includes('StatusList2021') || t.includes('OAuth2StatusList')) - if (!type) { - throw new Error('Invalid status list credential type') - } - const statusListType = type.replace('Credential', '') as StatusListType + const statusListType: StatusListType = determineStatusListType(credential) const implementation = getStatusListImplementation(statusListType) return implementation.updateStatusListIndex(args, context) } // Keeping helper function for backward compatibility export async function statusListCredentialToDetails(args: { - statusListCredential: OriginalVerifiableCredential + statusListCredential: StatusListVerifiableCredential correlationId?: string driverType?: StatusListDriverType }): Promise { @@ -188,7 +205,7 @@ export async function statusListCredentialToDetails(args: { { statusListCredential: args.statusListCredential, statusListIndex: 0, - value: false, + value: 0, }, {} as IAgentContext, ) @@ -206,7 +223,7 @@ export async function updateStatusListIndexFromEncodedList( export async function statusList2021ToVerifiableCredential( args: StatusList2021ToVerifiableCredentialArgs, context: IAgentContext, -): Promise { +): Promise { const { issuer, id, type } = getAssertedValues(args) const identifier = await context.agent.identifierManagedGet({ identifier: typeof issuer === 'string' ? issuer : issuer.id, @@ -236,5 +253,6 @@ export async function statusList2021ToVerifiableCredential( fetchRemoteContexts: true, }) - return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as OriginalVerifiableCredential).original + return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as StatusListVerifiableCredential) + .original as StatusListVerifiableCredential } diff --git a/packages/vc-status-list/src/impl/StatusList2021.ts b/packages/vc-status-list/src/impl/StatusList2021.ts index 9487245b0..cfaab064b 100644 --- a/packages/vc-status-list/src/impl/StatusList2021.ts +++ b/packages/vc-status-list/src/impl/StatusList2021.ts @@ -1,6 +1,6 @@ import { IAgentContext, ICredentialPlugin, ProofFormat } from '@veramo/core' import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { CredentialMapper, DocumentFormat, IIssuer, OriginalVerifiableCredential, StatusListType } from '@sphereon/ssi-types' +import { CredentialMapper, DocumentFormat, IIssuer, StatusListVerifiableCredential, StatusListType } from '@sphereon/ssi-types' import { StatusList } from '@sphereon/vc-status-list' import { IStatusList } from './IStatusList' import { @@ -154,7 +154,7 @@ export class StatusList2021Implementation implements IStatusList { keyRef?: string }, context: IAgentContext, - ): Promise { + ): Promise { const identifier = await context.agent.identifierManagedGet({ identifier: typeof args.issuer === 'string' ? args.issuer : args.issuer.id, vmRelationship: 'assertionMethod', @@ -181,6 +181,7 @@ export class StatusList2021Implementation implements IStatusList { fetchRemoteContexts: true, }) - return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as OriginalVerifiableCredential).original + return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as StatusListVerifiableCredential) + .original as StatusListVerifiableCredential } } diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index 4456aa82c..be62f763b 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -1,16 +1,15 @@ import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' import { - CompactJWT, ICredential, ICredentialStatus, IIssuer, IVerifiableCredential, - OriginalVerifiableCredential, OrPromise, StatusListCredentialIdMode, StatusListDriverType, StatusListIndexingDirection, StatusListType, + StatusListVerifiableCredential, StatusPurpose2021, } from '@sphereon/ssi-types' import { @@ -83,7 +82,7 @@ export interface UpdateStatusListFromEncodedListArgs { } export interface UpdateStatusListFromStatusListCredentialArgs { - statusListCredential: OriginalVerifiableCredential | CompactJWT + statusListCredential: StatusListVerifiableCredential // | CompactJWT keyRef?: string statusListIndex: number | string value: number | Status2021 | StatusOAuth @@ -91,7 +90,7 @@ export interface UpdateStatusListFromStatusListCredentialArgs { export interface StatusListResult { encodedList: string - statusListCredential: OriginalVerifiableCredential | CompactJWT + statusListCredential: StatusListVerifiableCredential // | CompactJWT length: number type: StatusListType proofFormat: ProofFormat @@ -152,14 +151,14 @@ export interface CreateStatusListArgs { } export interface UpdateStatusListIndexArgs { - statusListCredential: OriginalVerifiableCredential | CompactJWT + statusListCredential: StatusListVerifiableCredential // | CompactJWT keyRef?: string statusListIndex: number | string value: number | Status2021 | StatusOAuth } export interface CheckStatusIndexArgs { - statusListCredential: OriginalVerifiableCredential | CompactJWT + statusListCredential: StatusListVerifiableCredential // | CompactJWT statusListIndex: string | number } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02bafbc0a..1f36e7a11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3254,6 +3254,9 @@ importers: debug: specifier: ^4.3.5 version: 4.3.7 + jwt-decode: + specifier: ^4.0.0 + version: 4.0.0 typeorm: specifier: ^0.3.20 version: 0.3.20(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) From 0fa3d5f6a49cc77d36506ade0cdef745aff27c72 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Thu, 16 Jan 2025 18:19:10 +0100 Subject: [PATCH 10/24] chore: saving CBOR work --- packages/ssi-types/src/types/status-list.ts | 3 + packages/vc-status-list/package.json | 6 +- packages/vc-status-list/src/functions.ts | 19 ++- .../src/impl/OAuthStatusList.ts | 117 ++++++++++++++++-- .../vc-status-list/src/impl/StatusList2021.ts | 27 ++-- packages/vc-status-list/src/types/index.ts | 11 +- packages/vc-status-list/src/utils.ts | 14 ++- pnpm-lock.yaml | 27 +++- 8 files changed, 192 insertions(+), 32 deletions(-) diff --git a/packages/ssi-types/src/types/status-list.ts b/packages/ssi-types/src/types/status-list.ts index 07cfc71a1..b7803da3c 100644 --- a/packages/ssi-types/src/types/status-list.ts +++ b/packages/ssi-types/src/types/status-list.ts @@ -1,5 +1,6 @@ import { W3CVerifiableCredential } from './w3c-vc' import { MdocDocument } from './mso_mdoc' +import { ProofFormat as VmoProofFormat } from '@veramo/core/src/types/ICredentialIssuer' export enum StatusListType { StatusList2021 = 'StatusList2021', @@ -7,3 +8,5 @@ export enum StatusListType { } export type StatusListVerifiableCredential = W3CVerifiableCredential | MdocDocument + +export type ProofFormat = VmoProofFormat | 'cbor' diff --git a/packages/vc-status-list/package.json b/packages/vc-status-list/package.json index 64566c4d5..35f5cebcb 100644 --- a/packages/vc-status-list/package.json +++ b/packages/vc-status-list/package.json @@ -16,6 +16,8 @@ "@sphereon/ssi-sdk-ext.jwt-service": "0.27.0", "@sphereon/ssi-types": "workspace:*", "@sphereon/vc-status-list": "7.0.0-next.0", + "@sphereon/kmp-cbor": "0.2.0-SNAPSHOT.25", + "@sphereon/kmp-mdoc-core": "0.2.0-SNAPSHOT.26", "@veramo/core": "4.2.0", "@veramo/credential-status": "4.2.0", "base64url": "^3.0.1", @@ -23,7 +25,8 @@ "jwt-decode": "^4.0.0", "debug": "^4.3.5", "typeorm": "^0.3.20", - "uint8arrays": "^3.1.1" + "uint8arrays": "^3.1.1", + "pako": "^2.1.0" }, "devDependencies": { "@babel/cli": "^7.24.8", @@ -31,6 +34,7 @@ "@babel/preset-env": "^7.24.8", "@babel/preset-typescript": "^7.24.7", "@veramo/key-manager": "4.2.0", + "@types/pako": "2.0.3", "typescript": "5.4.2" }, "files": [ diff --git a/packages/vc-status-list/src/functions.ts b/packages/vc-status-list/src/functions.ts index b01f2f68e..767318cec 100644 --- a/packages/vc-status-list/src/functions.ts +++ b/packages/vc-status-list/src/functions.ts @@ -1,5 +1,13 @@ import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { CredentialMapper, StatusListVerifiableCredential, StatusListDriverType, StatusListType, StatusPurpose2021 } from '@sphereon/ssi-types' +import { + CredentialMapper, + ProofFormat, + StatusListDriverType, + StatusListType, + StatusListVerifiableCredential, + StatusPurpose2021, +} from '@sphereon/ssi-types' +import { ProofFormat as VmoProofFormat } from '@veramo/core/src/types/ICredentialIssuer' import { checkStatus } from '@sphereon/vc-status-list' import { CredentialStatus, DIDDocument, IAgentContext, ICredentialPlugin } from '@veramo/core' @@ -13,7 +21,7 @@ import { UpdateStatusListFromEncodedListArgs, UpdateStatusListFromStatusListCredentialArgs, } from './types' -import { getAssertedValue, getAssertedValues } from './utils' +import { assertValidProofType, getAssertedValue, getAssertedValues } from './utils' import { getStatusListImplementation } from './impl/StatusListFactory' import { jwtDecode } from 'jwt-decode' @@ -220,6 +228,7 @@ export async function updateStatusListIndexFromEncodedList( return implementation.updateStatusListFromEncodedList(args, context) } +// TODO Is this still in use? Or do we need to redesign this after having multiple status list types? export async function statusList2021ToVerifiableCredential( args: StatusList2021ToVerifiableCredentialArgs, context: IAgentContext, @@ -230,6 +239,10 @@ export async function statusList2021ToVerifiableCredential( vmRelationship: 'assertionMethod', offlineWhenNoDIDRegistered: true, // FIXME Fix identifier resolution for EBSI }) + const proofFormat: ProofFormat = args?.proofFormat ?? 'lds' + assertValidProofType(StatusListType.StatusList2021, proofFormat) + const vmoProofFormat: VmoProofFormat = proofFormat as VmoProofFormat + const encodedList = getAssertedValue('encodedList', args.encodedList) const statusPurpose = getAssertedValue('statusPurpose', args.statusPurpose) const credential = { @@ -249,7 +262,7 @@ export async function statusList2021ToVerifiableCredential( const verifiableCredential = await context.agent.createVerifiableCredential({ credential, keyRef: identifier.kmsKeyRef, - proofFormat: args.proofFormat ?? 'lds', + proofFormat: vmoProofFormat, fetchRemoteContexts: true, }) diff --git a/packages/vc-status-list/src/impl/OAuthStatusList.ts b/packages/vc-status-list/src/impl/OAuthStatusList.ts index c02fdc1b7..86ccad8dd 100644 --- a/packages/vc-status-list/src/impl/OAuthStatusList.ts +++ b/packages/vc-status-list/src/impl/OAuthStatusList.ts @@ -1,5 +1,5 @@ import { IAgentContext, ICredentialPlugin } from '@veramo/core' -import { CredentialMapper, StatusListType } from '@sphereon/ssi-types' +import { CompactJWT, CredentialMapper, ProofFormat, StatusListType, StatusListVerifiableCredential } from '@sphereon/ssi-types' import { CheckStatusIndexArgs, CreateStatusListArgs, @@ -14,12 +14,15 @@ import { createHeaderAndPayload, StatusList, StatusListJWTHeaderParameters, Stat import { JWTPayload } from 'did-jwt' import { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service' import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' +import { com, kotlin } from '@sphereon/kmp-cbor' +import base64url from 'base64url' +import { deflate } from 'pako' // extracted from @sd-jwt/jwt-status-list type IRequiredContext = IAgentContext export const BITS_PER_STATUS_DEFAULT = 2 // 2 bits are sufficient for 0x00 - "VALID" 0x01 - "INVALID" & 0x02 - "SUSPENDED" export const DEFAULT_LIST_LENGTH = 250000 -export const DEFAULT_PROOF_FORMAT = 'jwt' as const +export const DEFAULT_PROOF_FORMAT = 'jwt' as ProofFormat export const STATUS_LIST_JWT_HEADER: StatusListJWTHeaderParameters = { alg: 'EdDSA', typ: 'statuslist+jwt', @@ -45,11 +48,20 @@ export class OAuthStatusListImplementation implements IStatusList { const initialStatuses = new Array(length).fill(0) const statusList = new StatusList(initialStatuses, bitsPerStatus) const encodedList = statusList.compressStatusList() - const { jwt } = await this.createSignedPayload(context, statusList, issuerString, id, args.keyRef) + let statusListCredential: StatusListVerifiableCredential + if (proofFormat === 'jwt') { + const { jwt } = await this.createSignedJwt(context, statusList, issuerString, id, args.keyRef) + statusListCredential = jwt as CompactJWT + } else if (proofFormat === 'cbor') { + const { cbor } = await this.createSignedCbor(context, statusList, issuerString, id, args.keyRef) + statusListCredential = cbor + } else { + return Promise.reject(Error(`Unknown proofFormat ${proofFormat}`)) + } return { encodedList, - statusListCredential: jwt, + statusListCredential, oauthStatusList: { bitsPerStatus, }, @@ -64,7 +76,9 @@ export class OAuthStatusListImplementation implements IStatusList { async updateStatusListIndex(args: UpdateStatusListIndexArgs, context: IRequiredContext): Promise { const { statusListCredential, value } = args - if (!CredentialMapper.isJwtEncoded(statusListCredential) && !CredentialMapper.isMsoMdocOid4VPEncoded(statusListCredential)) { + const isJwtEncoded = CredentialMapper.isJwtEncoded(statusListCredential) + const isMsoMdocOid4VPEncoded = CredentialMapper.isMsoMdocOid4VPEncoded(statusListCredential) + if (!isJwtEncoded && !isMsoMdocOid4VPEncoded) { return Promise.reject(new Error('statusListCredential is neither a JWT nor an MDOC document')) } const sourcePayload = decodeStatusListJWT(statusListCredential) @@ -85,7 +99,7 @@ export class OAuthStatusListImplementation implements IStatusList { } statusList.setStatus(index, value) - const { jwt, encodedList } = await this.createSignedPayload(context, statusList, issuer, id, args.keyRef) + const { jwt, encodedList } = await this.createSignedJwt(context, statusList, issuer, id, args.keyRef) return { encodedList, @@ -114,7 +128,7 @@ export class OAuthStatusListImplementation implements IStatusList { const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) listToUpdate.setStatus(index, args.value ? 1 : 0) - const { jwt, encodedList } = await this.createSignedPayload(context, listToUpdate, issuerString, id, args.keyRef) + const { jwt, encodedList } = await this.createSignedJwt(context, listToUpdate, issuerString, id, args.keyRef) return { encodedList, @@ -148,13 +162,14 @@ export class OAuthStatusListImplementation implements IStatusList { return statusList.getStatus(index) } - private async createSignedPayload(context: IRequiredContext, statusList: StatusList, issuerString: string, id: string, keyRef?: string) { + private async createSignedJwt(context: IRequiredContext, statusList: StatusList, issuerString: string, id: string, keyRef?: string) { const identifier = await this.resolveIdentifier(context, issuerString, keyRef) const payload: JWTPayload = { iss: issuerString, sub: id, iat: Math.floor(new Date().getTime() / 1000), } + const values = createHeaderAndPayload(statusList, payload, STATUS_LIST_JWT_HEADER) const signedJwt = await context.agent.jwtCreateJwsCompactSignature({ issuer: { ...identifier, noIssPayloadUpdate: false }, @@ -168,6 +183,92 @@ export class OAuthStatusListImplementation implements IStatusList { } } + private async createSignedCbor( + context: IRequiredContext, + statusList: StatusList, + issuerString: string, + id: string, + keyRef?: string, + ): Promise<{ cwt: string; encodedList: string }> { + const identifier = await this.resolveIdentifier(context, issuerString, keyRef) + + const encodeStatusList = statusList.encodeStatusList() + const compressedList = deflate(encodeStatusList, { level: 9 }) + const compressedListInt8Array = new Int8Array(compressedList.buffer) + + const statusListMap = new com.sphereon.cbor.CborMap( + kotlin.collections.KtMutableMap.fromJsMap( + new Map>([ + [ + new com.sphereon.cbor.CborString('bits'), + new com.sphereon.cbor.CborUInt(com.sphereon.kmp.LongKMP.fromNumber(statusList.getBitsPerStatus())), + ], + [new com.sphereon.cbor.CborString('lst'), new com.sphereon.cbor.CborByteString(compressedListInt8Array)], + ]), + ), + ) + + const exp = Math.floor(new Date().getTime() / 1000) + const ttl = 65535 // FIXME figure out what value should be / come from and what the difference is with exp + const claimsMap = new com.sphereon.cbor.CborMap( + kotlin.collections.KtMutableMap.fromJsMap( + new Map([ + [new com.sphereon.cbor.CborUInt(2), new com.sphereon.cbor.CborString(issuerString)], // "sub" + [ + new com.sphereon.cbor.CborUInt(6), + new com.sphereon.cbor.CborUInt(com.sphereon.kmp.LongKMP.fromNumber(Math.floor(Date.now() / 1000))), // "iat" + ], + ...(exp + ? [ + [ + new com.sphereon.cbor.CborUInt(4), + new com.sphereon.cbor.CborUInt(com.sphereon.kmp.LongKMP.fromNumber(exp)), // "exp" + ], + ] + : []), + ...(ttl + ? [ + [ + new com.sphereon.cbor.CborUInt(65534), + new com.sphereon.cbor.CborUInt(com.sphereon.kmp.LongKMP.fromNumber(ttl)), // "time to live" + ], + ] + : []), + [new com.sphereon.cbor.CborUInt(65533), statusListMap], // "status list" + ]), + ), + ) + + const protectedHeader = new com.sphereon.cbor.CborMap( + kotlin.collections.KtMutableMap.fromJsMap( + new Map([[new com.sphereon.cbor.CborUInt(com.sphereon.kmp.LongKMP.fromNumber(16)), new com.sphereon.cbor.CborString('statuslist+cwt')]]), // "type" + ), + ) + const protectedHeaderEncoded = com.sphereon.cbor.Cbor.encode(protectedHeader) + const claimsEncoded = com.sphereon.cbor.Cbor.encode(claimsMap) + + const signedCWT = await context.agent.keyManagerSign({ + keyRef: identifier.kmsKeyRef, + data: claimsEncoded, + encoding: undefined, + }) + + const cwtArray = new com.sphereon.cbor.CborArray( + kotlin.collections.KtMutableList.fromJsArray([ + new com.sphereon.cbor.CborByteString(protectedHeaderEncoded), + new com.sphereon.cbor.CborByteString(claimsEncoded), + new com.sphereon.cbor.CborByteString(signedCWT.signature), + ]), + ) + const cwtEncoded = com.sphereon.cbor.Cbor.encode(cwtArray) + const cwtBuffer = Buffer.from(cwtEncoded) + const cwt = base64url.encode(cwtBuffer) + return { + cwt, + encodedList: '', // FIXME + } + } + private async resolveIdentifier(context: IRequiredContext, issuer: string, keyRef?: string) { return keyRef ? await context.agent.identifierManagedGetByKid({ diff --git a/packages/vc-status-list/src/impl/StatusList2021.ts b/packages/vc-status-list/src/impl/StatusList2021.ts index cfaab064b..e8c67df0d 100644 --- a/packages/vc-status-list/src/impl/StatusList2021.ts +++ b/packages/vc-status-list/src/impl/StatusList2021.ts @@ -1,6 +1,8 @@ -import { IAgentContext, ICredentialPlugin, ProofFormat } from '@veramo/core' +import { IAgentContext, ICredentialPlugin } from '@veramo/core' import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { CredentialMapper, DocumentFormat, IIssuer, StatusListVerifiableCredential, StatusListType } from '@sphereon/ssi-types' +import { CredentialMapper, DocumentFormat, IIssuer, ProofFormat, StatusListType, StatusListVerifiableCredential } from '@sphereon/ssi-types' +import { ProofFormat as VmoProofFormat } from '@veramo/core/src/types/ICredentialIssuer' + import { StatusList } from '@sphereon/vc-status-list' import { IStatusList } from './IStatusList' import { @@ -11,9 +13,10 @@ import { UpdateStatusListFromEncodedListArgs, UpdateStatusListIndexArgs, } from '../types' -import { getAssertedProperty, getAssertedValue, getAssertedValues } from '../utils' +import { assertValidProofType, getAssertedProperty, getAssertedValue, getAssertedValues } from '../utils' export const DEFAULT_LIST_LENGTH = 250000 +export const DEFAULT_PROOF_FORMAT = 'lds' as VmoProofFormat export class StatusList2021Implementation implements IStatusList { async createNewStatusList( @@ -21,7 +24,10 @@ export class StatusList2021Implementation implements IStatusList { context: IAgentContext, ): Promise { const length = args?.length ?? DEFAULT_LIST_LENGTH - const proofFormat: ProofFormat = args?.proofFormat ?? 'lds' + const proofFormat: ProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT + assertValidProofType(StatusListType.StatusList2021, proofFormat) + const vmoProofFormat: VmoProofFormat = proofFormat as VmoProofFormat + const { issuer, id } = args const correlationId = getAssertedValue('correlationId', args.correlationId) @@ -33,7 +39,7 @@ export class StatusList2021Implementation implements IStatusList { { ...args, encodedList, - proofFormat, + proofFormat: vmoProofFormat, }, context, ) @@ -102,8 +108,11 @@ export class StatusList2021Implementation implements IStatusList { if (!args.statusList2021) { throw new Error('statusList2021 options required for type StatusList2021') } - const { issuer, id } = getAssertedValues(args) + const proofFormat: ProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT + assertValidProofType(StatusListType.StatusList2021, proofFormat) + const vmoProofFormat: VmoProofFormat = proofFormat as VmoProofFormat + 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) @@ -114,7 +123,7 @@ export class StatusList2021Implementation implements IStatusList { id, issuer, encodedList: newEncodedList, - proofFormat: args.proofFormat, + proofFormat: vmoProofFormat, keyRef: args.keyRef, }, context, @@ -150,7 +159,7 @@ export class StatusList2021Implementation implements IStatusList { id: string issuer: string | IIssuer encodedList: string - proofFormat?: ProofFormat + proofFormat: VmoProofFormat keyRef?: string }, context: IAgentContext, @@ -177,7 +186,7 @@ export class StatusList2021Implementation implements IStatusList { const verifiableCredential = await context.agent.createVerifiableCredential({ credential, keyRef: args.keyRef ?? identifier.kmsKeyRef, - proofFormat: args.proofFormat ?? 'lds', + proofFormat: args.proofFormat, fetchRemoteContexts: true, }) diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index be62f763b..df3deba40 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -5,6 +5,7 @@ import { IIssuer, IVerifiableCredential, OrPromise, + ProofFormat, StatusListCredentialIdMode, StatusListDriverType, StatusListIndexingDirection, @@ -12,15 +13,7 @@ import { StatusListVerifiableCredential, StatusPurpose2021, } from '@sphereon/ssi-types' -import { - CredentialPayload, - IAgentContext, - ICredentialIssuer, - ICredentialPlugin, - ICredentialVerifier, - IPluginMethodMap, - ProofFormat, -} from '@veramo/core' +import { CredentialPayload, IAgentContext, ICredentialIssuer, ICredentialPlugin, ICredentialVerifier, IPluginMethodMap } from '@veramo/core' import { DataSource } from 'typeorm' import { BitsPerStatus } from '@sd-jwt/jwt-status-list/dist' diff --git a/packages/vc-status-list/src/utils.ts b/packages/vc-status-list/src/utils.ts index a9d856f02..d11777397 100644 --- a/packages/vc-status-list/src/utils.ts +++ b/packages/vc-status-list/src/utils.ts @@ -1,4 +1,4 @@ -import { CompactJWT, IIssuer, StatusListType, StatusListType as StatusListTypeW3C } from '@sphereon/ssi-types' +import { CompactJWT, IIssuer, ProofFormat, StatusListType, StatusListType as StatusListTypeW3C } from '@sphereon/ssi-types' import { StatusListJWTPayload } from '@sd-jwt/jwt-status-list' import base64url from 'base64url' @@ -35,3 +35,15 @@ export function getAssertedProperty(propertyName: string, obj: } return getAssertedValue(propertyName, (obj as any)[propertyName]) } + +const ValidProofTypeMap = new Map([ + [StatusListType.StatusList2021, ['jwt', 'lds', 'EthereumEip712Signature2021']], + [StatusListType.OAuthStatusList, ['jwt', 'cbor']], +]) + +export function assertValidProofType(type: StatusListType, proofFormat: ProofFormat) { + const validProofTypes = ValidProofTypeMap.get(type) + if (!validProofTypes?.includes(proofFormat)) { + throw Error(`Invalid proof format '${proofFormat}' for status list type ${type}`) + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f36e7a11..abb01e432 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3224,6 +3224,12 @@ importers: '@sd-jwt/jwt-status-list': specifier: ^0.9.1 version: 0.9.1 + '@sphereon/kmp-cbor': + specifier: 0.2.0-SNAPSHOT.25 + version: 0.2.0-SNAPSHOT.25 + '@sphereon/kmp-mdoc-core': + specifier: 0.2.0-SNAPSHOT.26 + version: 0.2.0-SNAPSHOT.26 '@sphereon/ssi-sdk-ext.did-utils': specifier: 0.27.0 version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) @@ -3257,6 +3263,9 @@ importers: jwt-decode: specifier: ^4.0.0 version: 4.0.0 + pako: + specifier: ^2.1.0 + version: 2.1.0 typeorm: specifier: ^0.3.20 version: 0.3.20(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) @@ -3276,6 +3285,9 @@ importers: '@babel/preset-typescript': specifier: ^7.24.7 version: 7.26.0(@babel/core@7.26.0) + '@types/pako': + specifier: 2.0.3 + version: 2.0.3 '@veramo/key-manager': specifier: 4.2.0 version: 4.2.0 @@ -6123,9 +6135,12 @@ packages: resolution: {integrity: sha512-FJTzPcNMEzEL1pK8QEnKIbZ8EnbU3yv2R9YiuIMPbHrEQuROq/MduqWmjWTKGPjFeNhZDJ8JSebN2pefhowCRA==} engines: {node: '>=18'} + '@sphereon/kmp-cbor@0.2.0-SNAPSHOT.25': + resolution: {integrity: sha512-EFuAtA4zaONe1/BGOntx9m1Oov6iVSMhbjhht6ST/6hFx+VMCs5iDTnl8qay3dleIOoDEluK8AnSiufi56+IJA==} + bundledDependencies: [] + '@sphereon/kmp-mdoc-core@0.2.0-SNAPSHOT.26': resolution: {integrity: sha512-QXJ6R8ENiZV2rPMbn06cw5JKwqUYN1kzVRbYfONqE1PEXx1noQ4md7uxr2zSczi0ubKkNcbyYDNtIMTZIhGzmQ==} - bundledDependencies: [] '@sphereon/lto-did-ts@0.1.8-unstable.0': resolution: {integrity: sha512-3jzwwuYX/VYuze+T9/yg4PcsJ5iNNwAfTp4WfS4aSfPFBErDAfKXqn6kOb0wFYGkhejr3Jz+rljPC2iKZiHiGA==} @@ -15986,6 +16001,10 @@ snapshots: '@js-joda/core@5.6.3': {} + '@js-joda/timezone@2.3.0(@js-joda/core@3.2.0)': + dependencies: + '@js-joda/core': 3.2.0 + '@js-joda/timezone@2.3.0(@js-joda/core@5.6.3)': dependencies: '@js-joda/core': 5.6.3 @@ -17165,6 +17184,12 @@ snapshots: transitivePeerDependencies: - typescript + '@sphereon/kmp-cbor@0.2.0-SNAPSHOT.25': + dependencies: + '@js-joda/core': 3.2.0 + '@js-joda/timezone': 2.3.0(@js-joda/core@3.2.0) + format-util: 1.0.5 + '@sphereon/kmp-mdoc-core@0.2.0-SNAPSHOT.26': dependencies: '@js-joda/core': 5.6.3 From d7cb05c03376cd515992ec3211ee74c7ce737ed3 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 17 Jan 2025 00:44:44 +0100 Subject: [PATCH 11/24] chore: CBOR test passing --- .../entities/statusList/StatusListEntities.ts | 2 +- .../src/types/statusList/statusList.ts | 2 +- .../__tests__/statuslist.test.ts | 36 ++- packages/vc-status-list/package.json | 1 - packages/vc-status-list/src/functions.ts | 3 + .../src/impl/OAuthStatusList.ts | 227 +++++------------- .../vc-status-list/src/impl/encoding/cbor.ts | 152 ++++++++++++ .../src/impl/encoding/common.ts | 25 ++ .../vc-status-list/src/impl/encoding/jwt.ts | 54 +++++ packages/vc-status-list/src/types/index.ts | 5 + packages/vc-status-list/src/utils.ts | 9 +- pnpm-lock.yaml | 4 +- 12 files changed, 334 insertions(+), 186 deletions(-) create mode 100644 packages/vc-status-list/src/impl/encoding/cbor.ts create mode 100644 packages/vc-status-list/src/impl/encoding/common.ts create mode 100644 packages/vc-status-list/src/impl/encoding/jwt.ts diff --git a/packages/data-store/src/entities/statusList/StatusListEntities.ts b/packages/data-store/src/entities/statusList/StatusListEntities.ts index e0b3e2ccb..20bf9ad85 100644 --- a/packages/data-store/src/entities/statusList/StatusListEntities.ts +++ b/packages/data-store/src/entities/statusList/StatusListEntities.ts @@ -6,8 +6,8 @@ import { StatusListIndexingDirection, StatusListType, StatusPurpose2021, + ProofFormat, } from '@sphereon/ssi-types' -import { ProofFormat } from '@veramo/core' import { BaseEntity, ChildEntity, Column, Entity, OneToMany, PrimaryColumn, TableInheritance, Unique } from 'typeorm' import { StatusListEntryEntity } from './StatusList2021EntryEntity' diff --git a/packages/data-store/src/types/statusList/statusList.ts b/packages/data-store/src/types/statusList/statusList.ts index f401f9adf..c27c4ac9b 100644 --- a/packages/data-store/src/types/statusList/statusList.ts +++ b/packages/data-store/src/types/statusList/statusList.ts @@ -6,8 +6,8 @@ import { StatusListIndexingDirection, StatusListType, StatusPurpose2021, + ProofFormat, } from '@sphereon/ssi-types' -import { ProofFormat } from '@veramo/core' import { StatusListEntity } from '../../entities/statusList/StatusListEntities' export interface IStatusListEntity { diff --git a/packages/vc-status-list-tests/__tests__/statuslist.test.ts b/packages/vc-status-list-tests/__tests__/statuslist.test.ts index 39444fe88..5ac0192ef 100644 --- a/packages/vc-status-list-tests/__tests__/statuslist.test.ts +++ b/packages/vc-status-list-tests/__tests__/statuslist.test.ts @@ -164,6 +164,37 @@ describe('Status list', () => { expect(status).toBe(StatusOAuth.Invalid) }) + it('should create and update using CBOR format', async () => { + const statusList = await createNewStatusList( + { + type: StatusListType.OAuthStatusList, + proofFormat: 'cbor', + id: 'http://localhost:9543/oauth3', + issuer: didKeyIdentifier.did, + length: 99999, + correlationId: 'test-6-' + Date.now(), + oauthStatusList: { + bitsPerStatus: 2, + }, + }, + { agent }, + ) + + const updated = await updateStatusIndexFromStatusListCredential( + { + statusListCredential: statusList.statusListCredential, + statusListIndex: 5, + value: StatusOAuth.Suspended, + }, + { agent }, + ) + const status = await checkStatusIndexFromStatusListCredential({ + statusListCredential: updated.statusListCredential, + statusListIndex: '5', + }) + expect(status).toBe(StatusOAuth.Suspended) + }) + it('should reject LD-Signatures format', async () => { await expect( createNewStatusList( @@ -171,6 +202,7 @@ describe('Status list', () => { type: StatusListType.OAuthStatusList, proofFormat: 'lds', id: 'http://localhost:9543/oauth2', + correlationId: 'test-4-' + Date.now(), issuer: didKeyIdentifier.did, length: 99999, oauthStatusList: { @@ -191,7 +223,7 @@ describe('Status list', () => { type: StatusListType.StatusList2021, proofFormat: 'jwt', id: 'http://localhost:9543/encoded1', - correlationId: 'test-4-' + Date.now(), + correlationId: 'test-5-' + Date.now(), issuer: didKeyIdentifier.did, length: 1000, statusList2021: { @@ -228,7 +260,7 @@ describe('Status list', () => { type: StatusListType.OAuthStatusList, proofFormat: 'jwt', id: 'http://localhost:9543/encoded2', - correlationId: 'test-5-' + Date.now(), + correlationId: 'test-6-' + Date.now(), issuer: didKeyIdentifier.did, length: 1000, oauthStatusList: { diff --git a/packages/vc-status-list/package.json b/packages/vc-status-list/package.json index 35f5cebcb..0d2a8a6f9 100644 --- a/packages/vc-status-list/package.json +++ b/packages/vc-status-list/package.json @@ -17,7 +17,6 @@ "@sphereon/ssi-types": "workspace:*", "@sphereon/vc-status-list": "7.0.0-next.0", "@sphereon/kmp-cbor": "0.2.0-SNAPSHOT.25", - "@sphereon/kmp-mdoc-core": "0.2.0-SNAPSHOT.26", "@veramo/core": "4.2.0", "@veramo/credential-status": "4.2.0", "base64url": "^3.0.1", diff --git a/packages/vc-status-list/src/functions.ts b/packages/vc-status-list/src/functions.ts index 767318cec..b55d5f5d1 100644 --- a/packages/vc-status-list/src/functions.ts +++ b/packages/vc-status-list/src/functions.ts @@ -169,6 +169,9 @@ function determineStatusListType(credential: StatusListVerifiableCredential): St if (!CredentialMapper.isCredential(payload) && 'status_list' in payload) { return StatusListType.OAuthStatusList } + } else if (CredentialMapper.isMsoMdocOid4VPEncoded(credential)) { + // Just assume Cbor status list for now, I'd need to decode the Cbor to know what it is + return StatusListType.OAuthStatusList } if (CredentialMapper.isCredential(credential)) { diff --git a/packages/vc-status-list/src/impl/OAuthStatusList.ts b/packages/vc-status-list/src/impl/OAuthStatusList.ts index 86ccad8dd..532bb2858 100644 --- a/packages/vc-status-list/src/impl/OAuthStatusList.ts +++ b/packages/vc-status-list/src/impl/OAuthStatusList.ts @@ -1,5 +1,5 @@ import { IAgentContext, ICredentialPlugin } from '@veramo/core' -import { CompactJWT, CredentialMapper, ProofFormat, StatusListType, StatusListVerifiableCredential } from '@sphereon/ssi-types' +import { CredentialMapper, ProofFormat, StatusListType, StatusListVerifiableCredential } from '@sphereon/ssi-types' import { CheckStatusIndexArgs, CreateStatusListArgs, @@ -8,15 +8,13 @@ import { UpdateStatusListFromEncodedListArgs, UpdateStatusListIndexArgs, } from '../types' -import { decodeStatusListJWT, getAssertedValue, getAssertedValues } from '../utils' +import { getAssertedValue, getAssertedValues } from '../utils' import { IStatusList } from './IStatusList' -import { createHeaderAndPayload, StatusList, StatusListJWTHeaderParameters, StatusListJWTPayload } from '@sd-jwt/jwt-status-list' -import { JWTPayload } from 'did-jwt' +import { StatusList, StatusListJWTHeaderParameters } from '@sd-jwt/jwt-status-list' import { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service' import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { com, kotlin } from '@sphereon/kmp-cbor' -import base64url from 'base64url' -import { deflate } from 'pako' // extracted from @sd-jwt/jwt-status-list +import { createSignedJwt, decodeStatusListJWT } from './encoding/jwt' +import { createSignedCbor, decodeStatusListCWT } from './encoding/cbor' type IRequiredContext = IAgentContext @@ -35,36 +33,35 @@ export class OAuthStatusListImplementation implements IStatusList { } const proofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT - if (proofFormat !== DEFAULT_PROOF_FORMAT) { - throw new Error(`Invalid proof format '${proofFormat}' for OAuthStatusList`) - } - const { issuer, id } = args const length = args.length ?? DEFAULT_LIST_LENGTH const bitsPerStatus = args.oauthStatusList.bitsPerStatus ?? BITS_PER_STATUS_DEFAULT const issuerString = typeof issuer === 'string' ? issuer : issuer.id const correlationId = getAssertedValue('correlationId', args.correlationId) - const initialStatuses = new Array(length).fill(0) - const statusList = new StatusList(initialStatuses, bitsPerStatus) + const statusList = new StatusList(new Array(length).fill(0), bitsPerStatus) const encodedList = statusList.compressStatusList() let statusListCredential: StatusListVerifiableCredential - if (proofFormat === 'jwt') { - const { jwt } = await this.createSignedJwt(context, statusList, issuerString, id, args.keyRef) - statusListCredential = jwt as CompactJWT - } else if (proofFormat === 'cbor') { - const { cbor } = await this.createSignedCbor(context, statusList, issuerString, id, args.keyRef) - statusListCredential = cbor - } else { - return Promise.reject(Error(`Unknown proofFormat ${proofFormat}`)) + + switch (proofFormat) { + case 'jwt': { + const { statusListCredential: slJwt } = await createSignedJwt(context, statusList, issuerString, id, args.keyRef) + statusListCredential = slJwt + break + } + case 'cbor': { + const { statusListCredential: slCbor } = await createSignedCbor(context, statusList, issuerString, id, args.keyRef) + statusListCredential = slCbor + break + } + default: + throw new Error(`Invalid proof format '${proofFormat}' for OAuthStatusList`) } return { encodedList, statusListCredential, - oauthStatusList: { - bitsPerStatus, - }, + oauthStatusList: { bitsPerStatus }, length, type: StatusListType.OAuthStatusList, proofFormat, @@ -77,21 +74,15 @@ export class OAuthStatusListImplementation implements IStatusList { async updateStatusListIndex(args: UpdateStatusListIndexArgs, context: IRequiredContext): Promise { const { statusListCredential, value } = args const isJwtEncoded = CredentialMapper.isJwtEncoded(statusListCredential) - const isMsoMdocOid4VPEncoded = CredentialMapper.isMsoMdocOid4VPEncoded(statusListCredential) - if (!isJwtEncoded && !isMsoMdocOid4VPEncoded) { - return Promise.reject(new Error('statusListCredential is neither a JWT nor an MDOC document')) - } - const sourcePayload = decodeStatusListJWT(statusListCredential) - if (!('iss' in sourcePayload)) { - throw new Error('issuer (iss) is missing in the status list JWT') - } - if (!('sub' in sourcePayload)) { - throw new Error('List id (sub) is missing in the status list JWT') + const isCborEncoded = CredentialMapper.isMsoMdocOid4VPEncoded(statusListCredential) + const proofFormat = isJwtEncoded ? 'jwt' : isCborEncoded ? 'cbor' : DEFAULT_PROOF_FORMAT + + if (!isJwtEncoded && !isCborEncoded) { + throw new Error('statusListCredential is neither a JWT nor a CBOR document') } - const { iss: issuer, sub: id } = sourcePayload as StatusListJWTPayload & { iss: string; sub: string } - const statusListContainer = sourcePayload.status_list - const statusList = StatusList.decompressStatusList(statusListContainer.lst, statusListContainer.bits) + const decoded = isJwtEncoded ? 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) { @@ -99,17 +90,19 @@ export class OAuthStatusListImplementation implements IStatusList { } statusList.setStatus(index, value) - const { jwt, encodedList } = await this.createSignedJwt(context, statusList, issuer, id, args.keyRef) + const result = + proofFormat === 'jwt' + ? await createSignedJwt(context, statusList, issuer, id, args.keyRef) + : await createSignedCbor(context, statusList, issuer, id, args.keyRef) return { - encodedList, - statusListCredential: jwt, + ...result, oauthStatusList: { - bitsPerStatus: statusListContainer.bits, + bitsPerStatus: statusList.getBitsPerStatus(), }, length: statusList.statusList.length, type: StatusListType.OAuthStatusList, - proofFormat: DEFAULT_PROOF_FORMAT, + proofFormat, id, issuer, } @@ -120,6 +113,7 @@ export class OAuthStatusListImplementation implements IStatusList { throw new Error('OAuthStatusList options are required for type OAuthStatusList') } + const proofFormat = args.proofFormat ?? DEFAULT_PROOF_FORMAT const { issuer, id } = getAssertedValues(args) const bitsPerStatus = args.oauthStatusList.bitsPerStatus ?? BITS_PER_STATUS_DEFAULT const issuerString = typeof issuer === 'string' ? issuer : issuer.id @@ -128,17 +122,28 @@ export class OAuthStatusListImplementation implements IStatusList { const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) listToUpdate.setStatus(index, args.value ? 1 : 0) - const { jwt, encodedList } = await this.createSignedJwt(context, listToUpdate, issuerString, id, args.keyRef) + let result: { statusListCredential: StatusListVerifiableCredential; encodedList: string } + + switch (proofFormat) { + case 'jwt': + result = await createSignedJwt(context, listToUpdate, issuerString, id, args.keyRef) + break + case 'cbor': + result = await createSignedCbor(context, listToUpdate, issuerString, id, args.keyRef) + break + default: + throw new Error(`Invalid proof format '${proofFormat}' for OAuthStatusList`) + } return { - encodedList, - statusListCredential: jwt, + encodedList: result.encodedList, + statusListCredential: result.statusListCredential, oauthStatusList: { bitsPerStatus, }, length: listToUpdate.statusList.length, type: StatusListType.OAuthStatusList, - proofFormat: args.proofFormat ?? DEFAULT_PROOF_FORMAT, + proofFormat, id, issuer, } @@ -146,13 +151,14 @@ export class OAuthStatusListImplementation implements IStatusList { async checkStatusIndex(args: CheckStatusIndexArgs): Promise { const { statusListCredential, statusListIndex } = args - if (!CredentialMapper.isJwtEncoded(statusListCredential) && !CredentialMapper.isMsoMdocOid4VPEncoded(statusListCredential)) { - return Promise.reject(new Error('statusListCredential is neither a JWT nor an MDOC document')) + const isJwtEncoded = CredentialMapper.isJwtEncoded(statusListCredential) + const isCborEncoded = CredentialMapper.isMsoMdocOid4VPEncoded(statusListCredential) + + if (!isJwtEncoded && !isCborEncoded) { + throw new Error('statusListCredential is neither a JWT nor a CBOR document') } - const sourcePayload = decodeStatusListJWT(statusListCredential) - const statusListContainer = sourcePayload.status_list - const statusList = StatusList.decompressStatusList(statusListContainer.lst, statusListContainer.bits) + const { statusList } = isJwtEncoded ? decodeStatusListJWT(statusListCredential) : decodeStatusListCWT(statusListCredential) const index = typeof statusListIndex === 'number' ? statusListIndex : parseInt(statusListIndex) if (index < 0 || index >= statusList.statusList.length) { @@ -161,123 +167,4 @@ export class OAuthStatusListImplementation implements IStatusList { return statusList.getStatus(index) } - - private async createSignedJwt(context: IRequiredContext, statusList: StatusList, issuerString: string, id: string, keyRef?: string) { - const identifier = await this.resolveIdentifier(context, issuerString, keyRef) - const payload: JWTPayload = { - iss: issuerString, - sub: id, - iat: Math.floor(new Date().getTime() / 1000), - } - - const values = createHeaderAndPayload(statusList, payload, STATUS_LIST_JWT_HEADER) - const signedJwt = await context.agent.jwtCreateJwsCompactSignature({ - issuer: { ...identifier, noIssPayloadUpdate: false }, - protectedHeader: values.header, - payload: values.payload, - }) - - return { - jwt: signedJwt.jwt, - encodedList: (values.payload as StatusListJWTPayload).status_list.lst, - } - } - - private async createSignedCbor( - context: IRequiredContext, - statusList: StatusList, - issuerString: string, - id: string, - keyRef?: string, - ): Promise<{ cwt: string; encodedList: string }> { - const identifier = await this.resolveIdentifier(context, issuerString, keyRef) - - const encodeStatusList = statusList.encodeStatusList() - const compressedList = deflate(encodeStatusList, { level: 9 }) - const compressedListInt8Array = new Int8Array(compressedList.buffer) - - const statusListMap = new com.sphereon.cbor.CborMap( - kotlin.collections.KtMutableMap.fromJsMap( - new Map>([ - [ - new com.sphereon.cbor.CborString('bits'), - new com.sphereon.cbor.CborUInt(com.sphereon.kmp.LongKMP.fromNumber(statusList.getBitsPerStatus())), - ], - [new com.sphereon.cbor.CborString('lst'), new com.sphereon.cbor.CborByteString(compressedListInt8Array)], - ]), - ), - ) - - const exp = Math.floor(new Date().getTime() / 1000) - const ttl = 65535 // FIXME figure out what value should be / come from and what the difference is with exp - const claimsMap = new com.sphereon.cbor.CborMap( - kotlin.collections.KtMutableMap.fromJsMap( - new Map([ - [new com.sphereon.cbor.CborUInt(2), new com.sphereon.cbor.CborString(issuerString)], // "sub" - [ - new com.sphereon.cbor.CborUInt(6), - new com.sphereon.cbor.CborUInt(com.sphereon.kmp.LongKMP.fromNumber(Math.floor(Date.now() / 1000))), // "iat" - ], - ...(exp - ? [ - [ - new com.sphereon.cbor.CborUInt(4), - new com.sphereon.cbor.CborUInt(com.sphereon.kmp.LongKMP.fromNumber(exp)), // "exp" - ], - ] - : []), - ...(ttl - ? [ - [ - new com.sphereon.cbor.CborUInt(65534), - new com.sphereon.cbor.CborUInt(com.sphereon.kmp.LongKMP.fromNumber(ttl)), // "time to live" - ], - ] - : []), - [new com.sphereon.cbor.CborUInt(65533), statusListMap], // "status list" - ]), - ), - ) - - const protectedHeader = new com.sphereon.cbor.CborMap( - kotlin.collections.KtMutableMap.fromJsMap( - new Map([[new com.sphereon.cbor.CborUInt(com.sphereon.kmp.LongKMP.fromNumber(16)), new com.sphereon.cbor.CborString('statuslist+cwt')]]), // "type" - ), - ) - const protectedHeaderEncoded = com.sphereon.cbor.Cbor.encode(protectedHeader) - const claimsEncoded = com.sphereon.cbor.Cbor.encode(claimsMap) - - const signedCWT = await context.agent.keyManagerSign({ - keyRef: identifier.kmsKeyRef, - data: claimsEncoded, - encoding: undefined, - }) - - const cwtArray = new com.sphereon.cbor.CborArray( - kotlin.collections.KtMutableList.fromJsArray([ - new com.sphereon.cbor.CborByteString(protectedHeaderEncoded), - new com.sphereon.cbor.CborByteString(claimsEncoded), - new com.sphereon.cbor.CborByteString(signedCWT.signature), - ]), - ) - const cwtEncoded = com.sphereon.cbor.Cbor.encode(cwtArray) - const cwtBuffer = Buffer.from(cwtEncoded) - const cwt = base64url.encode(cwtBuffer) - return { - cwt, - encodedList: '', // FIXME - } - } - - private async resolveIdentifier(context: IRequiredContext, issuer: string, keyRef?: string) { - return keyRef - ? await context.agent.identifierManagedGetByKid({ - identifier: keyRef, - }) - : await context.agent.identifierManagedGet({ - identifier: issuer, - vmRelationship: 'assertionMethod', - offlineWhenNoDIDRegistered: true, - }) - } } diff --git a/packages/vc-status-list/src/impl/encoding/cbor.ts b/packages/vc-status-list/src/impl/encoding/cbor.ts new file mode 100644 index 000000000..e8198512a --- /dev/null +++ b/packages/vc-status-list/src/impl/encoding/cbor.ts @@ -0,0 +1,152 @@ +import { StatusList } from '@sd-jwt/jwt-status-list' +import { deflate, inflate } from 'pako' +import { com, kotlin } from '@sphereon/kmp-cbor' +import base64url from 'base64url' +import { IRequiredContext, SignedStatusListData } from '../../types' +import { DecodedStatusListPayload, resolveIdentifier } from './common' +import { BitsPerStatus } from '@sd-jwt/jwt-status-list/dist' + +const cbor = com.sphereon.cbor +const kmp = com.sphereon.kmp +const decompressRawStatusList = (StatusList as any).decodeStatusList.bind(StatusList) + +const CWT_CLAIMS = { + SUBJECT: 2, + ISSUER: 1, + ISSUED_AT: 6, + EXPIRATION: 4, + TIME_TO_LIVE: 65534, + STATUS_LIST: 65533, +} as const + +export const createSignedCbor = async ( + context: IRequiredContext, + statusList: StatusList, + issuerString: string, + id: string, + keyRef?: string, +): Promise => { + const identifier = await resolveIdentifier(context, issuerString, keyRef) + + const encodeStatusList = statusList.encodeStatusList() + const compressedList = deflate(encodeStatusList, { level: 9 }) + const compressedBytes = new Int8Array(compressedList) + + const statusListMap = new cbor.CborMap( + kotlin.collections.KtMutableMap.fromJsMap( + new Map>([ + [new cbor.CborString('bits'), new cbor.CborUInt(kmp.LongKMP.fromNumber(statusList.getBitsPerStatus()))], + [new cbor.CborString('lst'), new cbor.CborByteString(compressedBytes)], + ]), + ), + ) + + const exp = Math.floor(new Date().getTime() / 1000) + const ttl = 65535 // FIXME figure out what value should be / come from and what the difference is with exp + const claimsEntries: Array<[com.sphereon.cbor.CborUInt, com.sphereon.cbor.CborItem]> = [ + [new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.SUBJECT)), new cbor.CborString(id)], // "sub" + [new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.ISSUER)), new cbor.CborString(issuerString)], // "iss" + [ + new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.ISSUED_AT)), + new cbor.CborUInt(kmp.LongKMP.fromNumber(Math.floor(Date.now() / 1000))), // "iat" + ], + ] + + if (exp) { + claimsEntries.push([ + new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.EXPIRATION)), + new cbor.CborUInt(kmp.LongKMP.fromNumber(exp)), // "exp" + ]) + } + + if (ttl) { + claimsEntries.push([ + new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.TIME_TO_LIVE)), + new cbor.CborUInt(kmp.LongKMP.fromNumber(ttl)), // "time to live" + ]) + } + + claimsEntries.push([new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.STATUS_LIST)), statusListMap]) + + const claimsMap = new cbor.CborMap(kotlin.collections.KtMutableMap.fromJsMap(new Map(claimsEntries))) + + const protectedHeader = new cbor.CborMap( + kotlin.collections.KtMutableMap.fromJsMap( + new Map([[new cbor.CborUInt(kmp.LongKMP.fromNumber(16)), new cbor.CborString('statuslist+cwt')]]), // "type" + ), + ) + const protectedHeaderEncoded = cbor.Cbor.encode(protectedHeader) + const claimsEncoded = cbor.Cbor.encode(claimsMap) + + const signedCWT = await context.agent.keyManagerSign({ + keyRef: identifier.kmsKeyRef, + data: claimsEncoded, + encoding: undefined, + }) + + const protectedHeaderEncodedInt8 = new Int8Array(protectedHeaderEncoded) + const claimsEncodedInt8 = new Int8Array(claimsEncoded) + const signatureInt8 = new Int8Array(signedCWT.signature) + + const cwtArray = new cbor.CborArray( + kotlin.collections.KtMutableList.fromJsArray([ + new cbor.CborByteString(protectedHeaderEncodedInt8), + new cbor.CborByteString(claimsEncodedInt8), + new cbor.CborByteString(signatureInt8), + ]), + ) + + const cwtEncoded = cbor.Cbor.encode(cwtArray) + const cwtBuffer = Buffer.from(cwtEncoded) + return { + statusListCredential: base64url.encode(cwtBuffer), + encodedList: base64url.encode(compressedList as Buffer), // JS in @sd-jwt/jwt-status-list drops it in like this, so keep the same method + } +} + +const getCborValueFromMap = (map: Map, com.sphereon.cbor.CborItem>, key: number): T | never => { + const value = map.get(new com.sphereon.cbor.CborUInt(kmp.LongKMP.fromNumber(key))) + if (!value) { + throw new Error(`Required claim ${key} not found`) + } + return value.value as T +} + +export const decodeStatusListCWT = (cwt: string): DecodedStatusListPayload => { + const encodedCbor = base64url.toBuffer(cwt) + const encodedCborArray = new Int8Array(encodedCbor) + const decodedCbor = com.sphereon.cbor.Cbor.decode(encodedCborArray) + + if (!(decodedCbor instanceof com.sphereon.cbor.CborArray)) { + throw new Error('Invalid CWT format: Expected a CBOR array') + } + + const [, payload] = decodedCbor.value.asJsArrayView() + if (!(payload instanceof com.sphereon.cbor.CborByteString)) { + throw new Error('Invalid payload format: Expected a CBOR ByteString') + } + + const claims = com.sphereon.cbor.Cbor.decode(payload.value) + if (!(claims instanceof com.sphereon.cbor.CborMap)) { + throw new Error('Invalid claims format: Expected a CBOR map') + } + + const claimsMap = claims.value.asJsMapView() + + const statusListMap = claimsMap.get(new com.sphereon.cbor.CborUInt(kmp.LongKMP.fromNumber(65533))).value.asJsMapView() + + const bits = Number(statusListMap.get(new com.sphereon.cbor.CborString('bits')).value) as BitsPerStatus + const decoded = new Uint8Array(statusListMap.get(new com.sphereon.cbor.CborString('lst')).value) + const uint8Array = inflate(decoded) + const rawStatusList = decompressRawStatusList(uint8Array, bits) + const statusList = new StatusList(rawStatusList, bits) + + return { + issuer: getCborValueFromMap(claimsMap, CWT_CLAIMS.ISSUER), + id: getCborValueFromMap(claimsMap, CWT_CLAIMS.SUBJECT), + statusList, + exp: getCborValueFromMap(claimsMap, CWT_CLAIMS.EXPIRATION), + ttl: getCborValueFromMap(claimsMap, CWT_CLAIMS.TIME_TO_LIVE), + iat: Number(getCborValueFromMap(claimsMap, CWT_CLAIMS.ISSUED_AT)), + } +} diff --git a/packages/vc-status-list/src/impl/encoding/common.ts b/packages/vc-status-list/src/impl/encoding/common.ts new file mode 100644 index 000000000..1d3cddc7f --- /dev/null +++ b/packages/vc-status-list/src/impl/encoding/common.ts @@ -0,0 +1,25 @@ +import { IRequiredContext } from '../../types' +import { StatusList } from '@sd-jwt/jwt-status-list' + +export interface DecodedStatusListPayload { + issuer: string + id: string + statusList: StatusList + exp?: number + ttl?: number + iat: number +} + +export const resolveIdentifier = async (context: IRequiredContext, issuer: string, keyRef?: string) => { + if (keyRef) { + return await context.agent.identifierManagedGetByKid({ + identifier: keyRef, + }) + } + + return await context.agent.identifierManagedGet({ + identifier: issuer, + vmRelationship: 'assertionMethod', + offlineWhenNoDIDRegistered: true, + }) +} diff --git a/packages/vc-status-list/src/impl/encoding/jwt.ts b/packages/vc-status-list/src/impl/encoding/jwt.ts new file mode 100644 index 000000000..37678886f --- /dev/null +++ b/packages/vc-status-list/src/impl/encoding/jwt.ts @@ -0,0 +1,54 @@ +import { CompactJWT } from '@sphereon/ssi-types' +import { createHeaderAndPayload, StatusList, StatusListJWTPayload } from '@sd-jwt/jwt-status-list' +import base64url from 'base64url' +import { JWTPayload } from 'did-jwt' +import { STATUS_LIST_JWT_HEADER } from '../OAuthStatusList' +import { IRequiredContext, SignedStatusListData } from '../../types' +import { DecodedStatusListPayload, resolveIdentifier } from './common' + +export const createSignedJwt = async ( + context: IRequiredContext, + statusList: StatusList, + issuerString: string, + id: string, + keyRef?: string, +): Promise => { + const identifier = await resolveIdentifier(context, issuerString, keyRef) + const payload: JWTPayload = { + iss: issuerString, + sub: id, + iat: Math.floor(Date.now() / 1000), + } + + const values = createHeaderAndPayload(statusList, payload, STATUS_LIST_JWT_HEADER) + const signedJwt = await context.agent.jwtCreateJwsCompactSignature({ + issuer: { ...identifier, noIssPayloadUpdate: false }, + protectedHeader: values.header, + payload: values.payload, + }) + + return { + statusListCredential: signedJwt.jwt, + encodedList: (values.payload as StatusListJWTPayload).status_list.lst, + } +} + +export const decodeStatusListJWT = (jwt: CompactJWT): DecodedStatusListPayload => { + const [, payloadBase64] = jwt.split('.') + const payload = JSON.parse(base64url.decode(payloadBase64)) + + if (!payload.iss || !payload.sub || !payload.status_list) { + throw new Error('Missing required fields in JWT payload') + } + + const statusList = StatusList.decompressStatusList(payload.status_list.lst, payload.status_list.bits) + + return { + issuer: payload.iss, + id: payload.sub, + statusList, + exp: payload.exp, + ttl: payload.ttl, + iat: payload.iat, + } +} diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index df3deba40..d735f3dc7 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -223,5 +223,10 @@ export type GetStatusListArgs = { export type CredentialWithStatusSupport = ICredential | CredentialPayload | IVerifiableCredential +export type SignedStatusListData = { + statusListCredential: StatusListVerifiableCredential + encodedList: string +} + export type IRequiredPlugins = ICredentialPlugin & IIdentifierResolution export type IRequiredContext = IAgentContext diff --git a/packages/vc-status-list/src/utils.ts b/packages/vc-status-list/src/utils.ts index d11777397..f5653bc99 100644 --- a/packages/vc-status-list/src/utils.ts +++ b/packages/vc-status-list/src/utils.ts @@ -1,11 +1,4 @@ -import { CompactJWT, IIssuer, ProofFormat, StatusListType, StatusListType as StatusListTypeW3C } from '@sphereon/ssi-types' -import { StatusListJWTPayload } from '@sd-jwt/jwt-status-list' -import base64url from 'base64url' - -export function decodeStatusListJWT(jwt: CompactJWT): StatusListJWTPayload { - const parts = jwt.split('.') - return JSON.parse(base64url.decode(parts[1])) -} +import { IIssuer, ProofFormat, StatusListType, StatusListType as StatusListTypeW3C } from '@sphereon/ssi-types' export function getAssertedStatusListType(type?: StatusListType) { const assertedType = type ?? StatusListType.StatusList2021 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abb01e432..003cb6b18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3227,9 +3227,6 @@ importers: '@sphereon/kmp-cbor': specifier: 0.2.0-SNAPSHOT.25 version: 0.2.0-SNAPSHOT.25 - '@sphereon/kmp-mdoc-core': - specifier: 0.2.0-SNAPSHOT.26 - version: 0.2.0-SNAPSHOT.26 '@sphereon/ssi-sdk-ext.did-utils': specifier: 0.27.0 version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) @@ -6141,6 +6138,7 @@ packages: '@sphereon/kmp-mdoc-core@0.2.0-SNAPSHOT.26': resolution: {integrity: sha512-QXJ6R8ENiZV2rPMbn06cw5JKwqUYN1kzVRbYfONqE1PEXx1noQ4md7uxr2zSczi0ubKkNcbyYDNtIMTZIhGzmQ==} + bundledDependencies: [] '@sphereon/lto-did-ts@0.1.8-unstable.0': resolution: {integrity: sha512-3jzwwuYX/VYuze+T9/yg4PcsJ5iNNwAfTp4WfS4aSfPFBErDAfKXqn6kOb0wFYGkhejr3Jz+rljPC2iKZiHiGA==} From bff9ebcf60fa8ed194464ac2de4e9cd71b15fcbf Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 17 Jan 2025 01:02:08 +0100 Subject: [PATCH 12/24] chore: datastore test passing --- .../src/__tests__/statusList.store.test.ts | 2 +- .../src/statusList/IStatusListStore.ts | 2 +- .../src/statusList/StatusListStore.ts | 28 ++++++++++++++----- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/data-store/src/__tests__/statusList.store.test.ts b/packages/data-store/src/__tests__/statusList.store.test.ts index 0eaac0f69..b2f88bf2f 100644 --- a/packages/data-store/src/__tests__/statusList.store.test.ts +++ b/packages/data-store/src/__tests__/statusList.store.test.ts @@ -227,6 +227,6 @@ describe('Status list store tests', () => { const result = await statusListStore.removeStatusList({ id: statusList.id }) expect(result).toEqual(true) - await expect(statusListStore.getStatusList({ id: statusList.id })).rejects.toThrow(`No status list found for id: ${statusList.id}`) + await expect(statusListStore.getStatusList({ id: statusList.id })).rejects.toThrow(`No status list found for id ${statusList.id}`) }) }) diff --git a/packages/data-store/src/statusList/IStatusListStore.ts b/packages/data-store/src/statusList/IStatusListStore.ts index da93ce426..ac5a06952 100644 --- a/packages/data-store/src/statusList/IStatusListStore.ts +++ b/packages/data-store/src/statusList/IStatusListStore.ts @@ -18,7 +18,7 @@ export interface IStatusListStore { getStatusLists(args: IGetStatusListsArgs): Promise> - removeStatusList(args: IRemoveStatusListArgs): Promise + removeStatusList(args: IRemoveStatusListArgs): Promise addStatusList(args: IAddStatusListArgs): Promise diff --git a/packages/data-store/src/statusList/StatusListStore.ts b/packages/data-store/src/statusList/StatusListStore.ts index 54ecfb47a..0e899d40d 100644 --- a/packages/data-store/src/statusList/StatusListStore.ts +++ b/packages/data-store/src/statusList/StatusListStore.ts @@ -74,7 +74,11 @@ export class StatusListStore implements IStatusListStore { { conflictPaths: ['statusList', 'statusListIndex'] }, ) console.log(updateResult) - return (await this.getStatusListEntryByIndex({ ...args, statusListId, errorOnNotFound: true })) as IStatusListEntryEntity + return (await this.getStatusListEntryByIndex({ + ...args, + statusListId, + errorOnNotFound: true, + })) as IStatusListEntryEntity } async getStatusListEntryByIndex(args: IGetStatusListEntryByIndexArgs): Promise { @@ -102,7 +106,10 @@ export class StatusListStore implements IStatusListStore { if (!credentialId) { throw Error('Can only get a credential by credentialId when a credentialId is supplied') } - const statusList = await this.getStatusList({ id: args.statusListId, correlationId: args.statusListCorrelationId }) + const statusList = await this.getStatusList({ + id: args.statusListId, + correlationId: args.statusListCorrelationId, + }) const where = { statusList: statusList.id, ...(args.entryCorrelationId && { correlationId: args.entryCorrelationId }), @@ -164,6 +171,10 @@ export class StatusListStore implements IStatusListStore { } async getStatusList(args: IGetStatusListArgs): Promise { + return statusListFrom(await this.getStatusListEntity(args)) + } + + private async getStatusListEntity(args: IGetStatusListArgs): Promise { if (!args.id && !args.correlationId) { throw Error(`At least and 'id' or 'correlationId' needs to be provided to lookup a status list`) } @@ -177,8 +188,7 @@ export class StatusListStore implements IStatusListStore { if (!result) { throw Error(`No status list found for id ${args.id}`) } - - return statusListFrom(result) + return result } async getStatusLists(args: IGetStatusListsArgs): Promise> { @@ -221,9 +231,13 @@ export class StatusListStore implements IStatusListStore { return statusListFrom(updatedResult) } - async removeStatusList(args: IRemoveStatusListArgs): Promise { - const result = await this.getStatusList(args) - await (await this.getStatusListRepo(result.type)).delete(result) + async removeStatusList(args: IRemoveStatusListArgs): Promise { + const result = await this.getStatusListEntity(args) + + await (await this.getStatusListEntryRepo()).delete({ statusList: result.id }) + const deletedEntity = await (await this.getStatusListRepo()).remove(result) + + return Boolean(deletedEntity) } private async getDS(): Promise { From 4798cc072aedec557f4be2bcf0b2bcecbf1100ae Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 17 Jan 2025 01:45:32 +0100 Subject: [PATCH 13/24] chore: fixed imports & tsconfigs --- packages/oid4vci-issuer-rest-api/package.json | 1 + packages/oid4vci-issuer-rest-api/src/types.ts | 2 +- packages/oid4vci-issuer/package.json | 1 + .../oid4vci-issuer/src/types/IOID4VCIIssuer.ts | 2 +- packages/ssi-types/src/types/status-list.ts | 2 +- packages/tsconfig.json | 1 + packages/vc-status-list-tests/tsconfig.json | 17 +++++++++++++++++ packages/vc-status-list/src/functions.ts | 3 +-- .../vc-status-list/src/impl/StatusList2021.ts | 3 +-- packages/w3c-vc-api/src/api-functions.ts | 2 +- pnpm-lock.yaml | 6 ++++++ 11 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 packages/vc-status-list-tests/tsconfig.json diff --git a/packages/oid4vci-issuer-rest-api/package.json b/packages/oid4vci-issuer-rest-api/package.json index 9dbeff277..a9ca18b2c 100644 --- a/packages/oid4vci-issuer-rest-api/package.json +++ b/packages/oid4vci-issuer-rest-api/package.json @@ -16,6 +16,7 @@ "@sphereon/oid4vci-issuer-server": "0.16.1-next.233", "@sphereon/ssi-express-support": "workspace:*", "@sphereon/ssi-sdk-ext.identifier-resolution": "0.27.0", + "@sphereon/ssi-sdk-ext.jwt-service": "0.27.0", "@sphereon/ssi-sdk.kv-store-temp": "workspace:*", "@sphereon/ssi-sdk.oid4vci-issuer": "workspace:*", "@sphereon/ssi-sdk.oid4vci-issuer-store": "workspace:*", diff --git a/packages/oid4vci-issuer-rest-api/src/types.ts b/packages/oid4vci-issuer-rest-api/src/types.ts index c51ada8a4..4f4d5d40f 100644 --- a/packages/oid4vci-issuer-rest-api/src/types.ts +++ b/packages/oid4vci-issuer-rest-api/src/types.ts @@ -2,7 +2,7 @@ import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resoluti import { IOID4VCIIssuer } from '@sphereon/ssi-sdk.oid4vci-issuer' import { IOID4VCIStore } from '@sphereon/ssi-sdk.oid4vci-issuer-store' import { IAgentContext, ICredentialIssuer, ICredentialVerifier, IDIDManager, IKeyManager, IResolver } from '@veramo/core' -import { IJwtService } from '@sphereon/ssi-sdk-ext.identifier-resolution/src/types/IJwtService' +import { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service' export type IRequiredContext = IAgentContext diff --git a/packages/oid4vci-issuer/package.json b/packages/oid4vci-issuer/package.json index e3b9bb0c1..d10a0650b 100644 --- a/packages/oid4vci-issuer/package.json +++ b/packages/oid4vci-issuer/package.json @@ -18,6 +18,7 @@ "@sphereon/oid4vci-issuer": "0.16.1-next.233", "@sphereon/ssi-sdk-ext.did-utils": "0.27.0", "@sphereon/ssi-sdk-ext.identifier-resolution": "0.27.0", + "@sphereon/ssi-sdk-ext.jwt-service": "0.27.0", "@sphereon/ssi-sdk.agent-config": "workspace:*", "@sphereon/ssi-sdk.core": "workspace:*", "@sphereon/ssi-sdk.kv-store-temp": "workspace:*", diff --git a/packages/oid4vci-issuer/src/types/IOID4VCIIssuer.ts b/packages/oid4vci-issuer/src/types/IOID4VCIIssuer.ts index 84bd75148..e5257241e 100644 --- a/packages/oid4vci-issuer/src/types/IOID4VCIIssuer.ts +++ b/packages/oid4vci-issuer/src/types/IOID4VCIIssuer.ts @@ -17,7 +17,7 @@ import { IOID4VCIStore } from '@sphereon/ssi-sdk.oid4vci-issuer-store' import { ICredential } from '@sphereon/ssi-types/dist' import { IAgentContext, ICredentialIssuer, IDIDManager, IKeyManager, IPluginMethodMap, IResolver } from '@veramo/core' import { IssuerInstance } from '../IssuerInstance' -import { IJwtService } from '@sphereon/ssi-sdk-ext.identifier-resolution/src/types/IJwtService' +import { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service' export type IssuerCredentialDefinition = JsonLdIssuerCredentialDefinition diff --git a/packages/ssi-types/src/types/status-list.ts b/packages/ssi-types/src/types/status-list.ts index b7803da3c..6e7a6da8c 100644 --- a/packages/ssi-types/src/types/status-list.ts +++ b/packages/ssi-types/src/types/status-list.ts @@ -1,6 +1,6 @@ import { W3CVerifiableCredential } from './w3c-vc' import { MdocDocument } from './mso_mdoc' -import { ProofFormat as VmoProofFormat } from '@veramo/core/src/types/ICredentialIssuer' +import { ProofFormat as VmoProofFormat } from '@veramo/core' export enum StatusListType { StatusList2021 = 'StatusList2021', diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 7d7233795..97cb99d9e 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -17,6 +17,7 @@ { "path": "ms-authenticator" }, { "path": "ms-request-api" }, { "path": "vc-status-list" }, + { "path": "vc-status-list-tests" }, { "path": "vc-status-list-issuer-drivers" }, { "path": "vc-status-list-issuer" }, { "path": "vc-status-list-issuer-rest-api" }, diff --git a/packages/vc-status-list-tests/tsconfig.json b/packages/vc-status-list-tests/tsconfig.json new file mode 100644 index 000000000..81b4960b4 --- /dev/null +++ b/packages/vc-status-list-tests/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "rootDir": "src", + "outDir": "dist", + "declarationDir": "dist" + }, + "references": [ + { + "path": "../ssi-types", + "path": "../vc-status-list", + "path": "../vc-handler-ld-local" + } + ], + "extends": "../tsconfig-base.json" +} diff --git a/packages/vc-status-list/src/functions.ts b/packages/vc-status-list/src/functions.ts index b55d5f5d1..742828088 100644 --- a/packages/vc-status-list/src/functions.ts +++ b/packages/vc-status-list/src/functions.ts @@ -7,10 +7,9 @@ import { StatusListVerifiableCredential, StatusPurpose2021, } from '@sphereon/ssi-types' -import { ProofFormat as VmoProofFormat } from '@veramo/core/src/types/ICredentialIssuer' +import { CredentialStatus, DIDDocument, IAgentContext, ICredentialPlugin, ProofFormat as VmoProofFormat } from '@veramo/core' import { checkStatus } from '@sphereon/vc-status-list' -import { CredentialStatus, DIDDocument, IAgentContext, ICredentialPlugin } from '@veramo/core' import { CredentialJwtOrJSON, StatusMethod } from 'credential-status' import { CreateNewStatusListFuncArgs, diff --git a/packages/vc-status-list/src/impl/StatusList2021.ts b/packages/vc-status-list/src/impl/StatusList2021.ts index e8c67df0d..81c3154b8 100644 --- a/packages/vc-status-list/src/impl/StatusList2021.ts +++ b/packages/vc-status-list/src/impl/StatusList2021.ts @@ -1,7 +1,6 @@ -import { IAgentContext, ICredentialPlugin } from '@veramo/core' +import { IAgentContext, ICredentialPlugin, ProofFormat as VmoProofFormat } from '@veramo/core' import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' import { CredentialMapper, DocumentFormat, IIssuer, ProofFormat, StatusListType, StatusListVerifiableCredential } from '@sphereon/ssi-types' -import { ProofFormat as VmoProofFormat } from '@veramo/core/src/types/ICredentialIssuer' import { StatusList } from '@sphereon/vc-status-list' import { IStatusList } from './IStatusList' diff --git a/packages/w3c-vc-api/src/api-functions.ts b/packages/w3c-vc-api/src/api-functions.ts index 04222a38c..1f6f59f0c 100644 --- a/packages/w3c-vc-api/src/api-functions.ts +++ b/packages/w3c-vc-api/src/api-functions.ts @@ -2,7 +2,7 @@ import { checkAuth, ISingleEndpointOpts, sendErrorResponse } from '@sphereon/ssi import { contextHasPlugin } from '@sphereon/ssi-sdk.agent-config' import { CredentialPayload } from '@veramo/core' import { ProofFormat } from '@veramo/core' -import { W3CVerifiableCredential } from '@veramo/core/src/types/vc-data-model' +import { W3CVerifiableCredential } from '@veramo/core' import { Request, Response, Router } from 'express' import { v4 } from 'uuid' import { IIssueCredentialEndpointOpts, IRequiredContext, IVCAPIIssueOpts, IVerifyCredentialEndpointOpts } from './types' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 003cb6b18..467f0873c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1271,6 +1271,9 @@ importers: '@sphereon/ssi-sdk-ext.identifier-resolution': specifier: 0.27.0 version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) + '@sphereon/ssi-sdk-ext.jwt-service': + specifier: 0.27.0 + version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) '@sphereon/ssi-sdk.agent-config': specifier: workspace:* version: link:../agent-config @@ -1353,6 +1356,9 @@ importers: '@sphereon/ssi-sdk-ext.identifier-resolution': specifier: 0.27.0 version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) + '@sphereon/ssi-sdk-ext.jwt-service': + specifier: 0.27.0 + version: 0.27.0(encoding@0.1.13)(pg@8.13.1)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.9)(typescript@5.6.3)) '@sphereon/ssi-sdk.kv-store-temp': specifier: workspace:* version: link:../kv-store From 2ee21ef766a6def5d30a1eb7dc86cacca61b4203 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 17 Jan 2025 09:06:28 +0100 Subject: [PATCH 14/24] chore: fixed issue-verify-flow-statuslist.test --- .../src/__tests__/issue-verify-flow-statuslist.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/vc-handler-ld-local/src/__tests__/issue-verify-flow-statuslist.test.ts b/packages/vc-handler-ld-local/src/__tests__/issue-verify-flow-statuslist.test.ts index 4d781c271..203797504 100644 --- a/packages/vc-handler-ld-local/src/__tests__/issue-verify-flow-statuslist.test.ts +++ b/packages/vc-handler-ld-local/src/__tests__/issue-verify-flow-statuslist.test.ts @@ -71,13 +71,15 @@ describe('credential-LD full flow', () => { it('create a new status list', async () => { const statusList = await createNewStatusList( { - statusPurpose: 'revocation', + type: StatusListType.StatusList2021, proofFormat: 'lds', id: 'http://localhost:9543/list1', issuer: didKeyIdentifier.did, length: 99999, correlationId: '1234', - type: StatusListType.StatusList2021, + statusList2021: { + indexingDirection: 'rightToLeft', + }, }, { agent }, ) @@ -86,7 +88,8 @@ describe('credential-LD full flow', () => { expect(statusList.encodedList).toBeDefined() expect(statusList.issuer).toEqual(didKeyIdentifier.did) expect(statusList.length).toEqual(99999) - expect(statusList.indexingDirection).toEqual('rightToLeft') + expect(statusList.statusList2021).toBeTruthy() + expect(statusList.statusList2021.indexingDirection).toEqual('rightToLeft') expect(statusList.proofFormat).toEqual('lds') expect(statusList.statusListCredential).toBeDefined() }) From 2d7644bdae3e182ad699b3106b38aa53708e6601 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 17 Jan 2025 09:40:23 +0100 Subject: [PATCH 15/24] chore: small refactor --- packages/vc-status-list/src/functions.ts | 28 +--------- .../src/impl/OAuthStatusList.ts | 27 ++++------ packages/vc-status-list/src/utils.ts | 51 ++++++++++++++++++- 3 files changed, 62 insertions(+), 44 deletions(-) diff --git a/packages/vc-status-list/src/functions.ts b/packages/vc-status-list/src/functions.ts index 742828088..93a0f1aaa 100644 --- a/packages/vc-status-list/src/functions.ts +++ b/packages/vc-status-list/src/functions.ts @@ -20,9 +20,8 @@ import { UpdateStatusListFromEncodedListArgs, UpdateStatusListFromStatusListCredentialArgs, } from './types' -import { assertValidProofType, getAssertedValue, getAssertedValues } from './utils' +import { assertValidProofType, determineStatusListType, getAssertedValue, getAssertedValues } from './utils' import { getStatusListImplementation } from './impl/StatusListFactory' -import { jwtDecode } from 'jwt-decode' export async function fetchStatusListCredential(args: { statusListCredential: string }): Promise { const url = getAssertedValue('statusListCredential', args.statusListCredential) @@ -162,31 +161,6 @@ export async function createNewStatusList( return implementation.createNewStatusList(args, context) } -function determineStatusListType(credential: StatusListVerifiableCredential): StatusListType { - if (CredentialMapper.isJwtEncoded(credential)) { - const payload: StatusListVerifiableCredential = jwtDecode(credential as string) - if (!CredentialMapper.isCredential(payload) && 'status_list' in payload) { - return StatusListType.OAuthStatusList - } - } else if (CredentialMapper.isMsoMdocOid4VPEncoded(credential)) { - // Just assume Cbor status list for now, I'd need to decode the Cbor to know what it is - return StatusListType.OAuthStatusList - } - - if (CredentialMapper.isCredential(credential)) { - const uniform = CredentialMapper.toUniformCredential(credential) - const type = uniform.type.find((t) => { - return Object.values(StatusListType).some((statusType) => t.includes(statusType)) - }) - if (!type) { - throw new Error('Invalid status list credential type') - } - return type.replace('Credential', '') as StatusListType - } - - throw new Error('Cannot decode credential payload') -} - export async function updateStatusIndexFromStatusListCredential( args: UpdateStatusListFromStatusListCredentialArgs, context: IAgentContext, diff --git a/packages/vc-status-list/src/impl/OAuthStatusList.ts b/packages/vc-status-list/src/impl/OAuthStatusList.ts index 532bb2858..dbe084222 100644 --- a/packages/vc-status-list/src/impl/OAuthStatusList.ts +++ b/packages/vc-status-list/src/impl/OAuthStatusList.ts @@ -1,5 +1,5 @@ import { IAgentContext, ICredentialPlugin } from '@veramo/core' -import { CredentialMapper, ProofFormat, StatusListType, StatusListVerifiableCredential } from '@sphereon/ssi-types' +import { ProofFormat, StatusListType, StatusListVerifiableCredential } from '@sphereon/ssi-types' import { CheckStatusIndexArgs, CreateStatusListArgs, @@ -8,7 +8,7 @@ import { UpdateStatusListFromEncodedListArgs, UpdateStatusListIndexArgs, } from '../types' -import { getAssertedValue, getAssertedValues } from '../utils' +import { determineProofFormat, getAssertedValue, getAssertedValues } from '../utils' import { IStatusList } from './IStatusList' import { StatusList, StatusListJWTHeaderParameters } from '@sd-jwt/jwt-status-list' import { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service' @@ -73,15 +73,12 @@ export class OAuthStatusListImplementation implements IStatusList { async updateStatusListIndex(args: UpdateStatusListIndexArgs, context: IRequiredContext): Promise { const { statusListCredential, value } = args - const isJwtEncoded = CredentialMapper.isJwtEncoded(statusListCredential) - const isCborEncoded = CredentialMapper.isMsoMdocOid4VPEncoded(statusListCredential) - const proofFormat = isJwtEncoded ? 'jwt' : isCborEncoded ? 'cbor' : DEFAULT_PROOF_FORMAT - - if (!isJwtEncoded && !isCborEncoded) { - throw new Error('statusListCredential is neither a JWT nor a CBOR document') + if (typeof statusListCredential !== 'string') { + return Promise.reject('statusListCredential in neither JWT nor CWT') } - const decoded = isJwtEncoded ? decodeStatusListJWT(statusListCredential) : decodeStatusListCWT(statusListCredential) + 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) @@ -151,16 +148,14 @@ export class OAuthStatusListImplementation implements IStatusList { async checkStatusIndex(args: CheckStatusIndexArgs): Promise { const { statusListCredential, statusListIndex } = args - const isJwtEncoded = CredentialMapper.isJwtEncoded(statusListCredential) - const isCborEncoded = CredentialMapper.isMsoMdocOid4VPEncoded(statusListCredential) - - if (!isJwtEncoded && !isCborEncoded) { - throw new Error('statusListCredential is neither a JWT nor a CBOR document') + if (typeof statusListCredential !== 'string') { + return Promise.reject('statusListCredential in neither JWT nor CWT') } - const { statusList } = isJwtEncoded ? decodeStatusListJWT(statusListCredential) : decodeStatusListCWT(statusListCredential) - const index = typeof statusListIndex === 'number' ? statusListIndex : parseInt(statusListIndex) + 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') } diff --git a/packages/vc-status-list/src/utils.ts b/packages/vc-status-list/src/utils.ts index f5653bc99..c1945cf21 100644 --- a/packages/vc-status-list/src/utils.ts +++ b/packages/vc-status-list/src/utils.ts @@ -1,4 +1,12 @@ -import { IIssuer, ProofFormat, StatusListType, StatusListType as StatusListTypeW3C } from '@sphereon/ssi-types' +import { + CredentialMapper, + IIssuer, + ProofFormat, + StatusListType, + StatusListType as StatusListTypeW3C, + StatusListVerifiableCredential, +} from '@sphereon/ssi-types' +import { jwtDecode } from 'jwt-decode' export function getAssertedStatusListType(type?: StatusListType) { const assertedType = type ?? StatusListType.StatusList2021 @@ -40,3 +48,44 @@ export function assertValidProofType(type: StatusListType, proofFormat: ProofFor throw Error(`Invalid proof format '${proofFormat}' for status list type ${type}`) } } + +export function determineStatusListType(credential: StatusListVerifiableCredential): StatusListType { + const proofFormat = determineProofFormat(credential) + switch (proofFormat) { + case 'jwt': + const payload: StatusListVerifiableCredential = jwtDecode(credential as string) + const keys = Object.keys(payload) + if (keys.includes('status_list')) { + return StatusListType.OAuthStatusList + } else if (keys.includes('vc')) { + return StatusListType.StatusList2021 + } + break + case 'lds': + const uniform = CredentialMapper.toUniformCredential(credential) + const type = uniform.type.find((t) => { + return Object.values(StatusListType).some((statusType) => t.includes(statusType)) + }) + if (!type) { + throw new Error('Invalid status list credential type') + } + return type.replace('Credential', '') as StatusListType + + case 'cbor': + return StatusListType.OAuthStatusList + } + + throw new Error('Cannot determine status list type from credential payload') +} + +export function determineProofFormat(credential: StatusListVerifiableCredential): ProofFormat { + if (CredentialMapper.isJwtEncoded(credential)) { + return 'jwt' + } else if (CredentialMapper.isMsoMdocOid4VPEncoded(credential)) { + // Just assume Cbor for now, I'd need to decode at least the header to what type of Cbor we have + return 'cbor' + } else if (CredentialMapper.isCredential(credential)) { + return 'lds' + } + throw Error('Cannot determine credential payload type') +} From 79cab93faa6a074add7688d04e58888a43e11829 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 17 Jan 2025 09:56:26 +0100 Subject: [PATCH 16/24] chore: fixed more tests --- .../src/__tests__/issue-verify-flow-statuslist.test.ts | 2 +- .../__tests__/status-list-vc-handling.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vc-handler-ld-local/src/__tests__/issue-verify-flow-statuslist.test.ts b/packages/vc-handler-ld-local/src/__tests__/issue-verify-flow-statuslist.test.ts index 203797504..9102bef57 100644 --- a/packages/vc-handler-ld-local/src/__tests__/issue-verify-flow-statuslist.test.ts +++ b/packages/vc-handler-ld-local/src/__tests__/issue-verify-flow-statuslist.test.ts @@ -89,7 +89,7 @@ describe('credential-LD full flow', () => { expect(statusList.issuer).toEqual(didKeyIdentifier.did) expect(statusList.length).toEqual(99999) expect(statusList.statusList2021).toBeTruthy() - expect(statusList.statusList2021.indexingDirection).toEqual('rightToLeft') + expect(statusList.statusList2021!.indexingDirection).toEqual('rightToLeft') expect(statusList.proofFormat).toEqual('lds') expect(statusList.statusListCredential).toBeDefined() }) diff --git a/packages/vc-status-list-issuer/__tests__/status-list-vc-handling.test.ts b/packages/vc-status-list-issuer/__tests__/status-list-vc-handling.test.ts index 00d36c8c8..6dcce4972 100644 --- a/packages/vc-status-list-issuer/__tests__/status-list-vc-handling.test.ts +++ b/packages/vc-status-list-issuer/__tests__/status-list-vc-handling.test.ts @@ -152,6 +152,7 @@ describe('JWT Verifiable Credential, should be', () => { type: StatusListType.OAuthStatusList, issuer: 'did:example:123', id: 'list123', + correlationId: 'test-1-' + Date.now(), proofFormat: 'lds', oauthStatusList: { bitsPerStatus: 2, From 59610f1c7cb1d8e021a59a024c97ec0363479de1 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 17 Jan 2025 11:16:08 +0100 Subject: [PATCH 17/24] chore: self-review --- .../entities/statusList/StatusListEntities.ts | 8 +- .../src/types/statusList/statusList.ts | 4 +- packages/ssi-types/src/types/status-list.ts | 4 +- .../issue-verify-flow-statuslist.test.ts | 2 + .../src/drivers.ts | 6 +- .../src/types.ts | 6 +- .../src/agent/StatusListPlugin.ts | 16 ++-- .../vc-status-list-issuer/src/functions.ts | 8 +- packages/vc-status-list/src/functions.ts | 30 +++----- .../src/impl/OAuthStatusList.ts | 76 +++++++++---------- .../vc-status-list/src/impl/StatusList2021.ts | 7 +- .../src/impl/StatusListFactory.ts | 5 -- .../vc-status-list/src/impl/encoding/cbor.ts | 66 +++++++++------- packages/vc-status-list/src/types/index.ts | 12 +-- packages/vc-status-list/src/utils.ts | 8 +- 15 files changed, 127 insertions(+), 131 deletions(-) diff --git a/packages/data-store/src/entities/statusList/StatusListEntities.ts b/packages/data-store/src/entities/statusList/StatusListEntities.ts index 20bf9ad85..6b74e7fbd 100644 --- a/packages/data-store/src/entities/statusList/StatusListEntities.ts +++ b/packages/data-store/src/entities/statusList/StatusListEntities.ts @@ -1,6 +1,6 @@ import { IIssuer, - StatusListVerifiableCredential, + StatusListCredential, StatusListCredentialIdMode, StatusListDriverType, StatusListIndexingDirection, @@ -71,13 +71,13 @@ export abstract class StatusListEntity extends BaseEntity { nullable: true, unique: false, transformer: { - from(value: string): StatusListVerifiableCredential { + from(value: string): StatusListCredential { if (value?.startsWith('ey')) { return value } return JSON.parse(value) }, - to(value: StatusListVerifiableCredential): string { + to(value: StatusListCredential): string { if (typeof value === 'string') { return value } @@ -85,7 +85,7 @@ export abstract class StatusListEntity extends BaseEntity { }, }, }) - statusListCredential?: StatusListVerifiableCredential + statusListCredential?: StatusListCredential @OneToMany((type) => StatusListEntryEntity, (entry) => entry.statusList) statusListEntries!: StatusListEntryEntity[] diff --git a/packages/data-store/src/types/statusList/statusList.ts b/packages/data-store/src/types/statusList/statusList.ts index c27c4ac9b..d681fb16d 100644 --- a/packages/data-store/src/types/statusList/statusList.ts +++ b/packages/data-store/src/types/statusList/statusList.ts @@ -1,6 +1,6 @@ import { IIssuer, - StatusListVerifiableCredential, + StatusListCredential, StatusListCredentialIdMode, StatusListDriverType, StatusListIndexingDirection, @@ -19,7 +19,7 @@ export interface IStatusListEntity { issuer: string | IIssuer type: StatusListType proofFormat: ProofFormat - statusListCredential?: StatusListVerifiableCredential + statusListCredential?: StatusListCredential } export interface IStatusList2021Entity extends IStatusListEntity { diff --git a/packages/ssi-types/src/types/status-list.ts b/packages/ssi-types/src/types/status-list.ts index 6e7a6da8c..d8af92b85 100644 --- a/packages/ssi-types/src/types/status-list.ts +++ b/packages/ssi-types/src/types/status-list.ts @@ -1,12 +1,12 @@ import { W3CVerifiableCredential } from './w3c-vc' -import { MdocDocument } from './mso_mdoc' import { ProofFormat as VmoProofFormat } from '@veramo/core' export enum StatusListType { StatusList2021 = 'StatusList2021', OAuthStatusList = 'OAuthStatusList', } +export type CWT = string -export type StatusListVerifiableCredential = W3CVerifiableCredential | MdocDocument +export type StatusListCredential = W3CVerifiableCredential | CWT export type ProofFormat = VmoProofFormat | 'cbor' diff --git a/packages/vc-handler-ld-local/src/__tests__/issue-verify-flow-statuslist.test.ts b/packages/vc-handler-ld-local/src/__tests__/issue-verify-flow-statuslist.test.ts index 9102bef57..a1691571e 100644 --- a/packages/vc-handler-ld-local/src/__tests__/issue-verify-flow-statuslist.test.ts +++ b/packages/vc-handler-ld-local/src/__tests__/issue-verify-flow-statuslist.test.ts @@ -78,6 +78,7 @@ describe('credential-LD full flow', () => { length: 99999, correlationId: '1234', statusList2021: { + statusPurpose: 'revocation', indexingDirection: 'rightToLeft', }, }, @@ -90,6 +91,7 @@ describe('credential-LD full flow', () => { expect(statusList.length).toEqual(99999) expect(statusList.statusList2021).toBeTruthy() expect(statusList.statusList2021!.indexingDirection).toEqual('rightToLeft') + expect(statusList.statusList2021!.statusPurpose).toEqual('revocation') expect(statusList.proofFormat).toEqual('lds') expect(statusList.statusListCredential).toBeDefined() }) diff --git a/packages/vc-status-list-issuer-drivers/src/drivers.ts b/packages/vc-status-list-issuer-drivers/src/drivers.ts index 85edb4b84..78850fe1e 100644 --- a/packages/vc-status-list-issuer-drivers/src/drivers.ts +++ b/packages/vc-status-list-issuer-drivers/src/drivers.ts @@ -14,7 +14,7 @@ import { StatusListOAuthEntryCredentialStatus, StatusListResult, } from '@sphereon/ssi-sdk.vc-status-list' -import { StatusListCredentialIdMode, StatusListDriverType, StatusListType, StatusListVerifiableCredential } from '@sphereon/ssi-types' +import { StatusListCredentialIdMode, StatusListDriverType, StatusListType, StatusListCredential } from '@sphereon/ssi-types' import { DataSource } from 'typeorm' import { IStatusListDriver } from './types' import { statusListResultToEntity } from './status-list-adapters' @@ -129,7 +129,7 @@ export class AgentDataSourceStatusListDriver implements IStatusListDriver { } async createStatusList(args: { - statusListCredential: StatusListVerifiableCredential + statusListCredential: StatusListCredential correlationId?: string credentialIdMode?: StatusListCredentialIdMode }): Promise { @@ -152,7 +152,7 @@ export class AgentDataSourceStatusListDriver implements IStatusListDriver { } async updateStatusList(args: { - statusListCredential: StatusListVerifiableCredential + statusListCredential: StatusListCredential correlationId: string type: StatusListType }): Promise { diff --git a/packages/vc-status-list-issuer-drivers/src/types.ts b/packages/vc-status-list-issuer-drivers/src/types.ts index 3cc610c7b..40cfe1bb5 100644 --- a/packages/vc-status-list-issuer-drivers/src/types.ts +++ b/packages/vc-status-list-issuer-drivers/src/types.ts @@ -12,7 +12,7 @@ import { StatusListOAuthEntryCredentialStatus, StatusListResult, } from '@sphereon/ssi-sdk.vc-status-list' -import { StatusListVerifiableCredential, StatusListDriverType } from '@sphereon/ssi-types' +import { StatusListCredential, StatusListDriverType } from '@sphereon/ssi-types' import { IAgentContext, ICredentialIssuer, @@ -45,7 +45,7 @@ export interface IStatusListDriver { getStatusListLength(args?: { correlationId?: string }): Promise - createStatusList(args: { statusListCredential: StatusListVerifiableCredential; correlationId?: string }): Promise + createStatusList(args: { statusListCredential: StatusListCredential; correlationId?: string }): Promise getStatusList(args?: { correlationId?: string }): Promise @@ -58,7 +58,7 @@ export interface IStatusListDriver { getStatusListEntryByIndex(args: IGetStatusListEntryByIndexArgs): Promise - updateStatusList(args: { statusListCredential: StatusListVerifiableCredential }): Promise + updateStatusList(args: { statusListCredential: StatusListCredential }): Promise deleteStatusList(): Promise diff --git a/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts b/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts index 7d9a72591..4abd501ea 100644 --- a/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts +++ b/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts @@ -86,29 +86,29 @@ export class StatusListPlugin implements IAgentPlugin { correlationId: sl.correlationId, dataSource, }) - let statusListResponse: StatusListResult | undefined = undefined + let statusList: StatusListResult | undefined = undefined try { - statusListResponse = await this.slGetStatusList(args, context) + statusList = await this.slGetStatusList(args, context) } catch (e) { // That is fine if there is no status list yet } - if (statusListResponse && this.instances.find((sl) => sl.id === args.id || sl.correlationId === args.correlationId)) { + if (statusList && this.instances.find((sl) => sl.id === args.id || sl.correlationId === args.correlationId)) { return Promise.reject(Error(`Status list with id ${args.id} or correlation id ${args.correlationId} already exists`)) } else { - statusListResponse = await driver.createStatusList({ + statusList = await driver.createStatusList({ statusListCredential: sl.statusListCredential, correlationId: sl.correlationId, }) this.instances.push({ - correlationId: statusListResponse.correlationId, - id: statusListResponse.id, + correlationId: statusList.correlationId, + id: statusList.id, dataSource, - driverType: statusListResponse.driverType!, + driverType: statusList.driverType!, driverOptions: driver.getOptions(), }) } - return statusListResponse + return statusList } private async slAddStatusToCredential(args: IAddStatusToCredentialArgs, context: IRequiredContext): Promise { diff --git a/packages/vc-status-list-issuer/src/functions.ts b/packages/vc-status-list-issuer/src/functions.ts index 2e90fa253..9ecd5291b 100644 --- a/packages/vc-status-list-issuer/src/functions.ts +++ b/packages/vc-status-list-issuer/src/functions.ts @@ -25,18 +25,18 @@ export const createStatusListFromInstance = async ( statusPurpose: args.instance.statusPurpose ?? 'revocation', correlationId: args.instance.correlationId ?? args.instance.id, } - let sl: StatusListResult + let statusList: StatusListResult try { - sl = await context.agent.slGetStatusList(instance) + statusList = await context.agent.slGetStatusList(instance) } catch (e) { const id = instance.id const correlationId = instance.correlationId if (!id || !correlationId) { return Promise.reject(Error(`No correlation id and id provided for status list`)) } - sl = await context.agent.slCreateStatusList({ ...instance, id, correlationId }) + statusList = await context.agent.slCreateStatusList({ ...instance, id, correlationId }) } - return sl + return statusList } export const handleCredentialStatus = async ( diff --git a/packages/vc-status-list/src/functions.ts b/packages/vc-status-list/src/functions.ts index 93a0f1aaa..67e1259fe 100644 --- a/packages/vc-status-list/src/functions.ts +++ b/packages/vc-status-list/src/functions.ts @@ -1,12 +1,5 @@ import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { - CredentialMapper, - ProofFormat, - StatusListDriverType, - StatusListType, - StatusListVerifiableCredential, - StatusPurpose2021, -} from '@sphereon/ssi-types' +import { CredentialMapper, ProofFormat, StatusListDriverType, StatusListType, StatusListCredential, StatusPurpose2021 } from '@sphereon/ssi-types' import { CredentialStatus, DIDDocument, IAgentContext, ICredentialPlugin, ProofFormat as VmoProofFormat } from '@veramo/core' import { checkStatus } from '@sphereon/vc-status-list' @@ -23,7 +16,7 @@ import { import { assertValidProofType, determineStatusListType, getAssertedValue, getAssertedValues } from './utils' import { getStatusListImplementation } from './impl/StatusListFactory' -export async function fetchStatusListCredential(args: { statusListCredential: string }): Promise { +export async function fetchStatusListCredential(args: { statusListCredential: string }): Promise { const url = getAssertedValue('statusListCredential', args.statusListCredential) try { const response = await fetch(url) @@ -32,9 +25,9 @@ export async function fetchStatusListCredential(args: { statusListCredential: st } const responseAsText = await response.text() if (responseAsText.trim().startsWith('{')) { - return JSON.parse(responseAsText) as StatusListVerifiableCredential + return JSON.parse(responseAsText) as StatusListCredential } - return responseAsText as StatusListVerifiableCredential + return responseAsText as StatusListCredential } catch (error) { console.error(`Fetching status list ${url} resulted in an unexpected error: ${error instanceof Error ? error.message : JSON.stringify(error)}`) throw error @@ -53,7 +46,7 @@ export function statusPluginStatusFunction(args: { const result = await checkStatusForCredential({ ...args, documentLoader: args.documentLoader, - credential: credential as StatusListVerifiableCredential, + credential: credential as StatusListCredential, errorUnknownListType: args.errorUnknownListType, }) @@ -76,7 +69,7 @@ export function vcLibCheckStatusFunction(args: { }) { const { mandatoryCredentialStatus, verifyStatusListCredential, verifyMatchingIssuers, errorUnknownListType } = args return (args: { - credential: StatusListVerifiableCredential + credential: StatusListCredential documentLoader: any suite: any }): Promise<{ @@ -94,7 +87,7 @@ export function vcLibCheckStatusFunction(args: { } export async function checkStatusForCredential(args: { - credential: StatusListVerifiableCredential + credential: StatusListCredential documentLoader: any suite: any mandatoryCredentialStatus?: boolean @@ -141,7 +134,7 @@ export async function simpleCheckStatusFromStatusListUrl(args: { } export async function checkStatusIndexFromStatusListCredential(args: { - statusListCredential: StatusListVerifiableCredential + statusListCredential: StatusListCredential statusPurpose?: StatusPurpose2021 type?: StatusListType | 'StatusList2021Entry' id?: string @@ -173,7 +166,7 @@ export async function updateStatusIndexFromStatusListCredential( // Keeping helper function for backward compatibility export async function statusListCredentialToDetails(args: { - statusListCredential: StatusListVerifiableCredential + statusListCredential: StatusListCredential correlationId?: string driverType?: StatusListDriverType }): Promise { @@ -208,7 +201,7 @@ export async function updateStatusListIndexFromEncodedList( export async function statusList2021ToVerifiableCredential( args: StatusList2021ToVerifiableCredentialArgs, context: IAgentContext, -): Promise { +): Promise { const { issuer, id, type } = getAssertedValues(args) const identifier = await context.agent.identifierManagedGet({ identifier: typeof issuer === 'string' ? issuer : issuer.id, @@ -242,6 +235,5 @@ export async function statusList2021ToVerifiableCredential( fetchRemoteContexts: true, }) - return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as StatusListVerifiableCredential) - .original as StatusListVerifiableCredential + return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as StatusListCredential).original as StatusListCredential } diff --git a/packages/vc-status-list/src/impl/OAuthStatusList.ts b/packages/vc-status-list/src/impl/OAuthStatusList.ts index dbe084222..ef0d40e95 100644 --- a/packages/vc-status-list/src/impl/OAuthStatusList.ts +++ b/packages/vc-status-list/src/impl/OAuthStatusList.ts @@ -1,8 +1,9 @@ import { IAgentContext, ICredentialPlugin } from '@veramo/core' -import { ProofFormat, StatusListType, StatusListVerifiableCredential } from '@sphereon/ssi-types' +import { ProofFormat, StatusListType } from '@sphereon/ssi-types' import { CheckStatusIndexArgs, CreateStatusListArgs, + SignedStatusListData, StatusListResult, StatusOAuth, UpdateStatusListFromEncodedListArgs, @@ -33,7 +34,7 @@ export class OAuthStatusListImplementation implements IStatusList { } const proofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT - const { issuer, id } = args + const { issuer, id, keyRef } = args const length = args.length ?? DEFAULT_LIST_LENGTH const bitsPerStatus = args.oauthStatusList.bitsPerStatus ?? BITS_PER_STATUS_DEFAULT const issuerString = typeof issuer === 'string' ? issuer : issuer.id @@ -41,22 +42,7 @@ export class OAuthStatusListImplementation implements IStatusList { const statusList = new StatusList(new Array(length).fill(0), bitsPerStatus) const encodedList = statusList.compressStatusList() - let statusListCredential: StatusListVerifiableCredential - - switch (proofFormat) { - case 'jwt': { - const { statusListCredential: slJwt } = await createSignedJwt(context, statusList, issuerString, id, args.keyRef) - statusListCredential = slJwt - break - } - case 'cbor': { - const { statusListCredential: slCbor } = await createSignedCbor(context, statusList, issuerString, id, args.keyRef) - statusListCredential = slCbor - break - } - default: - throw new Error(`Invalid proof format '${proofFormat}' for OAuthStatusList`) - } + const { statusListCredential } = await this.createSignedStatusList(proofFormat, context, statusList, issuerString, id, keyRef) return { encodedList, @@ -72,7 +58,7 @@ export class OAuthStatusListImplementation implements IStatusList { } async updateStatusListIndex(args: UpdateStatusListIndexArgs, context: IRequiredContext): Promise { - const { statusListCredential, value } = args + const { statusListCredential, value, keyRef } = args if (typeof statusListCredential !== 'string') { return Promise.reject('statusListCredential in neither JWT nor CWT') } @@ -87,13 +73,18 @@ export class OAuthStatusListImplementation implements IStatusList { } statusList.setStatus(index, value) - const result = - proofFormat === 'jwt' - ? await createSignedJwt(context, statusList, issuer, id, args.keyRef) - : await createSignedCbor(context, statusList, issuer, id, args.keyRef) + const { statusListCredential: signedCredential, encodedList } = await this.createSignedStatusList( + proofFormat, + context, + statusList, + issuer, + id, + keyRef, + ) return { - ...result, + statusListCredential: signedCredential, + encodedList, oauthStatusList: { bitsPerStatus: statusList.getBitsPerStatus(), }, @@ -119,22 +110,11 @@ export class OAuthStatusListImplementation implements IStatusList { const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) listToUpdate.setStatus(index, args.value ? 1 : 0) - let result: { statusListCredential: StatusListVerifiableCredential; encodedList: string } - - switch (proofFormat) { - case 'jwt': - result = await createSignedJwt(context, listToUpdate, issuerString, id, args.keyRef) - break - case 'cbor': - result = await createSignedCbor(context, listToUpdate, issuerString, id, args.keyRef) - break - default: - throw new Error(`Invalid proof format '${proofFormat}' for OAuthStatusList`) - } + const { statusListCredential, encodedList } = await this.createSignedStatusList(proofFormat, context, listToUpdate, issuerString, id, args.keyRef) return { - encodedList: result.encodedList, - statusListCredential: result.statusListCredential, + encodedList, + statusListCredential, oauthStatusList: { bitsPerStatus, }, @@ -162,4 +142,24 @@ export class OAuthStatusListImplementation implements IStatusList { return statusList.getStatus(index) } + + private async createSignedStatusList( + proofFormat: 'jwt' | 'lds' | 'EthereumEip712Signature2021' | 'cbor', + context: IAgentContext, + statusList: StatusList, + issuerString: string, + id: string, + keyRef?: string, + ): Promise { + switch (proofFormat) { + case 'jwt': { + return await createSignedJwt(context, statusList, issuerString, id, keyRef) + } + case 'cbor': { + return await createSignedCbor(context, statusList, issuerString, id, keyRef) + } + default: + throw new Error(`Invalid proof format '${proofFormat}' for OAuthStatusList`) + } + } } diff --git a/packages/vc-status-list/src/impl/StatusList2021.ts b/packages/vc-status-list/src/impl/StatusList2021.ts index 81c3154b8..d643c0cbc 100644 --- a/packages/vc-status-list/src/impl/StatusList2021.ts +++ b/packages/vc-status-list/src/impl/StatusList2021.ts @@ -1,6 +1,6 @@ import { IAgentContext, ICredentialPlugin, ProofFormat as VmoProofFormat } from '@veramo/core' import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { CredentialMapper, DocumentFormat, IIssuer, ProofFormat, StatusListType, StatusListVerifiableCredential } from '@sphereon/ssi-types' +import { CredentialMapper, DocumentFormat, IIssuer, ProofFormat, StatusListType, StatusListCredential } from '@sphereon/ssi-types' import { StatusList } from '@sphereon/vc-status-list' import { IStatusList } from './IStatusList' @@ -162,7 +162,7 @@ export class StatusList2021Implementation implements IStatusList { keyRef?: string }, context: IAgentContext, - ): Promise { + ): Promise { const identifier = await context.agent.identifierManagedGet({ identifier: typeof args.issuer === 'string' ? args.issuer : args.issuer.id, vmRelationship: 'assertionMethod', @@ -189,7 +189,6 @@ export class StatusList2021Implementation implements IStatusList { fetchRemoteContexts: true, }) - return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as StatusListVerifiableCredential) - .original as StatusListVerifiableCredential + return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as StatusListCredential).original as StatusListCredential } } diff --git a/packages/vc-status-list/src/impl/StatusListFactory.ts b/packages/vc-status-list/src/impl/StatusListFactory.ts index b1379f3b9..4ebea3ea8 100644 --- a/packages/vc-status-list/src/impl/StatusListFactory.ts +++ b/packages/vc-status-list/src/impl/StatusListFactory.ts @@ -27,11 +27,6 @@ export class StatusListFactory { } return implementation } - - // Optional: Method to register custom implementations if needed - public registerImplementation(type: StatusListType, implementation: IStatusList): void { - this.implementations.set(type, implementation) - } } export function getStatusListImplementation(type: StatusListType): IStatusList { diff --git a/packages/vc-status-list/src/impl/encoding/cbor.ts b/packages/vc-status-list/src/impl/encoding/cbor.ts index e8198512a..d7fddd51f 100644 --- a/packages/vc-status-list/src/impl/encoding/cbor.ts +++ b/packages/vc-status-list/src/impl/encoding/cbor.ts @@ -41,41 +41,13 @@ export const createSignedCbor = async ( ), ) - const exp = Math.floor(new Date().getTime() / 1000) - const ttl = 65535 // FIXME figure out what value should be / come from and what the difference is with exp - const claimsEntries: Array<[com.sphereon.cbor.CborUInt, com.sphereon.cbor.CborItem]> = [ - [new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.SUBJECT)), new cbor.CborString(id)], // "sub" - [new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.ISSUER)), new cbor.CborString(issuerString)], // "iss" - [ - new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.ISSUED_AT)), - new cbor.CborUInt(kmp.LongKMP.fromNumber(Math.floor(Date.now() / 1000))), // "iat" - ], - ] - - if (exp) { - claimsEntries.push([ - new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.EXPIRATION)), - new cbor.CborUInt(kmp.LongKMP.fromNumber(exp)), // "exp" - ]) - } - - if (ttl) { - claimsEntries.push([ - new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.TIME_TO_LIVE)), - new cbor.CborUInt(kmp.LongKMP.fromNumber(ttl)), // "time to live" - ]) - } - - claimsEntries.push([new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.STATUS_LIST)), statusListMap]) - - const claimsMap = new cbor.CborMap(kotlin.collections.KtMutableMap.fromJsMap(new Map(claimsEntries))) - const protectedHeader = new cbor.CborMap( kotlin.collections.KtMutableMap.fromJsMap( new Map([[new cbor.CborUInt(kmp.LongKMP.fromNumber(16)), new cbor.CborString('statuslist+cwt')]]), // "type" ), ) const protectedHeaderEncoded = cbor.Cbor.encode(protectedHeader) + const claimsMap = buildClaimsMap(id, issuerString, statusListMap) const claimsEncoded = cbor.Cbor.encode(claimsMap) const signedCWT = await context.agent.keyManagerSign({ @@ -104,6 +76,42 @@ export const createSignedCbor = async ( } } +function buildClaimsMap( + id: string, + issuerString: string, + statusListMap: com.sphereon.cbor.CborMap>, +) { + const exp = Math.floor(new Date().getTime() / 1000) + const ttl = 65535 // FIXME figure out what value should be / come from and what the difference is with exp + const claimsEntries: Array<[com.sphereon.cbor.CborUInt, com.sphereon.cbor.CborItem]> = [ + [new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.SUBJECT)), new cbor.CborString(id)], // "sub" + [new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.ISSUER)), new cbor.CborString(issuerString)], // "iss" + [ + new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.ISSUED_AT)), + new cbor.CborUInt(kmp.LongKMP.fromNumber(Math.floor(Date.now() / 1000))), // "iat" + ], + ] + + if (exp) { + claimsEntries.push([ + new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.EXPIRATION)), + new cbor.CborUInt(kmp.LongKMP.fromNumber(exp)), // "exp" + ]) + } + + if (ttl) { + claimsEntries.push([ + new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.TIME_TO_LIVE)), + new cbor.CborUInt(kmp.LongKMP.fromNumber(ttl)), // "time to live" + ]) + } + + claimsEntries.push([new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.STATUS_LIST)), statusListMap]) + + const claimsMap = new cbor.CborMap(kotlin.collections.KtMutableMap.fromJsMap(new Map(claimsEntries))) + return claimsMap +} + const getCborValueFromMap = (map: Map, com.sphereon.cbor.CborItem>, key: number): T | never => { const value = map.get(new com.sphereon.cbor.CborUInt(kmp.LongKMP.fromNumber(key))) if (!value) { diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index d735f3dc7..84b833576 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -10,7 +10,7 @@ import { StatusListDriverType, StatusListIndexingDirection, StatusListType, - StatusListVerifiableCredential, + StatusListCredential, StatusPurpose2021, } from '@sphereon/ssi-types' import { CredentialPayload, IAgentContext, ICredentialIssuer, ICredentialPlugin, ICredentialVerifier, IPluginMethodMap } from '@veramo/core' @@ -75,7 +75,7 @@ export interface UpdateStatusListFromEncodedListArgs { } export interface UpdateStatusListFromStatusListCredentialArgs { - statusListCredential: StatusListVerifiableCredential // | CompactJWT + statusListCredential: StatusListCredential // | CompactJWT keyRef?: string statusListIndex: number | string value: number | Status2021 | StatusOAuth @@ -83,7 +83,7 @@ export interface UpdateStatusListFromStatusListCredentialArgs { export interface StatusListResult { encodedList: string - statusListCredential: StatusListVerifiableCredential // | CompactJWT + statusListCredential: StatusListCredential // | CompactJWT length: number type: StatusListType proofFormat: ProofFormat @@ -144,14 +144,14 @@ export interface CreateStatusListArgs { } export interface UpdateStatusListIndexArgs { - statusListCredential: StatusListVerifiableCredential // | CompactJWT + statusListCredential: StatusListCredential // | CompactJWT keyRef?: string statusListIndex: number | string value: number | Status2021 | StatusOAuth } export interface CheckStatusIndexArgs { - statusListCredential: StatusListVerifiableCredential // | CompactJWT + statusListCredential: StatusListCredential // | CompactJWT statusListIndex: string | number } @@ -224,7 +224,7 @@ export type GetStatusListArgs = { export type CredentialWithStatusSupport = ICredential | CredentialPayload | IVerifiableCredential export type SignedStatusListData = { - statusListCredential: StatusListVerifiableCredential + statusListCredential: StatusListCredential encodedList: string } diff --git a/packages/vc-status-list/src/utils.ts b/packages/vc-status-list/src/utils.ts index c1945cf21..58e098837 100644 --- a/packages/vc-status-list/src/utils.ts +++ b/packages/vc-status-list/src/utils.ts @@ -4,7 +4,7 @@ import { ProofFormat, StatusListType, StatusListType as StatusListTypeW3C, - StatusListVerifiableCredential, + StatusListCredential, } from '@sphereon/ssi-types' import { jwtDecode } from 'jwt-decode' @@ -49,11 +49,11 @@ export function assertValidProofType(type: StatusListType, proofFormat: ProofFor } } -export function determineStatusListType(credential: StatusListVerifiableCredential): StatusListType { +export function determineStatusListType(credential: StatusListCredential): StatusListType { const proofFormat = determineProofFormat(credential) switch (proofFormat) { case 'jwt': - const payload: StatusListVerifiableCredential = jwtDecode(credential as string) + const payload: StatusListCredential = jwtDecode(credential as string) const keys = Object.keys(payload) if (keys.includes('status_list')) { return StatusListType.OAuthStatusList @@ -78,7 +78,7 @@ export function determineStatusListType(credential: StatusListVerifiableCredenti throw new Error('Cannot determine status list type from credential payload') } -export function determineProofFormat(credential: StatusListVerifiableCredential): ProofFormat { +export function determineProofFormat(credential: StatusListCredential): ProofFormat { if (CredentialMapper.isJwtEncoded(credential)) { return 'jwt' } else if (CredentialMapper.isMsoMdocOid4VPEncoded(credential)) { From 1f3364c343eef09f9181052949e81c01fc759ef8 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 17 Jan 2025 11:16:37 +0100 Subject: [PATCH 18/24] chore: lockfile --- pnpm-lock.yaml | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 467f0873c..86523a405 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4225,7 +4225,6 @@ packages: '@azure/msal-node@1.18.4': resolution: {integrity: sha512-Kc/dRvhZ9Q4+1FSfsTFDME/v6+R2Y1fuMty/TfwqE5p9GTPw08BPbKgeWinE8JRHRp+LemjQbUZsn4Q4l6Lszg==} engines: {node: 10 || 12 || 14 || 16 || 18} - deprecated: A newer major version of this library is available. Please upgrade to the latest available version. '@babel/cli@7.25.9': resolution: {integrity: sha512-I+02IfrTiSanpxJBlZQYb18qCxB6c2Ih371cVpfgIrPQrjAYkf45XxomTJOG8JBWX5GY35/+TmhCMdJ4ZPkL8Q==} @@ -4405,7 +4404,6 @@ packages: '@babel/plugin-proposal-export-namespace-from@7.18.9': resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead. peerDependencies: '@babel/core': ^7.0.0-0 @@ -5295,7 +5293,6 @@ packages: '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} @@ -5303,7 +5300,6 @@ packages: '@humanwhocodes/object-schema@2.0.3': resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead '@hutson/parse-repository-url@3.0.2': resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} @@ -5529,17 +5525,14 @@ packages: '@lto-network/lto-crypto@1.1.1': resolution: {integrity: sha512-YA6ATCP+RfWN/0Tvb6CZKs2meUAUAf3cvEVa5tpNNkJjhozxloAONxPP/9DxhUjkmiqWU6fy8xPD2eCYv3lvmQ==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@lto-network/lto-transactions@1.2.12': resolution: {integrity: sha512-bUCwN1xFMr8HFg+rdpxfj5vyCM/2aBSq8kyXyhFw2t8Ovl6BL4rI9zK+4UnOHl5e5z72UWsHgdT3taicxPQiug==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: typescript: 5.6.3 '@lto-network/signature-generator@1.0.0': resolution: {integrity: sha512-NhfsINt8rBoY7F8xijB7fGcY7fzr5dkqLcw3EE9fvVBBhyoI11LxTX78UlokY5T2+X8NvpNpXSSek2yJqYJxHg==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@mapbox/node-pre-gyp@1.0.11': resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} @@ -5670,7 +5663,6 @@ packages: '@npmcli/move-file@1.1.2': resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} engines: {node: '>=10'} - deprecated: This functionality has been moved to @npmcli/fs '@npmcli/name-from-folder@2.0.0': resolution: {integrity: sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==} @@ -6741,7 +6733,6 @@ packages: '@types/nock@11.1.0': resolution: {integrity: sha512-jI/ewavBQ7X5178262JQR0ewicPAcJhXS/iFaNJl0VHLfyosZ/kwSrsa6VNQNSO8i9d8SqdRgOtZSOKJ/+iNMw==} - deprecated: This is a stub types definition. nock provides its own type definitions, so you do not need this installed. '@types/node-forge@1.3.11': resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} @@ -7080,7 +7071,6 @@ packages: abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} - deprecated: Use your platform's native atob() and btoa() methods instead abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -7256,12 +7246,10 @@ packages: are-we-there-yet@2.0.0: resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} engines: {node: '>=10'} - deprecated: This package is no longer supported. are-we-there-yet@3.0.1: resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This package is no longer supported. arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -7572,7 +7560,6 @@ packages: bitcoin-ts@1.15.2: resolution: {integrity: sha512-N5cjC+PjAuTvU3mMcO9aZI5w6lseHickKh6tX6n5p89i2rrUbhgq0KHeOOCYNIbnFcemjGea8uuSXMFBRDl7NQ==} engines: {node: '>=8.9'} - deprecated: The 'bitcoin-ts' package has been renamed to '@bitauth/libauth', and the 'bitcoin-ts' package is no longer maintained. Please switch to '@bitauth/libauth'. bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -7597,7 +7584,6 @@ packages: boolean@3.2.0: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. borc@2.1.2: resolution: {integrity: sha512-Sy9eoUi4OiKzq7VovMn246iTo17kzuyHJKomCfpWMlI6RpfN1gk95w7d7gH264nApVLg0HZfcpz62/g4VH1Y4w==} @@ -8453,12 +8439,10 @@ packages: domexception@2.0.1: resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} engines: {node: '>=8'} - deprecated: Use your platform's native DOMException instead domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} - deprecated: Use your platform's native DOMException instead dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} @@ -8738,7 +8722,6 @@ packages: eslint@8.57.1: resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -9192,12 +9175,10 @@ packages: gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} - deprecated: This package is no longer supported. gauge@4.0.4: resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This package is no longer supported. genson-js@0.0.5: resolution: {integrity: sha512-1i1y9MIGzTRkn4TusWQwLWLu8IJGHgSE+fbQRt1fy68ZKEq2GjDZI/7NUSZFOfTbHz8bgjP4iCIOcdYrgEsMBA==} @@ -9301,16 +9282,13 @@ packages: glob@6.0.4: resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==} - deprecated: Glob versions prior to v9 are no longer supported glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} @@ -9570,7 +9548,6 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -11037,11 +11014,9 @@ packages: multibase@4.0.6: resolution: {integrity: sha512-x23pDe5+svdLz/k5JPGCVdfn7Q5mZVMBETiC+ORfO+sor9Sgs0smJzAjfTbM5tckeCqnaUuMYoz+k3RXMmJClQ==} engines: {node: '>=12.0.0', npm: '>=6.0.0'} - deprecated: This module has been superseded by the multiformats module multicodec@3.2.1: resolution: {integrity: sha512-+expTPftro8VAW8kfvcuNNNBgb9gPeNYV9dn+z1kJRWF2vih+/S79f2RVeIwmrJBUJ6NT9IUPWnZDQvegEh5pw==} - deprecated: This module has been superseded by the multiformats module multiformats@12.1.3: resolution: {integrity: sha512-eajQ/ZH7qXZQR2AgtfpmSMizQzmyYVmCql7pdhldPuYQi4atACekbJaQplk6dWyIi10jCaFnd6pqvcEFXjbaJw==} @@ -11356,12 +11331,10 @@ packages: npmlog@5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} - deprecated: This package is no longer supported. npmlog@6.0.2: resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This package is no longer supported. nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} @@ -11657,7 +11630,6 @@ packages: passport-azure-ad@4.3.5: resolution: {integrity: sha512-LBpXEght7hCMuMNFK4oegdN0uPBa3lpDMy71zQoB0zPg1RrGwdzpjwTiN1WzN0hY77fLyjz9tBr3TGAxnSgtEg==} engines: {node: '>= 8.0.0'} - deprecated: This package is deprecated and no longer supported. For more please visit https://github.com/AzureAD/passport-azure-ad?tab=readme-ov-file#node-js-validation-replacement-for-passportjs passport-http-bearer@1.0.1: resolution: {integrity: sha512-SELQM+dOTuMigr9yu8Wo4Fm3ciFfkMq5h/ZQ8ffi4ELgZrX1xh9PlglqZdcUZ1upzJD/whVyt+YWF62s3U6Ipw==} @@ -11976,10 +11948,6 @@ packages: q@1.5.1: resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} engines: {node: '>=0.6.0', teleport: '>=0.2.0'} - deprecated: |- - You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qr.js@0.0.0: resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==} @@ -12277,7 +12245,6 @@ packages: rimraf@2.4.5: resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==} - deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@2.6.3: @@ -12287,12 +12254,10 @@ packages: rimraf@2.7.1: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@4.4.1: @@ -13435,7 +13400,6 @@ packages: w3c-hr-time@1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} - deprecated: Use your platform's native performance.now() and performance.timeOrigin. w3c-xmlserializer@2.0.0: resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} From 6d68df1930a936857ab86b3292a2b419c51fd28b Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 17 Jan 2025 11:50:50 +0100 Subject: [PATCH 19/24] chore: split out migrations to -UpdateStatusList.ts --- .../migrations/generic/4-CreateStatusList.ts | 34 +++++--- .../1693866470001-CreateStatusList.ts | 41 +++++---- .../1737110469001-UpdateStatusList.ts | 25 ++++++ .../sqlite/1693866470000-CreateStatusList.ts | 8 +- .../sqlite/1737110469000-UpdateStatusList.ts | 84 +++++++++++++++++++ 5 files changed, 154 insertions(+), 38 deletions(-) create mode 100644 packages/data-store/src/migrations/postgres/1737110469001-UpdateStatusList.ts create mode 100644 packages/data-store/src/migrations/sqlite/1737110469000-UpdateStatusList.ts diff --git a/packages/data-store/src/migrations/generic/4-CreateStatusList.ts b/packages/data-store/src/migrations/generic/4-CreateStatusList.ts index 320823521..f826427a5 100644 --- a/packages/data-store/src/migrations/generic/4-CreateStatusList.ts +++ b/packages/data-store/src/migrations/generic/4-CreateStatusList.ts @@ -2,6 +2,8 @@ import Debug from 'debug' import { MigrationInterface, QueryRunner } from 'typeorm' import { CreateStatusList1693866470001 } from '../postgres/1693866470001-CreateStatusList' import { CreateStatusList1693866470002 } from '../sqlite/1693866470000-CreateStatusList' +import { UpdateStatusList1737110469001 } from '../postgres/1737110469001-UpdateStatusList' +import { UpdateStatusList1737110469000 } from '../sqlite/1737110469000-UpdateStatusList' const debug = Debug('sphereon:ssi-sdk:migrations') @@ -12,15 +14,19 @@ export class CreateStatusList1693866470000 implements MigrationInterface { debug('migration: creating issuance branding tables') const dbType = queryRunner.connection.driver.options.type if (dbType === 'postgres') { - debug('using postgres migration file') - const mig = new CreateStatusList1693866470001() - const up = await mig.up(queryRunner) + debug('using postgres migration files') + const createMig = new CreateStatusList1693866470001() + await createMig.up(queryRunner) + const updateMig = new UpdateStatusList1737110469001() + const up = await updateMig.up(queryRunner) debug('Migration statements executed') return up } else if (dbType === 'sqlite' || dbType === 'react-native' || dbType === 'expo') { - debug('using sqlite/react-native migration file') - const mig = new CreateStatusList1693866470002() - const up = await mig.up(queryRunner) + debug('using sqlite/react-native migration files') + const createMig = new CreateStatusList1693866470002() + await createMig.up(queryRunner) + const updateMig = new UpdateStatusList1737110469000() + const up = await updateMig.up(queryRunner) debug('Migration statements executed') return up } else { @@ -34,15 +40,19 @@ export class CreateStatusList1693866470000 implements MigrationInterface { debug('migration: reverting issuance branding tables') const dbType = queryRunner.connection.driver.options.type if (dbType === 'postgres') { - debug('using postgres migration file') - const mig = new CreateStatusList1693866470002() - const down = await mig.down(queryRunner) + debug('using postgres migration files') + const updateMig = new UpdateStatusList1737110469001() + await updateMig.down(queryRunner) + const createMig = new CreateStatusList1693866470001() + const down = await createMig.down(queryRunner) debug('Migration statements executed') return down } else if (dbType === 'sqlite' || dbType === 'react-native' || dbType === 'expo') { - debug('using sqlite/react-native migration file') - const mig = new CreateStatusList1693866470002() - const down = await mig.down(queryRunner) + debug('using sqlite/react-native migration files') + const updateMig = new UpdateStatusList1737110469000() + await updateMig.down(queryRunner) + const createMig = new CreateStatusList1693866470002() + const down = await createMig.down(queryRunner) debug('Migration statements executed') return down } else { diff --git a/packages/data-store/src/migrations/postgres/1693866470001-CreateStatusList.ts b/packages/data-store/src/migrations/postgres/1693866470001-CreateStatusList.ts index b32d20897..e0e94cbb6 100644 --- a/packages/data-store/src/migrations/postgres/1693866470001-CreateStatusList.ts +++ b/packages/data-store/src/migrations/postgres/1693866470001-CreateStatusList.ts @@ -9,40 +9,39 @@ export class CreateStatusList1693866470001 implements MigrationInterface { await queryRunner.query(`CREATE TYPE "StatusList_drivertype_enum" AS ENUM('agent_typeorm', 'agent_kv_store', 'github', 'agent_filesystem')`) await queryRunner.query(`CREATE TYPE "StatusList_credentialidmode_enum" AS ENUM('ISSUANCE', 'PERSISTENCE', 'NEVER')`) + await queryRunner.query( + `CREATE TABLE "StatusListEntry" + ( + "statusListId" character varying NOT NULL, + "statusListIndex" integer NOT NULL, + "credentialId" character varying, + "credentialHash" character varying(128), + "correlationId" character varying(255), + "value" character varying(50), + CONSTRAINT "PK_68704d2d13857360c6b44a3d1d0" PRIMARY KEY ("statusListId", "statusListIndex") + )`, + ) + await queryRunner.query(`CREATE TYPE "StatusList_type_enum" AS ENUM('StatusList2021')`) + await queryRunner.query(`CREATE TYPE "StatusList_drivertype_enum" AS ENUM('agent_typeorm', 'agent_kv_store', 'github', 'agent_filesystem')`) + await queryRunner.query(`CREATE TYPE "StatusList_credentialidmode_enum" AS ENUM('ISSUANCE', 'PERSISTENCE', 'NEVER')`) await queryRunner.query( `CREATE TABLE "StatusList" ( - "id" varchar NOT NULL, - "correlationId" varchar NOT NULL, + "id" character varying NOT NULL, + "correlationId" character varying NOT NULL, "length" integer NOT NULL, "issuer" text NOT NULL, "type" "StatusList_type_enum" NOT NULL DEFAULT 'StatusList2021', "driverType" "StatusList_drivertype_enum" NOT NULL DEFAULT 'agent_typeorm', "credentialIdMode" "StatusList_credentialidmode_enum" NOT NULL DEFAULT 'ISSUANCE', - "proofFormat" varchar NOT NULL DEFAULT 'lds', + "proofFormat" character varying NOT NULL DEFAULT 'lds', + "indexingDirection" character varying NOT NULL DEFAULT 'rightToLeft', + "statusPurpose" character varying NOT NULL DEFAULT 'revocation', "statusListCredential" text, - "indexingDirection" varchar, - "statusPurpose" varchar, - "bitsPerStatus" integer, - "expiresAt" varchar, CONSTRAINT "UQ_correlationId" UNIQUE ("correlationId"), CONSTRAINT "PK_StatusList_Id" PRIMARY KEY ("id") )`, ) - - await queryRunner.query( - `CREATE TABLE "StatusListEntry" - ( - "statusListId" varchar NOT NULL, - "statusListIndex" integer NOT NULL, - "credentialId" varchar, - "credentialHash" varchar(128), - "correlationId" varchar(255), - "value" varchar(50), - CONSTRAINT "PK_68704d2d13857360c6b44a3d1d0" PRIMARY KEY ("statusListId", "statusListIndex") - )`, - ) - await queryRunner.query( `ALTER TABLE "StatusListEntry" ADD CONSTRAINT "FK_statusListEntry_statusListId" FOREIGN KEY ("statusListId") REFERENCES "StatusList" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, diff --git a/packages/data-store/src/migrations/postgres/1737110469001-UpdateStatusList.ts b/packages/data-store/src/migrations/postgres/1737110469001-UpdateStatusList.ts new file mode 100644 index 000000000..958c9dff6 --- /dev/null +++ b/packages/data-store/src/migrations/postgres/1737110469001-UpdateStatusList.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class UpdateStatusList1737110469001 implements MigrationInterface { + name = 'UpdateStatusList1737110469001' + + public async up(queryRunner: QueryRunner): Promise { + // Add new enum value + await queryRunner.query(`ALTER TYPE "StatusList_type_enum" ADD VALUE 'OAuthStatusList'`) + + // Make columns nullable and add new columns + await queryRunner.query(`ALTER TABLE "StatusList" ALTER COLUMN "indexingDirection" DROP NOT NULL`) + await queryRunner.query(`ALTER TABLE "StatusList" ALTER COLUMN "statusPurpose" DROP NOT NULL`) + await queryRunner.query(`ALTER TABLE "StatusList" ADD "bitsPerStatus" integer`) + await queryRunner.query(`ALTER TABLE "StatusList" ADD "expiresAt" varchar`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "StatusList" DROP COLUMN "expiresAt"`) + await queryRunner.query(`ALTER TABLE "StatusList" DROP COLUMN "bitsPerStatus"`) + await queryRunner.query(`ALTER TABLE "StatusList" ALTER COLUMN "statusPurpose" SET NOT NULL`) + await queryRunner.query(`ALTER TABLE "StatusList" ALTER COLUMN "indexingDirection" SET NOT NULL`) + + // Note: Cannot remove enum value in Postgres, would need to recreate the type + } +} diff --git a/packages/data-store/src/migrations/sqlite/1693866470000-CreateStatusList.ts b/packages/data-store/src/migrations/sqlite/1693866470000-CreateStatusList.ts index c55fd50ff..70c5a207d 100644 --- a/packages/data-store/src/migrations/sqlite/1693866470000-CreateStatusList.ts +++ b/packages/data-store/src/migrations/sqlite/1693866470000-CreateStatusList.ts @@ -23,16 +23,14 @@ export class CreateStatusList1693866470002 implements MigrationInterface { "correlationId" varchar NOT NULL, "length" integer NOT NULL, "issuer" text NOT NULL, - "type" varchar CHECK ( "type" IN ('StatusList2021', 'OAuthStatusList') ) NOT NULL DEFAULT ('StatusList2021'), + "type" varchar CHECK ( "type" IN ('StatusList2021') ) NOT NULL DEFAULT ('StatusList2021'), "driverType" varchar CHECK ( "driverType" IN ('agent_typeorm', 'agent_kv_store', 'github', 'agent_filesystem') ) NOT NULL DEFAULT ('agent_typeorm'), "credentialIdMode" varchar CHECK ( "credentialIdMode" IN ('ISSUANCE', 'PERSISTENCE', 'NEVER') ) NOT NULL DEFAULT ('ISSUANCE'), "proofFormat" varchar CHECK ( "proofFormat" IN ('lds', 'jwt') ) NOT NULL DEFAULT ('lds'), - "indexingDirection" varchar CHECK ( "indexingDirection" IN ('rightToLeft') ), - "statusPurpose" varchar, + "indexingDirection" varchar CHECK ( "indexingDirection" IN ('rightToLeft') ) NOT NULL DEFAULT ('rightToLeft'), + "statusPurpose" varchar NOT NULL DEFAULT ('revocation'), "statusListCredential" text, - "bitsPerStatus" integer, - "expiresAt" varchar, CONSTRAINT "UQ_correlationId" UNIQUE ("correlationId") )`, ) diff --git a/packages/data-store/src/migrations/sqlite/1737110469000-UpdateStatusList.ts b/packages/data-store/src/migrations/sqlite/1737110469000-UpdateStatusList.ts new file mode 100644 index 000000000..11e004549 --- /dev/null +++ b/packages/data-store/src/migrations/sqlite/1737110469000-UpdateStatusList.ts @@ -0,0 +1,84 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class UpdateStatusList1737110469000 implements MigrationInterface { + name = 'UpdateStatusList1737110469000' + + public async up(queryRunner: QueryRunner): Promise { + // Create temporary table with new schema + await queryRunner.query( + `CREATE TABLE "temporary_StatusList" ( + "id" varchar PRIMARY KEY NOT NULL, + "correlationId" varchar NOT NULL, + "length" integer NOT NULL, + "issuer" text NOT NULL, + "type" varchar CHECK( "type" IN ('StatusList2021', 'OAuthStatusList') ) NOT NULL DEFAULT ('StatusList2021'), + "driverType" varchar CHECK( "driverType" IN ('agent_typeorm','agent_kv_store','github','agent_filesystem') ) NOT NULL DEFAULT ('agent_typeorm'), + "credentialIdMode" varchar CHECK( "credentialIdMode" IN ('ISSUANCE','PERSISTENCE','NEVER') ) NOT NULL DEFAULT ('ISSUANCE'), + "proofFormat" varchar CHECK( "proofFormat" IN ('lds','jwt') ) NOT NULL DEFAULT ('lds'), + "indexingDirection" varchar CHECK( "indexingDirection" IN ('rightToLeft') ), + "statusPurpose" varchar, + "statusListCredential" text, + "bitsPerStatus" integer, + "expiresAt" varchar, + CONSTRAINT "UQ_correlationId" UNIQUE ("correlationId") + )`, + ) + + // Copy data from old table to temporary table + await queryRunner.query( + `INSERT INTO "temporary_StatusList"( + "id", "correlationId", "length", "issuer", "type", "driverType", + "credentialIdMode", "proofFormat", "indexingDirection", "statusPurpose", + "statusListCredential" + ) + SELECT + "id", "correlationId", "length", "issuer", "type", "driverType", + "credentialIdMode", "proofFormat", "indexingDirection", "statusPurpose", + "statusListCredential" + FROM "StatusList"`, + ) + + // Drop old table and rename temporary table + await queryRunner.query(`DROP TABLE "StatusList"`) + await queryRunner.query(`ALTER TABLE "temporary_StatusList" RENAME TO "StatusList"`) + } + + public async down(queryRunner: QueryRunner): Promise { + // Create temporary table with old schema + await queryRunner.query( + `CREATE TABLE "temporary_StatusList" ( + "id" varchar PRIMARY KEY NOT NULL, + "correlationId" varchar NOT NULL, + "length" integer NOT NULL, + "issuer" text NOT NULL, + "type" varchar CHECK( "type" IN ('StatusList2021') ) NOT NULL DEFAULT ('StatusList2021'), + "driverType" varchar CHECK( "driverType" IN ('agent_typeorm','agent_kv_store','github','agent_filesystem') ) NOT NULL DEFAULT ('agent_typeorm'), + "credentialIdMode" varchar CHECK( "credentialIdMode" IN ('ISSUANCE','PERSISTENCE','NEVER') ) NOT NULL DEFAULT ('ISSUANCE'), + "proofFormat" varchar CHECK( "proofFormat" IN ('lds','jwt') ) NOT NULL DEFAULT ('lds'), + "indexingDirection" varchar CHECK( "indexingDirection" IN ('rightToLeft') ) NOT NULL DEFAULT ('rightToLeft'), + "statusPurpose" varchar NOT NULL DEFAULT ('revocation'), + "statusListCredential" text, + CONSTRAINT "UQ_correlationId" UNIQUE ("correlationId") + )`, + ) + + // Copy data back, excluding new columns + await queryRunner.query( + `INSERT INTO "temporary_StatusList"( + "id", "correlationId", "length", "issuer", "type", "driverType", + "credentialIdMode", "proofFormat", "indexingDirection", "statusPurpose", + "statusListCredential" + ) + SELECT + "id", "correlationId", "length", "issuer", + CASE WHEN "type" = 'OAuthStatusList' THEN 'StatusList2021' ELSE "type" END, + "driverType", "credentialIdMode", "proofFormat", "indexingDirection", + COALESCE("statusPurpose", 'revocation'), "statusListCredential" + FROM "StatusList"`, + ) + + // Drop new table and rename temporary table back + await queryRunner.query(`DROP TABLE "StatusList"`) + await queryRunner.query(`ALTER TABLE "temporary_StatusList" RENAME TO "StatusList"`) + } +} From 6ac08a740300b6862a75caf5fd2cb0537386f50c Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Sat, 25 Jan 2025 13:04:14 +0100 Subject: [PATCH 20/24] chore: PR feedback --- .../entities/statusList/StatusListEntities.ts | 4 +- .../1737110469001-UpdateStatusList.ts | 2 +- .../sqlite/1737110469000-UpdateStatusList.ts | 2 +- .../src/types/statusList/statusList.ts | 2 +- .../src/utils/statusList/MappingUtils.ts | 24 +++++----- packages/ssi-types/src/types/status-list.ts | 4 +- .../src/drivers.ts | 4 +- .../src/status-list-adapters.ts | 2 +- .../src/api-functions.ts | 2 +- .../src/agent/StatusListPlugin.ts | 25 ++++++----- packages/vc-status-list/src/functions.ts | 29 ++++++------ .../vc-status-list/src/impl/IStatusList.ts | 6 +++ .../src/impl/OAuthStatusList.ts | 43 +++++++++++++----- .../vc-status-list/src/impl/StatusList2021.ts | 45 +++++++++++++++---- .../src/impl/StatusListFactory.ts | 10 ++--- .../vc-status-list/src/impl/encoding/cbor.ts | 9 ++-- .../vc-status-list/src/impl/encoding/jwt.ts | 32 +++++++++++-- packages/vc-status-list/src/types/index.ts | 25 ++++++++--- packages/vc-status-list/src/utils.ts | 20 +++++---- 19 files changed, 192 insertions(+), 98 deletions(-) diff --git a/packages/data-store/src/entities/statusList/StatusListEntities.ts b/packages/data-store/src/entities/statusList/StatusListEntities.ts index 6b74e7fbd..48e948298 100644 --- a/packages/data-store/src/entities/statusList/StatusListEntities.ts +++ b/packages/data-store/src/entities/statusList/StatusListEntities.ts @@ -110,6 +110,6 @@ export class StatusList2021Entity extends StatusListEntity { export class OAuthStatusListEntity extends StatusListEntity { @Column({ type: 'integer', name: 'bitsPerStatus', nullable: false }) bitsPerStatus!: number - @Column({ type: 'varchar', name: 'expiresAt', nullable: true }) - expiresAt?: string + @Column({ type: 'timestamptz', name: 'expiresAt', nullable: true }) + expiresAt?: Date } diff --git a/packages/data-store/src/migrations/postgres/1737110469001-UpdateStatusList.ts b/packages/data-store/src/migrations/postgres/1737110469001-UpdateStatusList.ts index 958c9dff6..18ea0ade8 100644 --- a/packages/data-store/src/migrations/postgres/1737110469001-UpdateStatusList.ts +++ b/packages/data-store/src/migrations/postgres/1737110469001-UpdateStatusList.ts @@ -11,7 +11,7 @@ export class UpdateStatusList1737110469001 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "StatusList" ALTER COLUMN "indexingDirection" DROP NOT NULL`) await queryRunner.query(`ALTER TABLE "StatusList" ALTER COLUMN "statusPurpose" DROP NOT NULL`) await queryRunner.query(`ALTER TABLE "StatusList" ADD "bitsPerStatus" integer`) - await queryRunner.query(`ALTER TABLE "StatusList" ADD "expiresAt" varchar`) + await queryRunner.query(`ALTER TABLE "StatusList" ADD "expiresAt" timestamp with time zone`) } public async down(queryRunner: QueryRunner): Promise { diff --git a/packages/data-store/src/migrations/sqlite/1737110469000-UpdateStatusList.ts b/packages/data-store/src/migrations/sqlite/1737110469000-UpdateStatusList.ts index 11e004549..80772f0d4 100644 --- a/packages/data-store/src/migrations/sqlite/1737110469000-UpdateStatusList.ts +++ b/packages/data-store/src/migrations/sqlite/1737110469000-UpdateStatusList.ts @@ -19,7 +19,7 @@ export class UpdateStatusList1737110469000 implements MigrationInterface { "statusPurpose" varchar, "statusListCredential" text, "bitsPerStatus" integer, - "expiresAt" varchar, + "expiresAt" datetime, CONSTRAINT "UQ_correlationId" UNIQUE ("correlationId") )`, ) diff --git a/packages/data-store/src/types/statusList/statusList.ts b/packages/data-store/src/types/statusList/statusList.ts index d681fb16d..9ca1e9d23 100644 --- a/packages/data-store/src/types/statusList/statusList.ts +++ b/packages/data-store/src/types/statusList/statusList.ts @@ -29,7 +29,7 @@ export interface IStatusList2021Entity extends IStatusListEntity { export interface IOAuthStatusListEntity extends IStatusListEntity { bitsPerStatus: number - expiresAt?: string + expiresAt?: Date } export interface IStatusListEntryEntity { diff --git a/packages/data-store/src/utils/statusList/MappingUtils.ts b/packages/data-store/src/utils/statusList/MappingUtils.ts index 3afb78a24..c0de95aec 100644 --- a/packages/data-store/src/utils/statusList/MappingUtils.ts +++ b/packages/data-store/src/utils/statusList/MappingUtils.ts @@ -59,7 +59,7 @@ export const statusListFrom = (entity: StatusListEntity): IStatusListEntity => { throw new Error(`Invalid status list type ${typeof entity}`) } -function setBaseFields(entity: StatusListEntity, args: IStatusListEntity) { +const setBaseFields = (entity: StatusListEntity, args: IStatusListEntity) => { entity.id = args.id entity.correlationId = args.correlationId entity.length = args.length @@ -70,15 +70,13 @@ function setBaseFields(entity: StatusListEntity, args: IStatusListEntity) { entity.statusListCredential = args.statusListCredential } -function getBaseFields(entity: StatusListEntity): Omit { - return { - id: entity.id, - correlationId: entity.correlationId, - length: entity.length, - issuer: entity.issuer, - driverType: entity.driverType, - credentialIdMode: entity.credentialIdMode, - proofFormat: entity.proofFormat, - statusListCredential: entity.statusListCredential, - } -} +const getBaseFields = (entity: StatusListEntity): Omit => ({ + id: entity.id, + correlationId: entity.correlationId, + length: entity.length, + issuer: entity.issuer, + driverType: entity.driverType, + credentialIdMode: entity.credentialIdMode, + proofFormat: entity.proofFormat, + statusListCredential: entity.statusListCredential, +}) diff --git a/packages/ssi-types/src/types/status-list.ts b/packages/ssi-types/src/types/status-list.ts index d8af92b85..eb1e5277b 100644 --- a/packages/ssi-types/src/types/status-list.ts +++ b/packages/ssi-types/src/types/status-list.ts @@ -1,5 +1,5 @@ import { W3CVerifiableCredential } from './w3c-vc' -import { ProofFormat as VmoProofFormat } from '@veramo/core' +import { ProofFormat as VeramoProofFormat } from '@veramo/core' export enum StatusListType { StatusList2021 = 'StatusList2021', @@ -9,4 +9,4 @@ export type CWT = string export type StatusListCredential = W3CVerifiableCredential | CWT -export type ProofFormat = VmoProofFormat | 'cbor' +export type ProofFormat = VeramoProofFormat | 'cbor' diff --git a/packages/vc-status-list-issuer-drivers/src/drivers.ts b/packages/vc-status-list-issuer-drivers/src/drivers.ts index 78850fe1e..740f0250a 100644 --- a/packages/vc-status-list-issuer-drivers/src/drivers.ts +++ b/packages/vc-status-list-issuer-drivers/src/drivers.ts @@ -215,9 +215,7 @@ export class AgentDataSourceStatusListDriver implements IStatusListDriver { }, statusListEntry, } - } - - if (this.isOAuthStatusListEntity(statusList)) { + } else if (this.isOAuthStatusListEntity(statusList)) { return { credentialStatus: { id: `${statusList.id}#${statusListEntry.statusListIndex}`, diff --git a/packages/vc-status-list-issuer-drivers/src/status-list-adapters.ts b/packages/vc-status-list-issuer-drivers/src/status-list-adapters.ts index 6f309a3fe..fddbeea79 100644 --- a/packages/vc-status-list-issuer-drivers/src/status-list-adapters.ts +++ b/packages/vc-status-list-issuer-drivers/src/status-list-adapters.ts @@ -31,7 +31,7 @@ export function statusListResultToEntity(result: StatusListResult): StatusList20 return Object.assign(new OAuthStatusListEntity(), { ...baseFields, bitsPerStatus: result.oauthStatusList.bitsPerStatus, - expiresAt: undefined, // Optional field + expiresAt: result.oauthStatusList.expiresAt, }) } throw new Error(`Unsupported status list type: ${result.type}`) diff --git a/packages/vc-status-list-issuer-rest-api/src/api-functions.ts b/packages/vc-status-list-issuer-rest-api/src/api-functions.ts index 9136ec507..dd770dfee 100644 --- a/packages/vc-status-list-issuer-rest-api/src/api-functions.ts +++ b/packages/vc-status-list-issuer-rest-api/src/api-functions.ts @@ -104,7 +104,7 @@ export function getStatusListCredentialIndexStatusEndpoint(router: Router, conte const type = details.type === StatusListType.StatusList2021 ? 'StatusList2021Entry' : details.type const status = await checkStatusIndexFromStatusListCredential({ statusListCredential: details.statusListCredential, - statusPurpose: details.statusList2021?.statusPurpose, + ...(details.type === StatusListType.StatusList2021 ? { statusPurpose: details.statusList2021?.statusPurpose } : {}), type, id: details.id, statusListIndex, diff --git a/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts b/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts index 4abd501ea..dd07c2f42 100644 --- a/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts +++ b/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts @@ -12,7 +12,7 @@ import { } from '@sphereon/ssi-sdk.vc-status-list' import { getDriver } from '@sphereon/ssi-sdk.vc-status-list-issuer-drivers' import { Loggers } from '@sphereon/ssi-types' -import { IAgentContext, IAgentPlugin } from '@veramo/core' +import { IAgentContext, IAgentPlugin, IKeyManager } from '@veramo/core' import { createStatusListFromInstance, handleCredentialStatus } from '../functions' import { StatusListInstance } from '../types' @@ -47,7 +47,10 @@ export class StatusListPlugin implements IAgentPlugin { this.autoCreateInstances = opts.autoCreateInstances ?? true } - private async slGetStatusList(args: GetStatusListArgs, context: IAgentContext): Promise { + private async slGetStatusList( + args: GetStatusListArgs, + context: IAgentContext, + ): Promise { const sl = this.instances.find((instance) => instance.id === args.id || instance.correlationId === args.correlationId) const dataSource = (sl?.dataSource ?? args?.dataSource) @@ -73,7 +76,7 @@ export class StatusListPlugin implements IAgentPlugin { private async slCreateStatusList( args: CreateNewStatusListArgs, - context: IAgentContext, + context: IAgentContext, ): Promise { const sl = await createNewStatusList(args, context) const dataSource = args?.dataSource @@ -86,29 +89,29 @@ export class StatusListPlugin implements IAgentPlugin { correlationId: sl.correlationId, dataSource, }) - let statusList: StatusListResult | undefined = undefined + let statusListDetails: StatusListResult | undefined = undefined try { - statusList = await this.slGetStatusList(args, context) + statusListDetails = await this.slGetStatusList(args, context) } catch (e) { // That is fine if there is no status list yet } - if (statusList && this.instances.find((sl) => sl.id === args.id || sl.correlationId === args.correlationId)) { + if (statusListDetails && this.instances.find((sl) => sl.id === args.id || sl.correlationId === args.correlationId)) { return Promise.reject(Error(`Status list with id ${args.id} or correlation id ${args.correlationId} already exists`)) } else { - statusList = await driver.createStatusList({ + statusListDetails = await driver.createStatusList({ statusListCredential: sl.statusListCredential, correlationId: sl.correlationId, }) this.instances.push({ - correlationId: statusList.correlationId, - id: statusList.id, + correlationId: statusListDetails.correlationId, + id: statusListDetails.id, dataSource, - driverType: statusList.driverType!, + driverType: statusListDetails.driverType!, driverOptions: driver.getOptions(), }) } - return statusList + return statusListDetails } private async slAddStatusToCredential(args: IAddStatusToCredentialArgs, context: IRequiredContext): Promise { diff --git a/packages/vc-status-list/src/functions.ts b/packages/vc-status-list/src/functions.ts index 67e1259fe..e0833bbe6 100644 --- a/packages/vc-status-list/src/functions.ts +++ b/packages/vc-status-list/src/functions.ts @@ -1,6 +1,6 @@ import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { CredentialMapper, ProofFormat, StatusListDriverType, StatusListType, StatusListCredential, StatusPurpose2021 } from '@sphereon/ssi-types' -import { CredentialStatus, DIDDocument, IAgentContext, ICredentialPlugin, ProofFormat as VmoProofFormat } from '@veramo/core' +import { CredentialMapper, ProofFormat, StatusListCredential, StatusListDriverType, StatusListType, StatusPurpose2021 } from '@sphereon/ssi-types' +import { CredentialStatus, DIDDocument, IAgentContext, ICredentialPlugin, ProofFormat as VeramoProofFormat } from '@veramo/core' import { checkStatus } from '@sphereon/vc-status-list' import { CredentialJwtOrJSON, StatusMethod } from 'credential-status' @@ -11,7 +11,7 @@ import { StatusListResult, StatusOAuth, UpdateStatusListFromEncodedListArgs, - UpdateStatusListFromStatusListCredentialArgs, + UpdateStatusListIndexArgs, } from './types' import { assertValidProofType, determineStatusListType, getAssertedValue, getAssertedValues } from './utils' import { getStatusListImplementation } from './impl/StatusListFactory' @@ -155,7 +155,7 @@ export async function createNewStatusList( } export async function updateStatusIndexFromStatusListCredential( - args: UpdateStatusListFromStatusListCredentialArgs, + args: UpdateStatusListIndexArgs, context: IAgentContext, ): Promise { const credential = getAssertedValue('statusListCredential', args.statusListCredential) @@ -176,16 +176,14 @@ export async function statusListCredentialToDetails(args: { if (!type) { throw new Error('Invalid status list credential type') } - const statusListType = type.replace('Credential', '') as StatusListType + const statusListType = type.replace('Credential', '') as StatusListType // "StatusList2021Credential" is a VC schema type and does not map 1:1 to our internal StatusListType enum + const implementation = getStatusListImplementation(statusListType) - return implementation.updateStatusListIndex( - { - statusListCredential: args.statusListCredential, - statusListIndex: 0, - value: 0, - }, - {} as IAgentContext, - ) + return await implementation.toStatusListDetails({ + statusListPayload: credential, + correlationId: args.correlationId, + driverType: args.driverType, + }) } export async function updateStatusListIndexFromEncodedList( @@ -197,7 +195,6 @@ export async function updateStatusListIndexFromEncodedList( return implementation.updateStatusListFromEncodedList(args, context) } -// TODO Is this still in use? Or do we need to redesign this after having multiple status list types? export async function statusList2021ToVerifiableCredential( args: StatusList2021ToVerifiableCredentialArgs, context: IAgentContext, @@ -210,7 +207,7 @@ export async function statusList2021ToVerifiableCredential( }) const proofFormat: ProofFormat = args?.proofFormat ?? 'lds' assertValidProofType(StatusListType.StatusList2021, proofFormat) - const vmoProofFormat: VmoProofFormat = proofFormat as VmoProofFormat + const veramoProofFormat: VeramoProofFormat = proofFormat as VeramoProofFormat const encodedList = getAssertedValue('encodedList', args.encodedList) const statusPurpose = getAssertedValue('statusPurpose', args.statusPurpose) @@ -231,7 +228,7 @@ export async function statusList2021ToVerifiableCredential( const verifiableCredential = await context.agent.createVerifiableCredential({ credential, keyRef: identifier.kmsKeyRef, - proofFormat: vmoProofFormat, + proofFormat: veramoProofFormat, fetchRemoteContexts: true, }) diff --git a/packages/vc-status-list/src/impl/IStatusList.ts b/packages/vc-status-list/src/impl/IStatusList.ts index 727f90bee..9bbf0092b 100644 --- a/packages/vc-status-list/src/impl/IStatusList.ts +++ b/packages/vc-status-list/src/impl/IStatusList.ts @@ -6,6 +6,7 @@ import { Status2021, StatusListResult, StatusOAuth, + ToStatusListDetailsArgs, UpdateStatusListFromEncodedListArgs, UpdateStatusListIndexArgs, } from '../types' @@ -33,4 +34,9 @@ export interface IStatusList { * Checks the status at a given index in the status list */ checkStatusIndex(args: CheckStatusIndexArgs): Promise + + /** + * Collects the status list details + */ + toStatusListDetails(args: ToStatusListDetailsArgs): Promise } diff --git a/packages/vc-status-list/src/impl/OAuthStatusList.ts b/packages/vc-status-list/src/impl/OAuthStatusList.ts index ef0d40e95..3b7d11eb8 100644 --- a/packages/vc-status-list/src/impl/OAuthStatusList.ts +++ b/packages/vc-status-list/src/impl/OAuthStatusList.ts @@ -1,31 +1,28 @@ -import { IAgentContext, ICredentialPlugin } from '@veramo/core' -import { ProofFormat, StatusListType } from '@sphereon/ssi-types' +import { IAgentContext, ICredentialPlugin, IKeyManager } from '@veramo/core' +import { CompactJWT, CWT, ProofFormat, StatusListType } from '@sphereon/ssi-types' import { CheckStatusIndexArgs, CreateStatusListArgs, SignedStatusListData, StatusListResult, StatusOAuth, + ToStatusListDetailsArgs, UpdateStatusListFromEncodedListArgs, UpdateStatusListIndexArgs, } from '../types' import { determineProofFormat, getAssertedValue, getAssertedValues } from '../utils' import { IStatusList } from './IStatusList' -import { StatusList, StatusListJWTHeaderParameters } from '@sd-jwt/jwt-status-list' +import { StatusList } from '@sd-jwt/jwt-status-list' import { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service' import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' import { createSignedJwt, decodeStatusListJWT } from './encoding/jwt' import { createSignedCbor, decodeStatusListCWT } from './encoding/cbor' -type IRequiredContext = IAgentContext +type IRequiredContext = IAgentContext -export const BITS_PER_STATUS_DEFAULT = 2 // 2 bits are sufficient for 0x00 - "VALID" 0x01 - "INVALID" & 0x02 - "SUSPENDED" +export const DEFAULT_BITS_PER_STATUS = 2 // 2 bits are sufficient for 0x00 - "VALID" 0x01 - "INVALID" & 0x02 - "SUSPENDED" export const DEFAULT_LIST_LENGTH = 250000 export const DEFAULT_PROOF_FORMAT = 'jwt' as ProofFormat -export const STATUS_LIST_JWT_HEADER: StatusListJWTHeaderParameters = { - alg: 'EdDSA', - typ: 'statuslist+jwt', -} export class OAuthStatusListImplementation implements IStatusList { async createNewStatusList(args: CreateStatusListArgs, context: IRequiredContext): Promise { @@ -36,7 +33,7 @@ export class OAuthStatusListImplementation implements IStatusList { const proofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT const { issuer, id, keyRef } = args const length = args.length ?? DEFAULT_LIST_LENGTH - const bitsPerStatus = args.oauthStatusList.bitsPerStatus ?? BITS_PER_STATUS_DEFAULT + const bitsPerStatus = args.oauthStatusList.bitsPerStatus ?? DEFAULT_BITS_PER_STATUS const issuerString = typeof issuer === 'string' ? issuer : issuer.id const correlationId = getAssertedValue('correlationId', args.correlationId) @@ -103,7 +100,7 @@ export class OAuthStatusListImplementation implements IStatusList { const proofFormat = args.proofFormat ?? DEFAULT_PROOF_FORMAT const { issuer, id } = getAssertedValues(args) - const bitsPerStatus = args.oauthStatusList.bitsPerStatus ?? BITS_PER_STATUS_DEFAULT + const bitsPerStatus = args.oauthStatusList.bitsPerStatus ?? DEFAULT_BITS_PER_STATUS const issuerString = typeof issuer === 'string' ? issuer : issuer.id const listToUpdate = StatusList.decompressStatusList(args.encodedList, bitsPerStatus) @@ -143,9 +140,31 @@ export class OAuthStatusListImplementation implements IStatusList { return statusList.getStatus(index) } + async toStatusListDetails(args: ToStatusListDetailsArgs): Promise { + const { statusListPayload } = args as { statusListPayload: CompactJWT | CWT } + const proofFormat = determineProofFormat(statusListPayload) + const decoded = proofFormat === 'jwt' ? decodeStatusListJWT(statusListPayload) : decodeStatusListCWT(statusListPayload) + const { statusList, issuer, id } = decoded + + return { + id, + encodedList: statusList.compressStatusList(), + issuer, + type: StatusListType.OAuthStatusList, + proofFormat, + length: statusList.statusList.length, + statusListCredential: statusListPayload, + oauthStatusList: { + bitsPerStatus: statusList.getBitsPerStatus(), + }, + ...(args.correlationId && { correlationId: args.correlationId }), + ...(args.driverType && { driverType: args.driverType }), + } + } + private async createSignedStatusList( proofFormat: 'jwt' | 'lds' | 'EthereumEip712Signature2021' | 'cbor', - context: IAgentContext, + context: IAgentContext, statusList: StatusList, issuerString: string, id: string, diff --git a/packages/vc-status-list/src/impl/StatusList2021.ts b/packages/vc-status-list/src/impl/StatusList2021.ts index d643c0cbc..3277c3369 100644 --- a/packages/vc-status-list/src/impl/StatusList2021.ts +++ b/packages/vc-status-list/src/impl/StatusList2021.ts @@ -1,6 +1,6 @@ -import { IAgentContext, ICredentialPlugin, ProofFormat as VmoProofFormat } from '@veramo/core' +import { IAgentContext, ICredentialPlugin, ProofFormat as VeramoProofFormat } from '@veramo/core' import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { CredentialMapper, DocumentFormat, IIssuer, ProofFormat, StatusListType, StatusListCredential } from '@sphereon/ssi-types' +import { CredentialMapper, DocumentFormat, IIssuer, ProofFormat, StatusListCredential, StatusListType } from '@sphereon/ssi-types' import { StatusList } from '@sphereon/vc-status-list' import { IStatusList } from './IStatusList' @@ -9,13 +9,14 @@ import { CreateStatusListArgs, Status2021, StatusListResult, + ToStatusListDetailsArgs, UpdateStatusListFromEncodedListArgs, UpdateStatusListIndexArgs, } from '../types' import { assertValidProofType, getAssertedProperty, getAssertedValue, getAssertedValues } from '../utils' export const DEFAULT_LIST_LENGTH = 250000 -export const DEFAULT_PROOF_FORMAT = 'lds' as VmoProofFormat +export const DEFAULT_PROOF_FORMAT = 'lds' as VeramoProofFormat export class StatusList2021Implementation implements IStatusList { async createNewStatusList( @@ -25,7 +26,7 @@ export class StatusList2021Implementation implements IStatusList { const length = args?.length ?? DEFAULT_LIST_LENGTH const proofFormat: ProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT assertValidProofType(StatusListType.StatusList2021, proofFormat) - const vmoProofFormat: VmoProofFormat = proofFormat as VmoProofFormat + const veramoProofFormat: VeramoProofFormat = proofFormat as VeramoProofFormat const { issuer, id } = args const correlationId = getAssertedValue('correlationId', args.correlationId) @@ -38,7 +39,7 @@ export class StatusList2021Implementation implements IStatusList { { ...args, encodedList, - proofFormat: vmoProofFormat, + proofFormat: veramoProofFormat, }, context, ) @@ -109,7 +110,7 @@ export class StatusList2021Implementation implements IStatusList { } const proofFormat: ProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT assertValidProofType(StatusListType.StatusList2021, proofFormat) - const vmoProofFormat: VmoProofFormat = proofFormat as VmoProofFormat + const veramoProofFormat: VeramoProofFormat = proofFormat as VeramoProofFormat const { issuer, id } = getAssertedValues(args) const statusList = await StatusList.decode({ encodedList: args.encodedList }) @@ -122,7 +123,7 @@ export class StatusList2021Implementation implements IStatusList { id, issuer, encodedList: newEncodedList, - proofFormat: vmoProofFormat, + proofFormat: veramoProofFormat, keyRef: args.keyRef, }, context, @@ -153,12 +154,40 @@ export class StatusList2021Implementation implements IStatusList { return status ? Status2021.Invalid : Status2021.Valid } + async toStatusListDetails(args: ToStatusListDetailsArgs): Promise { + 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: ProofFormat = 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, + 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: VmoProofFormat + proofFormat: VeramoProofFormat keyRef?: string }, context: IAgentContext, diff --git a/packages/vc-status-list/src/impl/StatusListFactory.ts b/packages/vc-status-list/src/impl/StatusListFactory.ts index 4ebea3ea8..b644e69a9 100644 --- a/packages/vc-status-list/src/impl/StatusListFactory.ts +++ b/packages/vc-status-list/src/impl/StatusListFactory.ts @@ -20,15 +20,15 @@ export class StatusListFactory { return StatusListFactory.instance } - public getImplementation(type: StatusListType): IStatusList { - const implementation = this.implementations.get(type) - if (!implementation) { + public getByType(type: StatusListType): IStatusList { + const statusList = this.implementations.get(type) + if (!statusList) { throw new Error(`No implementation found for status list type: ${type}`) } - return implementation + return statusList } } export function getStatusListImplementation(type: StatusListType): IStatusList { - return StatusListFactory.getInstance().getImplementation(type) + return StatusListFactory.getInstance().getByType(type) } diff --git a/packages/vc-status-list/src/impl/encoding/cbor.ts b/packages/vc-status-list/src/impl/encoding/cbor.ts index d7fddd51f..4883bf1ab 100644 --- a/packages/vc-status-list/src/impl/encoding/cbor.ts +++ b/packages/vc-status-list/src/impl/encoding/cbor.ts @@ -48,17 +48,18 @@ export const createSignedCbor = async ( ) const protectedHeaderEncoded = cbor.Cbor.encode(protectedHeader) const claimsMap = buildClaimsMap(id, issuerString, statusListMap) - const claimsEncoded = cbor.Cbor.encode(claimsMap) + const claimsEncoded: Int8Array = cbor.Cbor.encode(claimsMap) - const signedCWT = await context.agent.keyManagerSign({ + const signedCWT: string = await context.agent.keyManagerSign({ keyRef: identifier.kmsKeyRef, - data: claimsEncoded, + data: base64url.encode(Buffer.from(claimsEncoded)), // TODO test on RN encoding: undefined, }) const protectedHeaderEncodedInt8 = new Int8Array(protectedHeaderEncoded) const claimsEncodedInt8 = new Int8Array(claimsEncoded) - const signatureInt8 = new Int8Array(signedCWT.signature) + const signatureBytes = base64url.decode(signedCWT) + const signatureInt8 = new Int8Array(Buffer.from(signatureBytes)) const cwtArray = new cbor.CborArray( kotlin.collections.KtMutableList.fromJsArray([ diff --git a/packages/vc-status-list/src/impl/encoding/jwt.ts b/packages/vc-status-list/src/impl/encoding/jwt.ts index 37678886f..27c2aeb63 100644 --- a/packages/vc-status-list/src/impl/encoding/jwt.ts +++ b/packages/vc-status-list/src/impl/encoding/jwt.ts @@ -1,10 +1,13 @@ -import { CompactJWT } from '@sphereon/ssi-types' -import { createHeaderAndPayload, StatusList, StatusListJWTPayload } from '@sd-jwt/jwt-status-list' +import { CompactJWT, JoseSignatureAlgorithm } from '@sphereon/ssi-types' +import { createHeaderAndPayload, StatusList, StatusListJWTHeaderParameters, StatusListJWTPayload } from '@sd-jwt/jwt-status-list' import base64url from 'base64url' import { JWTPayload } from 'did-jwt' -import { STATUS_LIST_JWT_HEADER } from '../OAuthStatusList' import { IRequiredContext, SignedStatusListData } from '../../types' import { DecodedStatusListPayload, resolveIdentifier } from './common' +import { TKeyType } from '@veramo/core' +import { ensureManagedIdentifierResult } from '@sphereon/ssi-sdk-ext.identifier-resolution' + +const STATUS_LIST_JWT_TYP = 'statuslist+jwt' export const createSignedJwt = async ( context: IRequiredContext, @@ -14,13 +17,19 @@ export const createSignedJwt = async ( keyRef?: string, ): Promise => { const identifier = await resolveIdentifier(context, issuerString, keyRef) + const resolution = await ensureManagedIdentifierResult(identifier, context) + const payload: JWTPayload = { iss: issuerString, sub: id, iat: Math.floor(Date.now() / 1000), } - const values = createHeaderAndPayload(statusList, payload, STATUS_LIST_JWT_HEADER) + const header: StatusListJWTHeaderParameters = { + alg: getSigningAlgo(resolution.key.type), + typ: STATUS_LIST_JWT_TYP, + } + const values = createHeaderAndPayload(statusList, payload, header) const signedJwt = await context.agent.jwtCreateJwsCompactSignature({ issuer: { ...identifier, noIssPayloadUpdate: false }, protectedHeader: values.header, @@ -52,3 +61,18 @@ export const decodeStatusListJWT = (jwt: CompactJWT): DecodedStatusListPayload = iat: payload.iat, } } + +export const getSigningAlgo = (type: TKeyType): JoseSignatureAlgorithm => { + switch (type) { + case 'Ed25519': + return JoseSignatureAlgorithm.EdDSA + case 'Secp256k1': + return JoseSignatureAlgorithm.ES256K + case 'Secp256r1': + return JoseSignatureAlgorithm.ES256 + case 'RSA': + return JoseSignatureAlgorithm.RS256 + default: + throw Error('Key type not yet supported') + } +} diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index 84b833576..71e8e95fd 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -13,7 +13,15 @@ import { StatusListCredential, StatusPurpose2021, } from '@sphereon/ssi-types' -import { CredentialPayload, IAgentContext, ICredentialIssuer, ICredentialPlugin, ICredentialVerifier, IPluginMethodMap } from '@veramo/core' +import { + CredentialPayload, + IAgentContext, + ICredentialIssuer, + ICredentialPlugin, + ICredentialVerifier, + IKeyManager, + IPluginMethodMap, +} from '@veramo/core' import { DataSource } from 'typeorm' import { BitsPerStatus } from '@sd-jwt/jwt-status-list/dist' @@ -36,7 +44,7 @@ export type StatusList2021Args = { export type OAuthStatusListArgs = { bitsPerStatus?: BitsPerStatus - expiresAt?: string + expiresAt?: Date } export type BaseCreateNewStatusListArgs = { @@ -57,7 +65,7 @@ export type UpdateStatusList2021Args = { export type UpdateOAuthStatusListArgs = { bitsPerStatus: BitsPerStatus - expiresAt?: string + expiresAt?: Date } export interface UpdateStatusListFromEncodedListArgs { @@ -105,6 +113,7 @@ interface StatusList2021Details { interface OAuthStatusDetails { bitsPerStatus?: BitsPerStatus + expiresAt?: Date } export interface StatusList2021EntryCredentialStatus extends ICredentialStatus { @@ -119,7 +128,7 @@ export interface StatusListOAuthEntryCredentialStatus extends ICredentialStatus bitsPerStatus: number statusListIndex: string statusListCredential: string - expiresAt?: string + expiresAt?: Date } export interface StatusList2021ToVerifiableCredentialArgs { @@ -155,6 +164,12 @@ export interface CheckStatusIndexArgs { statusListIndex: string | number } +export interface ToStatusListDetailsArgs { + statusListPayload: StatusListCredential + correlationId?: string + driverType?: StatusListDriverType +} + /** * The interface definition for a plugin that can add statuslist info to a credential * @@ -229,4 +244,4 @@ export type SignedStatusListData = { } export type IRequiredPlugins = ICredentialPlugin & IIdentifierResolution -export type IRequiredContext = IAgentContext +export type IRequiredContext = IAgentContext diff --git a/packages/vc-status-list/src/utils.ts b/packages/vc-status-list/src/utils.ts index 58e098837..1b6d70157 100644 --- a/packages/vc-status-list/src/utils.ts +++ b/packages/vc-status-list/src/utils.ts @@ -5,6 +5,7 @@ import { StatusListType, StatusListType as StatusListTypeW3C, StatusListCredential, + DocumentFormat, } from '@sphereon/ssi-types' import { jwtDecode } from 'jwt-decode' @@ -79,13 +80,16 @@ export function determineStatusListType(credential: StatusListCredential): Statu } export function determineProofFormat(credential: StatusListCredential): ProofFormat { - if (CredentialMapper.isJwtEncoded(credential)) { - return 'jwt' - } else if (CredentialMapper.isMsoMdocOid4VPEncoded(credential)) { - // Just assume Cbor for now, I'd need to decode at least the header to what type of Cbor we have - return 'cbor' - } else if (CredentialMapper.isCredential(credential)) { - return 'lds' + const type: DocumentFormat = CredentialMapper.detectDocumentType(credential) + switch (type) { + case DocumentFormat.JWT: + return 'jwt' + case DocumentFormat.MSO_MDOC: + // Not really mdoc, just assume Cbor for now, I'd need to decode at least the header to what type of Cbor we have + return 'cbor' + case DocumentFormat.JSONLD: + return 'lds' + default: + throw Error('Cannot determine credential payload type') } - throw Error('Cannot determine credential payload type') } From 0fb7aec794822e87df77ebf650fee22bbb7c6676 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Mon, 27 Jan 2025 10:25:18 +0100 Subject: [PATCH 21/24] chore: Added test coverage to statusListCredentialToDetails and fixed expiresAt handling --- .../__tests__/statuslist.test.ts | 92 ++++++++++++++++++- packages/vc-status-list/src/functions.ts | 36 ++++++-- .../src/impl/OAuthStatusList.ts | 38 +++++--- .../vc-status-list/src/impl/encoding/cbor.ts | 42 +++++---- .../vc-status-list/src/impl/encoding/jwt.ts | 2 + packages/vc-status-list/src/types/index.ts | 3 +- 6 files changed, 176 insertions(+), 37 deletions(-) diff --git a/packages/vc-status-list-tests/__tests__/statuslist.test.ts b/packages/vc-status-list-tests/__tests__/statuslist.test.ts index 5ac0192ef..0ac707d47 100644 --- a/packages/vc-status-list-tests/__tests__/statuslist.test.ts +++ b/packages/vc-status-list-tests/__tests__/statuslist.test.ts @@ -13,6 +13,7 @@ import { createNewStatusList, Status2021, statusList2021ToVerifiableCredential, + statusListCredentialToDetails, StatusOAuth, updateStatusIndexFromStatusListCredential, updateStatusListIndexFromEncodedList, @@ -28,7 +29,7 @@ import { } from '@sphereon/ssi-sdk.vc-handler-ld-local' // @ts-ignore import nock from 'nock' -import { StatusListType } from '@sphereon/ssi-types' +import { StatusListDriverType, StatusListType } from '@sphereon/ssi-types' import { JwtService } from '@sphereon/ssi-sdk-ext.jwt-service' jest.setTimeout(100000) @@ -342,4 +343,93 @@ describe('Status list', () => { ).rejects.toThrow() }) }) + + describe('statusListCredentialToDetails', () => { + it('should handle StatusList2021 JWT credential', async () => { + const initialList = await createNewStatusList( + { + type: StatusListType.StatusList2021, + proofFormat: 'jwt', + id: 'http://localhost:9543/details1', + issuer: didKeyIdentifier.did, + length: 1000, + correlationId: 'test-details-1', + statusList2021: { + indexingDirection: 'rightToLeft', + }, + }, + { agent }, + ) + + const details = await statusListCredentialToDetails({ + statusListCredential: initialList.statusListCredential, + correlationId: 'test-details-1', + driverType: StatusListDriverType.AGENT_TYPEORM, + }) + + expect(details.type).toBe(StatusListType.StatusList2021) + expect(details.proofFormat).toBe('jwt') + expect(details.correlationId).toBe('test-details-1') + expect(details.driverType).toBe(StatusListDriverType.AGENT_TYPEORM) + expect(details.statusList2021?.indexingDirection).toBe('rightToLeft') + }) + + it('should handle OAuthStatusList credential', async () => { + const initialList = await createNewStatusList( + { + type: StatusListType.OAuthStatusList, + proofFormat: 'jwt', + id: 'http://localhost:9543/details2', + issuer: didKeyIdentifier.did, + length: 1000, + correlationId: 'test-details-2', + oauthStatusList: { + bitsPerStatus: 2, + expiresAt: new Date('2025-01-01'), + }, + }, + { agent }, + ) + + const details = await statusListCredentialToDetails({ + statusListCredential: initialList.statusListCredential, + correlationId: 'test-details-2', + }) + + expect(details.type).toBe(StatusListType.OAuthStatusList) + expect(details.proofFormat).toBe('jwt') + expect(details.correlationId).toBe('test-details-2') + expect(details.oauthStatusList?.bitsPerStatus).toBe(2) + expect(details.oauthStatusList?.expiresAt).toEqual(new Date('2025-01-01')) + }) + + it('should handle OAuthStatusList with CBOR format', async () => { + const initialList = await createNewStatusList( + { + type: StatusListType.OAuthStatusList, + proofFormat: 'cbor', + id: 'http://localhost:9543/details3', + issuer: didKeyIdentifier.did, + length: 1000, + correlationId: 'test-details-3', + oauthStatusList: { + bitsPerStatus: 2, + expiresAt: new Date('2025-01-01'), + }, + }, + { agent }, + ) + + const details = await statusListCredentialToDetails({ + statusListCredential: initialList.statusListCredential, + correlationId: 'test-details-3', + }) + + expect(details.type).toBe(StatusListType.OAuthStatusList) + expect(details.proofFormat).toBe('cbor') + expect(details.correlationId).toBe('test-details-3') + expect(details.oauthStatusList?.bitsPerStatus).toBe(2) + expect(details.oauthStatusList?.expiresAt).toEqual(new Date('2025-01-01')) + }) + }) }) diff --git a/packages/vc-status-list/src/functions.ts b/packages/vc-status-list/src/functions.ts index e0833bbe6..97b4c422b 100644 --- a/packages/vc-status-list/src/functions.ts +++ b/packages/vc-status-list/src/functions.ts @@ -1,5 +1,13 @@ import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { CredentialMapper, ProofFormat, StatusListCredential, StatusListDriverType, StatusListType, StatusPurpose2021 } from '@sphereon/ssi-types' +import { + CredentialMapper, + DocumentFormat, + ProofFormat, + StatusListCredential, + StatusListDriverType, + StatusListType, + StatusPurpose2021, +} from '@sphereon/ssi-types' import { CredentialStatus, DIDDocument, IAgentContext, ICredentialPlugin, ProofFormat as VeramoProofFormat } from '@veramo/core' import { checkStatus } from '@sphereon/vc-status-list' @@ -171,12 +179,28 @@ export async function statusListCredentialToDetails(args: { driverType?: StatusListDriverType }): Promise { const credential = getAssertedValue('statusListCredential', args.statusListCredential) - const uniform = CredentialMapper.toUniformCredential(credential) - const type = uniform.type.find((t) => t.includes('StatusList2021') || t.includes('OAuth2StatusList')) - if (!type) { - throw new Error('Invalid status list credential type') + + let statusListType: StatusListType | undefined + const documentFormat = CredentialMapper.detectDocumentType(credential) + if (documentFormat === DocumentFormat.JWT) { + const [header] = credential.split('.') + const decodedHeader = JSON.parse(Buffer.from(header, 'base64').toString()) + + if (decodedHeader.typ === 'statuslist+jwt') { + statusListType = StatusListType.OAuthStatusList + } + } else if (documentFormat === DocumentFormat.MSO_MDOC) { + statusListType = StatusListType.OAuthStatusList + // TODO check CBOR content? + } + if (!statusListType) { + const uniform = CredentialMapper.toUniformCredential(credential) + const type = uniform.type.find((t) => t.includes('StatusList2021') || t.includes('OAuth2StatusList')) + if (!type) { + throw new Error('Invalid status list credential type') + } + statusListType = type.replace('Credential', '') as StatusListType } - const statusListType = type.replace('Credential', '') as StatusListType // "StatusList2021Credential" is a VC schema type and does not map 1:1 to our internal StatusListType enum const implementation = getStatusListImplementation(statusListType) return await implementation.toStatusListDetails({ diff --git a/packages/vc-status-list/src/impl/OAuthStatusList.ts b/packages/vc-status-list/src/impl/OAuthStatusList.ts index 3b7d11eb8..66615aecd 100644 --- a/packages/vc-status-list/src/impl/OAuthStatusList.ts +++ b/packages/vc-status-list/src/impl/OAuthStatusList.ts @@ -31,15 +31,15 @@ export class OAuthStatusListImplementation implements IStatusList { } const proofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT - const { issuer, id, keyRef } = args + const { issuer, id, oauthStatusList, keyRef } = args + const { bitsPerStatus, expiresAt } = oauthStatusList const length = args.length ?? DEFAULT_LIST_LENGTH - const bitsPerStatus = args.oauthStatusList.bitsPerStatus ?? DEFAULT_BITS_PER_STATUS const issuerString = typeof issuer === 'string' ? issuer : issuer.id const correlationId = getAssertedValue('correlationId', args.correlationId) - const statusList = new StatusList(new Array(length).fill(0), bitsPerStatus) + 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, keyRef) + const { statusListCredential } = await this.createSignedStatusList(proofFormat, context, statusList, issuerString, id, expiresAt, keyRef) return { encodedList, @@ -55,7 +55,7 @@ export class OAuthStatusListImplementation implements IStatusList { } async updateStatusListIndex(args: UpdateStatusListIndexArgs, context: IRequiredContext): Promise { - const { statusListCredential, value, keyRef } = args + const { statusListCredential, value, expiresAt, keyRef } = args if (typeof statusListCredential !== 'string') { return Promise.reject('statusListCredential in neither JWT nor CWT') } @@ -76,6 +76,7 @@ export class OAuthStatusListImplementation implements IStatusList { statusList, issuer, id, + expiresAt, keyRef, ) @@ -97,27 +98,36 @@ export class OAuthStatusListImplementation implements IStatusList { if (!args.oauthStatusList) { throw new Error('OAuthStatusList options are required for type OAuthStatusList') } + const { proofFormat, oauthStatusList, keyRef } = args + const { bitsPerStatus, expiresAt } = oauthStatusList - const proofFormat = args.proofFormat ?? DEFAULT_PROOF_FORMAT const { issuer, id } = getAssertedValues(args) - const bitsPerStatus = args.oauthStatusList.bitsPerStatus ?? DEFAULT_BITS_PER_STATUS const issuerString = typeof issuer === 'string' ? issuer : issuer.id - const listToUpdate = StatusList.decompressStatusList(args.encodedList, bitsPerStatus) + const listToUpdate = StatusList.decompressStatusList(args.encodedList, bitsPerStatus ?? DEFAULT_BITS_PER_STATUS) const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex) listToUpdate.setStatus(index, args.value ? 1 : 0) - const { statusListCredential, encodedList } = await this.createSignedStatusList(proofFormat, context, listToUpdate, issuerString, id, args.keyRef) + 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: proofFormat ?? DEFAULT_PROOF_FORMAT, id, issuer, } @@ -144,7 +154,7 @@ export class OAuthStatusListImplementation implements IStatusList { const { statusListPayload } = args as { statusListPayload: CompactJWT | CWT } const proofFormat = determineProofFormat(statusListPayload) const decoded = proofFormat === 'jwt' ? decodeStatusListJWT(statusListPayload) : decodeStatusListCWT(statusListPayload) - const { statusList, issuer, id } = decoded + const { statusList, issuer, id, exp } = decoded return { id, @@ -156,6 +166,7 @@ export class OAuthStatusListImplementation implements IStatusList { statusListCredential: statusListPayload, oauthStatusList: { bitsPerStatus: statusList.getBitsPerStatus(), + ...(exp && { expiresAt: new Date(exp * 1000) }), }, ...(args.correlationId && { correlationId: args.correlationId }), ...(args.driverType && { driverType: args.driverType }), @@ -168,14 +179,15 @@ export class OAuthStatusListImplementation implements IStatusList { statusList: StatusList, issuerString: string, id: string, + expiresAt?: Date, keyRef?: string, ): Promise { switch (proofFormat) { case 'jwt': { - return await createSignedJwt(context, statusList, issuerString, id, keyRef) + return await createSignedJwt(context, statusList, issuerString, id, expiresAt, keyRef) } case 'cbor': { - return await createSignedCbor(context, statusList, issuerString, id, keyRef) + return await createSignedCbor(context, statusList, issuerString, id, expiresAt, keyRef) } default: throw new Error(`Invalid proof format '${proofFormat}' for OAuthStatusList`) diff --git a/packages/vc-status-list/src/impl/encoding/cbor.ts b/packages/vc-status-list/src/impl/encoding/cbor.ts index 4883bf1ab..33358ed4c 100644 --- a/packages/vc-status-list/src/impl/encoding/cbor.ts +++ b/packages/vc-status-list/src/impl/encoding/cbor.ts @@ -24,6 +24,7 @@ export const createSignedCbor = async ( statusList: StatusList, issuerString: string, id: string, + expiresAt?: Date, keyRef?: string, ): Promise => { const identifier = await resolveIdentifier(context, issuerString, keyRef) @@ -47,7 +48,7 @@ export const createSignedCbor = async ( ), ) const protectedHeaderEncoded = cbor.Cbor.encode(protectedHeader) - const claimsMap = buildClaimsMap(id, issuerString, statusListMap) + const claimsMap = buildClaimsMap(id, issuerString, statusListMap, expiresAt) const claimsEncoded: Int8Array = cbor.Cbor.encode(claimsMap) const signedCWT: string = await context.agent.keyManagerSign({ @@ -61,14 +62,12 @@ export const createSignedCbor = async ( const signatureBytes = base64url.decode(signedCWT) const signatureInt8 = new Int8Array(Buffer.from(signatureBytes)) - const cwtArray = new cbor.CborArray( - kotlin.collections.KtMutableList.fromJsArray([ - new cbor.CborByteString(protectedHeaderEncodedInt8), - new cbor.CborByteString(claimsEncodedInt8), - new cbor.CborByteString(signatureInt8), - ]), - ) - + const cwtArrayElements: Array> = [ + new cbor.CborByteString(protectedHeaderEncodedInt8), + new cbor.CborByteString(claimsEncodedInt8), + new cbor.CborByteString(signatureInt8), + ] + const cwtArray = new cbor.CborArray(kotlin.collections.KtMutableList.fromJsArray(cwtArrayElements)) const cwtEncoded = cbor.Cbor.encode(cwtArray) const cwtBuffer = Buffer.from(cwtEncoded) return { @@ -81,8 +80,8 @@ function buildClaimsMap( id: string, issuerString: string, statusListMap: com.sphereon.cbor.CborMap>, + expiresAt?: Date, ) { - const exp = Math.floor(new Date().getTime() / 1000) const ttl = 65535 // FIXME figure out what value should be / come from and what the difference is with exp const claimsEntries: Array<[com.sphereon.cbor.CborUInt, com.sphereon.cbor.CborItem]> = [ [new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.SUBJECT)), new cbor.CborString(id)], // "sub" @@ -93,10 +92,10 @@ function buildClaimsMap( ], ] - if (exp) { + if (expiresAt) { claimsEntries.push([ new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.EXPIRATION)), - new cbor.CborUInt(kmp.LongKMP.fromNumber(exp)), // "exp" + new cbor.CborUInt(kmp.LongKMP.fromNumber(Math.floor(expiresAt.getTime() / 1000))), // "exp" ]) } @@ -113,10 +112,21 @@ function buildClaimsMap( return claimsMap } -const getCborValueFromMap = (map: Map, com.sphereon.cbor.CborItem>, key: number): T | never => { +const getCborValueFromMap = (map: Map, com.sphereon.cbor.CborItem>, key: number): T => { + const value = getCborOptionalValueFromMap(map, key) + if (value === undefined) { + throw new Error(`Required claim ${key} not found`) + } + return value +} + +const getCborOptionalValueFromMap = ( + map: Map, com.sphereon.cbor.CborItem>, + key: number, +): T | undefined | never => { const value = map.get(new com.sphereon.cbor.CborUInt(kmp.LongKMP.fromNumber(key))) if (!value) { - throw new Error(`Required claim ${key} not found`) + return undefined } return value.value as T } @@ -154,8 +164,8 @@ export const decodeStatusListCWT = (cwt: string): DecodedStatusListPayload => { issuer: getCborValueFromMap(claimsMap, CWT_CLAIMS.ISSUER), id: getCborValueFromMap(claimsMap, CWT_CLAIMS.SUBJECT), statusList, - exp: getCborValueFromMap(claimsMap, CWT_CLAIMS.EXPIRATION), - ttl: getCborValueFromMap(claimsMap, CWT_CLAIMS.TIME_TO_LIVE), iat: Number(getCborValueFromMap(claimsMap, CWT_CLAIMS.ISSUED_AT)), + exp: getCborOptionalValueFromMap(claimsMap, CWT_CLAIMS.EXPIRATION), + ttl: getCborOptionalValueFromMap(claimsMap, CWT_CLAIMS.TIME_TO_LIVE), } } diff --git a/packages/vc-status-list/src/impl/encoding/jwt.ts b/packages/vc-status-list/src/impl/encoding/jwt.ts index 27c2aeb63..bf28e516c 100644 --- a/packages/vc-status-list/src/impl/encoding/jwt.ts +++ b/packages/vc-status-list/src/impl/encoding/jwt.ts @@ -14,6 +14,7 @@ export const createSignedJwt = async ( statusList: StatusList, issuerString: string, id: string, + expiresAt?: Date, keyRef?: string, ): Promise => { const identifier = await resolveIdentifier(context, issuerString, keyRef) @@ -23,6 +24,7 @@ export const createSignedJwt = async ( iss: issuerString, sub: id, iat: Math.floor(Date.now() / 1000), + ...(expiresAt && { exp: Math.floor(expiresAt.getTime() / 1000) }), } const header: StatusListJWTHeaderParameters = { diff --git a/packages/vc-status-list/src/types/index.ts b/packages/vc-status-list/src/types/index.ts index 71e8e95fd..e90690d11 100644 --- a/packages/vc-status-list/src/types/index.ts +++ b/packages/vc-status-list/src/types/index.ts @@ -154,9 +154,10 @@ export interface CreateStatusListArgs { export interface UpdateStatusListIndexArgs { statusListCredential: StatusListCredential // | CompactJWT - keyRef?: string statusListIndex: number | string value: number | Status2021 | StatusOAuth + keyRef?: string + expiresAt?: Date } export interface CheckStatusIndexArgs { From 48c95e18301bf7b0430025c1aac7da024987b33f Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Mon, 27 Jan 2025 15:25:03 +0100 Subject: [PATCH 22/24] chore: fixed expiresAt type for sqlite --- .../data-store/src/entities/statusList/StatusListEntities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/data-store/src/entities/statusList/StatusListEntities.ts b/packages/data-store/src/entities/statusList/StatusListEntities.ts index 48e948298..333b124ca 100644 --- a/packages/data-store/src/entities/statusList/StatusListEntities.ts +++ b/packages/data-store/src/entities/statusList/StatusListEntities.ts @@ -110,6 +110,6 @@ export class StatusList2021Entity extends StatusListEntity { export class OAuthStatusListEntity extends StatusListEntity { @Column({ type: 'integer', name: 'bitsPerStatus', nullable: false }) bitsPerStatus!: number - @Column({ type: 'timestamptz', name: 'expiresAt', nullable: true }) + @Column({ type: 'datetime', name: 'expiresAt', nullable: true }) expiresAt?: Date } From be93b0b0e7c3af83debe3babded93a0ea95ec81e Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Mon, 27 Jan 2025 15:46:42 +0100 Subject: [PATCH 23/24] chore: fixed expiresAt in data store test --- .../data-store/src/__tests__/statusList.entities.test.ts | 2 +- packages/data-store/src/__tests__/statusList.store.test.ts | 2 +- .../vc-status-list-issuer/src/agent/StatusListPlugin.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/data-store/src/__tests__/statusList.entities.test.ts b/packages/data-store/src/__tests__/statusList.entities.test.ts index 7296c575c..f5ef9adb4 100644 --- a/packages/data-store/src/__tests__/statusList.entities.test.ts +++ b/packages/data-store/src/__tests__/statusList.entities.test.ts @@ -109,7 +109,7 @@ describe('Status list entities tests', () => { statusList.credentialIdMode = StatusListCredentialIdMode.ISSUANCE statusList.proofFormat = 'jwt' statusList.bitsPerStatus = 1 - statusList.expiresAt = '2025-01-01T00:00:00Z' + statusList.expiresAt = new Date('2025-01-01T00:00:00Z') statusList.issuer = 'did:example:123' const fromDb = await dbConnection.getRepository(OAuthStatusListEntity).save(statusList) diff --git a/packages/data-store/src/__tests__/statusList.store.test.ts b/packages/data-store/src/__tests__/statusList.store.test.ts index b2f88bf2f..86bc05415 100644 --- a/packages/data-store/src/__tests__/statusList.store.test.ts +++ b/packages/data-store/src/__tests__/statusList.store.test.ts @@ -90,7 +90,7 @@ describe('Status list store tests', () => { type: StatusListType.OAuthStatusList, proofFormat: 'jwt', bitsPerStatus: 1, - expiresAt: '2025-01-01T00:00:00Z', + expiresAt: new Date('2025-01-01T00:00:00Z'), issuer: 'did:example:123', } diff --git a/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts b/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts index dd07c2f42..accd7c11d 100644 --- a/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts +++ b/packages/vc-status-list-issuer/src/agent/StatusListPlugin.ts @@ -103,10 +103,10 @@ export class StatusListPlugin implements IAgentPlugin { correlationId: sl.correlationId, }) this.instances.push({ - correlationId: statusListDetails.correlationId, - id: statusListDetails.id, + correlationId: statusListDetails!.correlationId, + id: statusListDetails!.id, dataSource, - driverType: statusListDetails.driverType!, + driverType: statusListDetails!.driverType!, driverOptions: driver.getOptions(), }) } From 65f181252d478bcc3a55de30f78b64887bc96a22 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Thu, 30 Jan 2025 22:29:09 +0100 Subject: [PATCH 24/24] chore: fixed type in unit test --- .../data-store/src/__tests__/statusList.entities.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/data-store/src/__tests__/statusList.entities.test.ts b/packages/data-store/src/__tests__/statusList.entities.test.ts index f5ef9adb4..9004e64c4 100644 --- a/packages/data-store/src/__tests__/statusList.entities.test.ts +++ b/packages/data-store/src/__tests__/statusList.entities.test.ts @@ -3,7 +3,7 @@ import { DataSources } from '@sphereon/ssi-sdk.agent-config' import { DataStoreStatusListEntities, StatusListEntryEntity } from '../index' import { DataStoreStatusListMigrations } from '../migrations' import { OAuthStatusListEntity, StatusList2021Entity } from '../entities/statusList/StatusListEntities' -import { StatusListCredentialIdMode, StatusListDriverType } from '@sphereon/ssi-types' +import { IIssuer, StatusListCredentialIdMode, StatusListDriverType } from '@sphereon/ssi-types' describe('Status list entities tests', () => { let dbConnection: DataSource @@ -96,8 +96,8 @@ describe('Status list entities tests', () => { expect(fromDb).toBeDefined() expect(fromDb.issuer).toEqual(statusList.issuer) expect(typeof fromDb.issuer).toEqual('object') - expect((fromDb.issuer as any).id).toEqual('did:example:123') - expect((fromDb.issuer as any).name).toEqual('Test Issuer') + expect((fromDb.issuer as IIssuer).id).toEqual('did:example:123') + expect((fromDb.issuer as IIssuer).name).toEqual('Test Issuer') }) it('should save OAuth status list to database', async () => {