-
Notifications
You must be signed in to change notification settings - Fork 136
Expand file tree
/
Copy pathcredential-status-list-2021.ts
More file actions
358 lines (308 loc) · 12.3 KB
/
credential-status-list-2021.ts
File metadata and controls
358 lines (308 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
import { createList, StatusList } from "@digitalcredentials/vc-status-list"
import {
CredentialStatus,
CredentialStatusGenerateArgs,
CredentialStatusReference,
CredentialStatusUpdateArgs,
CredentialStatusRequestArgs,
IAgentContext,
IAgentPlugin,
ICheckCredentialStatusArgs,
ICredentialStatus,
IResolver,
IssuerType,
ProofType,
UnsignedCredential,
VerifiableCredential,
} from '@veramo/core'
import { ICredentialIssuer } from '@veramo/credential-w3c'
const method: string = "StatusList2021Entry"
/**
* The length of all status list created by this agent.
*
* TODO: Improve this by allowing any size of status list and storing the size of each list separately.
*/
const statusListLength = 100000
/**
*
* `revocation`: Used to cancel the validity of a verifiable credential. This status is not reversible.
* `suspension`: Used to temporarily prevent the acceptance of a verifiable credential. This status is reversible. *
*/
export type StatusPurpose = 'revocation' | 'suspension'
export interface CredentialStatusList2021Reference extends CredentialStatusReference {
/**
* @inheritdoc {@link CredentialStatusReference.id}
*
* It MUST NOT be the URL for the status list.
*/
id: string
/**
* @inheritdoc {@link CredentialStatusReference.type}
*/
type: 'StatusList2021Entry'
/**
* The purpose of the status entry MUST be a string. While the value of the string is arbitrary, the following values MUST be used for their intended purpose:
*/
statusPurpose: StatusPurpose
/**
* Identify the bit position of the status of the verifiable credential (arbitrary integer >= 0).
*
* ```The statusListIndex property MUST be an arbitrary size integer greater than or equal to 0,
* expressed as a string. The value identifies the bit position of the status of the verifiable credential.```
*/
statusListIndex: string
/**
* URL to the `StatusList2021Credential` credential.
*/
statusListCredential: string
}
/**
* Arguments to generate a credential status referencing a new/existent status list.
*/
export interface StatusList2021GenerateArgs extends CredentialStatusGenerateArgs {
/**
* The `credentialStatus.statusListCredentia` URL, which will prefix the `credentialStatus.id`.
*
* @example If the `credentialStatus`.`id` will be something like `https://example.com/credentials/status/3#94567`,
* then `statusListCredentialUrl` should be `https://example.com/credentials/status/3`
*/
statusListCredentialUrl: string
/**
* The index in the status list which will be assigned to the credential.
* If not informed, an unallocatev index will be used instead.
*/
statusListIndex?: number;
/**
* The purpose of the status entry
*/
statusPurpose: StatusPurpose
}
/**
* Arguments to update a verificable credential status.
*/
export interface StatusList2021UpdateArgs extends CredentialStatusUpdateArgs {
options: {
purpose: StatusPurpose
value: boolean
}
}
/**
* Used to store the credntials status list by an ID.
*
* @beta
*/
export interface StatusListStorage {
/**
* Store the status list (as string) referenced by a key.
*
* @param key the status list id
* @param value the status list encoded
*/
set(key: string, value: string): Promise<void>
/**
* Restore a credential status list (as string) by its key.
*
* @param key The status list id
*/
get(key: string): Promise<string | undefined>
/**
* List all the keys from stored status lists.
*/
keys(): string[]
}
/**
* A Veramo plugin that enables status information for verifiable credentials
* using the [Status List 2021](https://w3c-ccg.github.io/vc-status-list-2021/) method.
*
* This plugin implements the {@link @veramo/core#ICredentialStatusManager | ICredentialStatusManager} interface.
*
* @beta This API may change without a BREAKING CHANGE notice.
*/
export class CredentialStatusList2021Plugin implements IAgentPlugin {
readonly methods: ICredentialStatus
/**
* @param storage A storage provider for storing the lists is expected as parameter.
*/
constructor(private readonly storage: StatusListStorage) {
this.methods = {
checkCredentialStatus: this.checkCredentialStatus.bind(this),
credentialStatusGenerate: this.credentialStatusGenerate.bind(this),
credentialStatusUpdate: this.credentialStatusUpdate.bind(this),
credentialStatusRead: this.credentialStatusRead.bind(this),
credentialStatusTypes: this.credentialStatusTypes.bind(this)
}
}
/**
* @see https://w3c-ccg.github.io/vc-status-list-2021/#generate-algorithm
*
* {@inheritdoc ICredentialStatusManager.credentialStatusGenerate}
*/
async credentialStatusGenerate(args: StatusList2021GenerateArgs): Promise<CredentialStatusList2021Reference> {
if (args.type !== method) throw new Error(`invalid_argument: unrecognized method '${args.type}'. Expected '${method}'.`)
const statusListCredentialUrl = args.statusListCredentialUrl
const allocationListName = `${statusListCredentialUrl}#_____allocation`
// Load or create the staus list and the allocation list
const allocationList = await this.getList(allocationListName)
// Look for an unallocated index in the status list and assign it to this credential
const index: number = this.defineIndex(args.statusListIndex, allocationList)
allocationList.setStatus(index, true); // allocate the index (not revoking!)
this.setList(allocationListName, allocationList); // persist the new allocation state
const statusList = await this.getList(statusListCredentialUrl)
statusList.setStatus(index, false); // Marking is as NOT revoked, initially!
this.setList(statusListCredentialUrl, statusList); // persist the new allocation state
const statusPurpose: StatusPurpose = args.statusPurpose || 'revocation'
return {
id: `${statusListCredentialUrl}#${index}`,
type: "StatusList2021Entry",
statusListIndex: `${index}`,
statusPurpose,
statusListCredential: `${statusListCredentialUrl}`,
}
}
private defineIndex(index: number | undefined, allocationList: any) {
if (index !== undefined && index >= statusListLength)
throw new Error(`illegal_state: index is greater than list size (${statusListLength})`)
if (index !== undefined) return index
// Get the first index unallocated
for (let i = 0; i < statusListLength; i++) {
const allocated: boolean = allocationList.getStatus(i)
if (!allocated) {
return i
}
}
throw new Error("illegal_state: no position available in the status list")
}
/**
*
* Gets a list by name, creating it if it doesn't exist.
*
* @param listName The list to be retrieved
* @returns
*/
private async getList(listName: string, create: boolean = true) {
let encodedList = await this.storage.get(listName)
const exists = encodedList !== undefined
if (!exists && !create) throw new Error(`not_found: no status list found with the name ${listName}`)
const list = exists ?
await StatusList.decode({ encodedList }) // decode an existent list
: await this.createList(listName, statusListLength) // create a new list
return list
}
/**
* Creates and persist a new list of bolleans with a defined length.
*
* @param listName Name used to save (persist) the list
* @param length
* @returns a boolean list with the define length
*/
private async createList(listName: string, length: number) {
const list = await createList({ length: length })
await this.setList(listName, list)
return list
}
private async setList(listName: string, list: any) {
const encodedList = await list.encode()
if (!encodedList)
throw new Error("illegal_state: the list should be encoded")
await this.storage.set(listName, encodedList)
return encodedList
}
/**
* {@inheritdoc ICredentialStatusVerifier.checkCredentialStatus}
*/
async checkCredentialStatus(args: ICheckCredentialStatusArgs, context: IAgentContext<IResolver>): Promise<CredentialStatus> {
const vc = args.credential
const statusReference = <CredentialStatusList2021Reference>vc.credentialStatus
const statusListCredential = statusReference.statusListCredential
const list = await this.getList(statusListCredential, false);
const revoked: boolean = list.getStatus(+statusReference.statusListIndex)
return { verified: !revoked }
}
/**
* {@inheritdoc ICredentialStatusManager.credentialStatusUpdate}
*/
async credentialStatusUpdate(args: StatusList2021UpdateArgs): Promise<any> {
const credentialStatus = args.vc.credentialStatus
if (!credentialStatus || !credentialStatus.id) throw new Error("invalid_argument: `credentialStatus.id` must be defined in the credential")
if (credentialStatus.type !== method) throw new Error(`invalid_argument: unrecognized method '${credentialStatus.type}'. Expected '${method}'.`)
const vc = args.vc
const statusReference = <CredentialStatusList2021Reference>vc.credentialStatus
const statusListIndex = statusReference.statusListIndex
if (statusListIndex === undefined) {
throw new Error(`illegal_state: the credential status implementing StatusList2021 needs a 'statusListIndex' defined.`)
}
const list = await this.getList(statusReference.statusListCredential, false)
await list.setStatus(+statusListIndex, args.options.value)
const encodedList = await list.encode()
if (!encodedList) throw new Error("illegal_state: the list should be encoded")
await this.storage.set(statusReference.statusListCredential, encodedList)
}
/**
* Reads the credential status
*/
async credentialStatusRead(args: CredentialStatusRequestArgs, context: IAgentContext<ICredentialIssuer>): Promise<StatusList2021CredentialSigned> {
const credential = args.credential
const statusReference = <CredentialStatusList2021Reference>credential.credentialStatus
const statusListCredentialUrl = statusReference.statusListCredential
const list = await this.getList(statusListCredentialUrl);
const unsignedStatusListCredential = buildStatusList2021Credential(statusListCredentialUrl, credential.issuer, statusReference.statusPurpose, list.encode())
const signed = <StatusList2021CredentialSigned>await context.agent.createVerifiableCredential({
credential: unsignedStatusListCredential,
proofFormat: 'jwt',
})
return signed
}
/**
* {@inheritdoc ICredentialStatusManager.credentialStatusTypes}
*/
async credentialStatusTypes(): Promise<Array<string>> {
return [method]
}
}
export interface StatusList2021Credential extends UnsignedCredential {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://w3id.org/vc/status-list/2021/v1"
],
id: string,
type: ["VerifiableCredential", "StatusList2021Credential"],
issuanceDate: string,
credentialSubject: {
id: string,
type: 'StatusList2021',
statusPurpose: StatusPurpose,
encodedList: string
}
}
export type StatusList2021CredentialSigned = StatusList2021Credential & { proof: ProofType }
/**
* Build a VC that encapsulates the status list.
*
* @see https://w3c-ccg.github.io/vc-status-list-2021/#statuslist2021credential
*
* @param statusListCredential URL to the VC that contains the status list. Example: "https://example.com/credentials/status/3"
* @param issuer DID issuing the status list. Example: "did:example:12345"
* @param statusPurpose The purpose of the status entry
* @param encodedList The GZIP-compressed [RFC1952], base-64 encoded [RFC4648] bitstring values
* for the associated range of verifiable credential status values.
* The uncompressed bitstring MUST be at least 16KB in size.
* @returns
*/
function buildStatusList2021Credential(statusListCredential: string, issuer: IssuerType, statusPurpose: StatusPurpose, encodedList: string): StatusList2021Credential {
return {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://w3id.org/vc/status-list/2021/v1"
],
id: statusListCredential,
type: ["VerifiableCredential", "StatusList2021Credential"],
issuer,
issuanceDate: new Date().toISOString(),
credentialSubject: {
id: `${statusListCredential}#list`,
type: "StatusList2021",
statusPurpose,
encodedList
},
}
}