@sphereon/ssi-sdk.vc-status-list 0.32.1-next.54 → 0.33.1-feature.jose.vcdm.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ import type { IAgentContext, ICredentialPlugin } from '@veramo/core'
2
+ import type { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution'
3
+ import type {
4
+ CheckStatusIndexArgs,
5
+ CreateStatusListArgs,
6
+ Status2021,
7
+ StatusListResult,
8
+ StatusOAuth,
9
+ ToStatusListDetailsArgs,
10
+ UpdateStatusListFromEncodedListArgs,
11
+ UpdateStatusListIndexArgs,
12
+ } from '../types'
13
+
14
+ export interface IStatusList {
15
+ /**
16
+ * Creates a new status list of the specific type
17
+ */
18
+ createNewStatusList(args: CreateStatusListArgs, context: IAgentContext<ICredentialPlugin & IIdentifierResolution>): Promise<StatusListResult>
19
+
20
+ /**
21
+ * Updates a status at the given index in the status list
22
+ */
23
+ updateStatusListIndex(args: UpdateStatusListIndexArgs, context: IAgentContext<ICredentialPlugin & IIdentifierResolution>): Promise<StatusListResult>
24
+
25
+ /**
26
+ * Updates a status list using a base64 encoded list of statuses
27
+ */
28
+ updateStatusListFromEncodedList(
29
+ args: UpdateStatusListFromEncodedListArgs,
30
+ context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
31
+ ): Promise<StatusListResult>
32
+
33
+ /**
34
+ * Checks the status at a given index in the status list
35
+ */
36
+ checkStatusIndex(args: CheckStatusIndexArgs): Promise<number | Status2021 | StatusOAuth>
37
+
38
+ /**
39
+ * Collects the status list details
40
+ */
41
+ toStatusListDetails(args: ToStatusListDetailsArgs): Promise<StatusListResult>
42
+ }
@@ -0,0 +1,206 @@
1
+ import type { IAgentContext, ICredentialPlugin, IKeyManager } from '@veramo/core'
2
+ import { type CompactJWT, type CWT, type CredentialProofFormat, StatusListType } from '@sphereon/ssi-types'
3
+ import type {
4
+ CheckStatusIndexArgs,
5
+ CreateStatusListArgs,
6
+ SignedStatusListData,
7
+ StatusListResult,
8
+ StatusOAuth,
9
+ ToStatusListDetailsArgs,
10
+ UpdateStatusListFromEncodedListArgs,
11
+ UpdateStatusListIndexArgs,
12
+ } from '../types'
13
+ import { determineProofFormat, getAssertedValue, getAssertedValues } from '../utils'
14
+ import type { IStatusList } from './IStatusList'
15
+ import { StatusList } from '@sd-jwt/jwt-status-list'
16
+ import type { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service'
17
+ import type { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution'
18
+ import { createSignedJwt, decodeStatusListJWT } from './encoding/jwt'
19
+ import { createSignedCbor, decodeStatusListCWT } from './encoding/cbor'
20
+
21
+ type IRequiredContext = IAgentContext<ICredentialPlugin & IJwtService & IIdentifierResolution & IKeyManager>
22
+
23
+ export const DEFAULT_BITS_PER_STATUS = 1 // 1 bit is sufficient for 0x00 - "VALID" 0x01 - "INVALID" saving space in the process
24
+ export const DEFAULT_LIST_LENGTH = 250000
25
+ export const DEFAULT_PROOF_FORMAT = 'jwt' as CredentialProofFormat
26
+
27
+ export class OAuthStatusListImplementation implements IStatusList {
28
+ async createNewStatusList(args: CreateStatusListArgs, context: IRequiredContext): Promise<StatusListResult> {
29
+ if (!args.oauthStatusList) {
30
+ throw new Error('OAuthStatusList options are required for type OAuthStatusList')
31
+ }
32
+
33
+ const proofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT
34
+ const { issuer, id, oauthStatusList, keyRef } = args
35
+ const { bitsPerStatus, expiresAt } = oauthStatusList
36
+ const length = args.length ?? DEFAULT_LIST_LENGTH
37
+ const issuerString = typeof issuer === 'string' ? issuer : issuer.id
38
+ const correlationId = getAssertedValue('correlationId', args.correlationId)
39
+
40
+ const statusList = new StatusList(new Array(length).fill(0), bitsPerStatus ?? DEFAULT_BITS_PER_STATUS)
41
+ const encodedList = statusList.compressStatusList()
42
+ const { statusListCredential } = await this.createSignedStatusList(proofFormat, context, statusList, issuerString, id, expiresAt, keyRef)
43
+
44
+ return {
45
+ encodedList,
46
+ statusListCredential,
47
+ oauthStatusList: { bitsPerStatus },
48
+ length,
49
+ type: StatusListType.OAuthStatusList,
50
+ proofFormat,
51
+ id,
52
+ correlationId,
53
+ issuer,
54
+ statuslistContentType: this.buildContentType(proofFormat),
55
+ }
56
+ }
57
+
58
+ async updateStatusListIndex(args: UpdateStatusListIndexArgs, context: IRequiredContext): Promise<StatusListResult> {
59
+ const { statusListCredential, value, expiresAt, keyRef } = args
60
+ if (typeof statusListCredential !== 'string') {
61
+ return Promise.reject('statusListCredential in neither JWT nor CWT')
62
+ }
63
+
64
+ const proofFormat = determineProofFormat(statusListCredential)
65
+ const decoded = proofFormat === 'jwt' ? decodeStatusListJWT(statusListCredential) : decodeStatusListCWT(statusListCredential)
66
+ const { statusList, issuer, id } = decoded
67
+
68
+ const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex)
69
+ if (index < 0 || index >= statusList.statusList.length) {
70
+ throw new Error('Status list index out of bounds')
71
+ }
72
+
73
+ statusList.setStatus(index, value)
74
+ const { statusListCredential: signedCredential, encodedList } = await this.createSignedStatusList(
75
+ proofFormat,
76
+ context,
77
+ statusList,
78
+ issuer,
79
+ id,
80
+ expiresAt,
81
+ keyRef,
82
+ )
83
+
84
+ return {
85
+ statusListCredential: signedCredential,
86
+ encodedList,
87
+ oauthStatusList: {
88
+ bitsPerStatus: statusList.getBitsPerStatus(),
89
+ },
90
+ length: statusList.statusList.length,
91
+ type: StatusListType.OAuthStatusList,
92
+ proofFormat,
93
+ id,
94
+ issuer,
95
+ statuslistContentType: this.buildContentType(proofFormat),
96
+ }
97
+ }
98
+
99
+ // FIXME: This still assumes only two values (boolean), whilst this list supports 8 bits max
100
+ async updateStatusListFromEncodedList(args: UpdateStatusListFromEncodedListArgs, context: IRequiredContext): Promise<StatusListResult> {
101
+ if (!args.oauthStatusList) {
102
+ throw new Error('OAuthStatusList options are required for type OAuthStatusList')
103
+ }
104
+ const { proofFormat, oauthStatusList, keyRef } = args
105
+ const { bitsPerStatus, expiresAt } = oauthStatusList
106
+
107
+ const { issuer, id } = getAssertedValues(args)
108
+ const issuerString = typeof issuer === 'string' ? issuer : issuer.id
109
+
110
+ const listToUpdate = StatusList.decompressStatusList(args.encodedList, bitsPerStatus ?? DEFAULT_BITS_PER_STATUS)
111
+ const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex)
112
+ // FIXME: See above.
113
+ listToUpdate.setStatus(index, args.value ? 1 : 0)
114
+
115
+ const { statusListCredential, encodedList } = await this.createSignedStatusList(
116
+ proofFormat ?? DEFAULT_PROOF_FORMAT,
117
+ context,
118
+ listToUpdate,
119
+ issuerString,
120
+ id,
121
+ expiresAt,
122
+ keyRef,
123
+ )
124
+
125
+ return {
126
+ encodedList,
127
+ statusListCredential,
128
+ oauthStatusList: {
129
+ bitsPerStatus,
130
+ expiresAt,
131
+ },
132
+ length: listToUpdate.statusList.length,
133
+ type: StatusListType.OAuthStatusList,
134
+ proofFormat: proofFormat ?? DEFAULT_PROOF_FORMAT,
135
+ id,
136
+ issuer,
137
+ statuslistContentType: this.buildContentType(proofFormat),
138
+ }
139
+ }
140
+
141
+ private buildContentType(proofFormat: 'jwt' | 'lds' | 'EthereumEip712Signature2021' | 'cbor' | undefined) {
142
+ return `application/statuslist+${proofFormat === 'cbor' ? 'cwt' : 'jwt'}`
143
+ }
144
+
145
+ async checkStatusIndex(args: CheckStatusIndexArgs): Promise<number | StatusOAuth> {
146
+ const { statusListCredential, statusListIndex } = args
147
+ if (typeof statusListCredential !== 'string') {
148
+ return Promise.reject('statusListCredential in neither JWT nor CWT')
149
+ }
150
+
151
+ const proofFormat = determineProofFormat(statusListCredential)
152
+ const { statusList } = proofFormat === 'jwt' ? decodeStatusListJWT(statusListCredential) : decodeStatusListCWT(statusListCredential)
153
+
154
+ const index = typeof statusListIndex === 'number' ? statusListIndex : parseInt(statusListIndex)
155
+ if (index < 0 || index >= statusList.statusList.length) {
156
+ throw new Error('Status list index out of bounds')
157
+ }
158
+
159
+ return statusList.getStatus(index)
160
+ }
161
+
162
+ async toStatusListDetails(args: ToStatusListDetailsArgs): Promise<StatusListResult> {
163
+ const { statusListPayload } = args as { statusListPayload: CompactJWT | CWT }
164
+ const proofFormat = determineProofFormat(statusListPayload)
165
+ const decoded = proofFormat === 'jwt' ? decodeStatusListJWT(statusListPayload) : decodeStatusListCWT(statusListPayload)
166
+ const { statusList, issuer, id, exp } = decoded
167
+
168
+ return {
169
+ id,
170
+ encodedList: statusList.compressStatusList(),
171
+ issuer,
172
+ type: StatusListType.OAuthStatusList,
173
+ proofFormat,
174
+ length: statusList.statusList.length,
175
+ statusListCredential: statusListPayload,
176
+ statuslistContentType: this.buildContentType(proofFormat),
177
+ oauthStatusList: {
178
+ bitsPerStatus: statusList.getBitsPerStatus(),
179
+ ...(exp && { expiresAt: new Date(exp * 1000) }),
180
+ },
181
+ ...(args.correlationId && { correlationId: args.correlationId }),
182
+ ...(args.driverType && { driverType: args.driverType }),
183
+ }
184
+ }
185
+
186
+ private async createSignedStatusList(
187
+ proofFormat: 'jwt' | 'lds' | 'EthereumEip712Signature2021' | 'cbor',
188
+ context: IAgentContext<ICredentialPlugin & IJwtService & IIdentifierResolution & IKeyManager>,
189
+ statusList: StatusList,
190
+ issuerString: string,
191
+ id: string,
192
+ expiresAt?: Date,
193
+ keyRef?: string,
194
+ ): Promise<SignedStatusListData> {
195
+ switch (proofFormat) {
196
+ case 'jwt': {
197
+ return await createSignedJwt(context, statusList, issuerString, id, expiresAt, keyRef)
198
+ }
199
+ case 'cbor': {
200
+ return await createSignedCbor(context, statusList, issuerString, id, expiresAt, keyRef)
201
+ }
202
+ default:
203
+ throw new Error(`Invalid proof format '${proofFormat}' for OAuthStatusList`)
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,249 @@
1
+ import type { IAgentContext, ICredentialPlugin, ProofFormat as VeramoProofFormat } from '@veramo/core'
2
+ import type { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution'
3
+ import {
4
+ CredentialMapper,
5
+ DocumentFormat,
6
+ type IIssuer,
7
+ type CredentialProofFormat,
8
+ type StatusListCredential,
9
+ StatusListType,
10
+ } from '@sphereon/ssi-types'
11
+
12
+ import { StatusList } from '@sphereon/vc-status-list'
13
+ import type { IStatusList } from './IStatusList'
14
+ import type {
15
+ CheckStatusIndexArgs,
16
+ CreateStatusListArgs,
17
+ StatusListResult,
18
+ ToStatusListDetailsArgs,
19
+ UpdateStatusListFromEncodedListArgs,
20
+ UpdateStatusListIndexArgs,
21
+ } from '../types'
22
+
23
+ import { Status2021 } from '../types'
24
+ import { assertValidProofType, getAssertedProperty, getAssertedValue, getAssertedValues } from '../utils'
25
+
26
+ export const DEFAULT_LIST_LENGTH = 250000
27
+ export const DEFAULT_PROOF_FORMAT = 'lds' as VeramoProofFormat
28
+
29
+ export class StatusList2021Implementation implements IStatusList {
30
+ async createNewStatusList(
31
+ args: CreateStatusListArgs,
32
+ context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
33
+ ): Promise<StatusListResult> {
34
+ const length = args?.length ?? DEFAULT_LIST_LENGTH
35
+ const proofFormat: CredentialProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT
36
+ assertValidProofType(StatusListType.StatusList2021, proofFormat)
37
+ const veramoProofFormat: VeramoProofFormat = proofFormat as VeramoProofFormat
38
+
39
+ const { issuer, id } = args
40
+ const correlationId = getAssertedValue('correlationId', args.correlationId)
41
+
42
+ const list = new StatusList({ length })
43
+ const encodedList = await list.encode()
44
+ const statusPurpose = 'revocation'
45
+
46
+ const statusListCredential = await this.createVerifiableCredential(
47
+ {
48
+ ...args,
49
+ encodedList,
50
+ proofFormat: veramoProofFormat,
51
+ },
52
+ context,
53
+ )
54
+
55
+ return {
56
+ encodedList,
57
+ statusListCredential: statusListCredential,
58
+ statusList2021: {
59
+ statusPurpose,
60
+ indexingDirection: 'rightToLeft',
61
+ },
62
+ length,
63
+ type: StatusListType.StatusList2021,
64
+ proofFormat,
65
+ id,
66
+ correlationId,
67
+ issuer,
68
+ statuslistContentType: this.buildContentType(proofFormat),
69
+ }
70
+ }
71
+
72
+ async updateStatusListIndex(
73
+ args: UpdateStatusListIndexArgs,
74
+ context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
75
+ ): Promise<StatusListResult> {
76
+ const credential = args.statusListCredential
77
+ const uniform = CredentialMapper.toUniformCredential(credential)
78
+ const { issuer, credentialSubject } = uniform
79
+ const id = getAssertedValue('id', uniform.id)
80
+ const origEncodedList = getAssertedProperty('encodedList', credentialSubject)
81
+
82
+ const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex)
83
+ const statusList = await StatusList.decode({ encodedList: origEncodedList })
84
+ statusList.setStatus(index, args.value != 0)
85
+ const encodedList = await statusList.encode()
86
+
87
+ const proofFormat = CredentialMapper.detectDocumentType(credential) === DocumentFormat.JWT ? 'jwt' : 'lds'
88
+ const updatedCredential = await this.createVerifiableCredential(
89
+ {
90
+ ...args,
91
+ id,
92
+ issuer,
93
+ encodedList,
94
+ proofFormat: proofFormat,
95
+ },
96
+ context,
97
+ )
98
+
99
+ return {
100
+ statusListCredential: updatedCredential,
101
+ encodedList,
102
+ statusList2021: {
103
+ ...('statusPurpose' in credentialSubject ? { statusPurpose: credentialSubject.statusPurpose } : {}),
104
+ indexingDirection: 'rightToLeft',
105
+ },
106
+ length: statusList.length - 1,
107
+ type: StatusListType.StatusList2021,
108
+ proofFormat: proofFormat,
109
+ id,
110
+ issuer,
111
+ statuslistContentType: this.buildContentType(proofFormat),
112
+ }
113
+ }
114
+
115
+ async updateStatusListFromEncodedList(
116
+ args: UpdateStatusListFromEncodedListArgs,
117
+ context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
118
+ ): Promise<StatusListResult> {
119
+ if (!args.statusList2021) {
120
+ throw new Error('statusList2021 options required for type StatusList2021')
121
+ }
122
+ const proofFormat: CredentialProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT
123
+ assertValidProofType(StatusListType.StatusList2021, proofFormat)
124
+ const veramoProofFormat: VeramoProofFormat = proofFormat as VeramoProofFormat
125
+
126
+ const { issuer, id } = getAssertedValues(args)
127
+ const statusList = await StatusList.decode({ encodedList: args.encodedList })
128
+ const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex)
129
+ statusList.setStatus(index, args.value)
130
+
131
+ const newEncodedList = await statusList.encode()
132
+ const credential = await this.createVerifiableCredential(
133
+ {
134
+ id,
135
+ issuer,
136
+ encodedList: newEncodedList,
137
+ proofFormat: veramoProofFormat,
138
+ keyRef: args.keyRef,
139
+ },
140
+ context,
141
+ )
142
+
143
+ return {
144
+ type: StatusListType.StatusList2021,
145
+ statusListCredential: credential,
146
+ encodedList: newEncodedList,
147
+ statusList2021: {
148
+ statusPurpose: args.statusList2021.statusPurpose,
149
+ indexingDirection: 'rightToLeft',
150
+ },
151
+ length: statusList.length,
152
+ proofFormat: args.proofFormat ?? 'lds',
153
+ id: id,
154
+ issuer: issuer,
155
+ statuslistContentType: this.buildContentType(proofFormat),
156
+ }
157
+ }
158
+
159
+ async checkStatusIndex(args: CheckStatusIndexArgs): Promise<number | Status2021> {
160
+ const uniform = CredentialMapper.toUniformCredential(args.statusListCredential)
161
+ const { credentialSubject } = uniform
162
+ const encodedList = getAssertedProperty('encodedList', credentialSubject)
163
+
164
+ const statusList = await StatusList.decode({ encodedList })
165
+ const status = statusList.getStatus(typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex))
166
+ return status ? Status2021.Invalid : Status2021.Valid
167
+ }
168
+
169
+ async toStatusListDetails(args: ToStatusListDetailsArgs): Promise<StatusListResult> {
170
+ const { statusListPayload } = args
171
+ const uniform = CredentialMapper.toUniformCredential(statusListPayload)
172
+ const { issuer, credentialSubject } = uniform
173
+ const id = getAssertedValue('id', uniform.id)
174
+ const encodedList = getAssertedProperty('encodedList', credentialSubject)
175
+ const proofFormat: CredentialProofFormat = CredentialMapper.detectDocumentType(statusListPayload) === DocumentFormat.JWT ? 'jwt' : 'lds'
176
+
177
+ const statusPurpose = getAssertedProperty('statusPurpose', credentialSubject)
178
+ const list = await StatusList.decode({ encodedList })
179
+
180
+ return {
181
+ id,
182
+ encodedList,
183
+ issuer,
184
+ type: StatusListType.StatusList2021,
185
+ proofFormat,
186
+ length: list.length,
187
+ statusListCredential: statusListPayload,
188
+ statuslistContentType: this.buildContentType(proofFormat),
189
+ statusList2021: {
190
+ indexingDirection: 'rightToLeft',
191
+ statusPurpose,
192
+ },
193
+ ...(args.correlationId && { correlationId: args.correlationId }),
194
+ ...(args.driverType && { driverType: args.driverType }),
195
+ }
196
+ }
197
+
198
+ private async createVerifiableCredential(
199
+ args: {
200
+ id: string
201
+ issuer: string | IIssuer
202
+ encodedList: string
203
+ proofFormat: VeramoProofFormat
204
+ keyRef?: string
205
+ },
206
+ context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
207
+ ): Promise<StatusListCredential> {
208
+ const identifier = await context.agent.identifierManagedGet({
209
+ identifier: typeof args.issuer === 'string' ? args.issuer : args.issuer.id,
210
+ vmRelationship: 'assertionMethod',
211
+ offlineWhenNoDIDRegistered: true,
212
+ })
213
+
214
+ const credential = {
215
+ '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'],
216
+ id: args.id,
217
+ issuer: args.issuer,
218
+ type: ['VerifiableCredential', 'StatusList2021Credential'],
219
+ credentialSubject: {
220
+ id: args.id,
221
+ type: 'StatusList2021',
222
+ statusPurpose: 'revocation',
223
+ encodedList: args.encodedList,
224
+ },
225
+ }
226
+
227
+ const verifiableCredential = await context.agent.createVerifiableCredential({
228
+ credential,
229
+ keyRef: args.keyRef ?? identifier.kmsKeyRef,
230
+ proofFormat: args.proofFormat,
231
+ fetchRemoteContexts: true,
232
+ })
233
+
234
+ return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as StatusListCredential).original as StatusListCredential
235
+ }
236
+
237
+ private buildContentType(proofFormat: 'jwt' | 'lds' | 'EthereumEip712Signature2021' | 'cbor' | undefined) {
238
+ switch (proofFormat) {
239
+ case 'jwt':
240
+ return `application/statuslist+jwt`
241
+ case 'cbor':
242
+ return `application/statuslist+cwt`
243
+ case 'lds':
244
+ return 'application/statuslist+ld+json'
245
+ default:
246
+ throw Error(`Unsupported content type '${proofFormat}' for status lists`)
247
+ }
248
+ }
249
+ }
@@ -0,0 +1,34 @@
1
+ import type { IStatusList } from './IStatusList'
2
+ import { StatusList2021Implementation } from './StatusList2021'
3
+ import { OAuthStatusListImplementation } from './OAuthStatusList'
4
+ import { StatusListType } from '@sphereon/ssi-types'
5
+
6
+ export class StatusListFactory {
7
+ private static instance: StatusListFactory
8
+ private implementations: Map<StatusListType, IStatusList>
9
+
10
+ private constructor() {
11
+ this.implementations = new Map()
12
+ this.implementations.set(StatusListType.StatusList2021, new StatusList2021Implementation())
13
+ this.implementations.set(StatusListType.OAuthStatusList, new OAuthStatusListImplementation())
14
+ }
15
+
16
+ public static getInstance(): StatusListFactory {
17
+ if (!StatusListFactory.instance) {
18
+ StatusListFactory.instance = new StatusListFactory()
19
+ }
20
+ return StatusListFactory.instance
21
+ }
22
+
23
+ public getByType(type: StatusListType): IStatusList {
24
+ const statusList = this.implementations.get(type)
25
+ if (!statusList) {
26
+ throw new Error(`No implementation found for status list type: ${type}`)
27
+ }
28
+ return statusList
29
+ }
30
+ }
31
+
32
+ export function getStatusListImplementation(type: StatusListType): IStatusList {
33
+ return StatusListFactory.getInstance().getByType(type)
34
+ }