@digitalbazaar/vc 6.2.0 → 7.0.0
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.
- package/README.md +149 -10
- package/lib/contexts/index.js +6 -8
- package/lib/helpers.js +103 -0
- package/lib/index.js +223 -75
- package/package.json +24 -20
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ the following:
|
|
|
48
48
|
|
|
49
49
|
## Install
|
|
50
50
|
|
|
51
|
-
- Browsers and Node.js
|
|
51
|
+
- Browsers and Node.js 18+ are supported.
|
|
52
52
|
|
|
53
53
|
To install from NPM:
|
|
54
54
|
|
|
@@ -122,15 +122,18 @@ Pre-requisites:
|
|
|
122
122
|
|
|
123
123
|
* You have a private key (with id and controller) and corresponding suite
|
|
124
124
|
* You have are using a cryptosuite that supports selective disclosure, such
|
|
125
|
-
as `ecdsa-sd-2023`
|
|
125
|
+
as `ecdsa-sd-2023` or `bbs-2023`
|
|
126
126
|
* If you're using a custom `@context`, make sure it's resolvable
|
|
127
127
|
* (Recommended) You have a strategy for where to publish your Controller
|
|
128
128
|
Document and Public Key
|
|
129
129
|
|
|
130
|
+
Issuing using `ecdsa-sd-2023`:
|
|
131
|
+
|
|
130
132
|
```js
|
|
131
|
-
import * as
|
|
133
|
+
import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';
|
|
132
134
|
import * as ecdsaSd2023Cryptosuite from
|
|
133
135
|
'@digitalbazaar/ecdsa-sd-2023-cryptosuite';
|
|
136
|
+
import * as vc from '@digitalbazaar/vc';
|
|
134
137
|
import {DataIntegrityProof} from '@digitalbazaar/data-integrity';
|
|
135
138
|
|
|
136
139
|
const ecdsaKeyPair = await EcdsaMultikey.generate({
|
|
@@ -139,6 +142,18 @@ const ecdsaKeyPair = await EcdsaMultikey.generate({
|
|
|
139
142
|
controller: 'https://example.edu/issuers/565049'
|
|
140
143
|
});
|
|
141
144
|
|
|
145
|
+
// sample exported key pair
|
|
146
|
+
/*
|
|
147
|
+
{
|
|
148
|
+
"@context": "https://w3id.org/security/multikey/v1",
|
|
149
|
+
"id": "https://example.edu/issuers/keys/2",
|
|
150
|
+
"type": "Multikey",
|
|
151
|
+
"controller": "https://example.edu/issuers/565049",
|
|
152
|
+
"publicKeyMultibase": "zDnaeWJjGpXnQAbEpRur3kSWFapGZbwGnFCkzyhiq7nDeXXrM",
|
|
153
|
+
"secretKeyMultibase": "z42trzSpncjWFaB9cKE2Gg5hxtbuAQa5mVJgGwjrugHMacdM"
|
|
154
|
+
}
|
|
155
|
+
*/
|
|
156
|
+
|
|
142
157
|
// sample unsigned credential
|
|
143
158
|
const credential = {
|
|
144
159
|
"@context": [
|
|
@@ -176,6 +191,68 @@ const signedVC = await vc.issue({credential, suite, documentLoader});
|
|
|
176
191
|
console.log(JSON.stringify(signedVC, null, 2));
|
|
177
192
|
```
|
|
178
193
|
|
|
194
|
+
Issuing using `bbs-2023`:
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
import * as bbs2023Cryptosuite from '@digitalbazaar/bbs-2023-cryptosuite';
|
|
198
|
+
import * as bls12381Multikey from '@digitalbazaar/bls12-381-multikey';
|
|
199
|
+
import * as vc from '@digitalbazaar/vc';
|
|
200
|
+
import {DataIntegrityProof} from '@digitalbazaar/data-integrity';
|
|
201
|
+
|
|
202
|
+
const bbsKeyPair = await bls12381Multikey.generate({
|
|
203
|
+
algorithm: 'BBS-BLS12-381-SHA-256';
|
|
204
|
+
id: 'https://example.edu/issuers/keys/3',
|
|
205
|
+
controller: 'https://example.edu/issuers/565049'
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// sample exported key pair
|
|
209
|
+
/*
|
|
210
|
+
{
|
|
211
|
+
"@context": "https://w3id.org/security/multikey/v1",
|
|
212
|
+
"id": "https://example.edu/issuers/keys/3",
|
|
213
|
+
"type": "Multikey",
|
|
214
|
+
"controller": "https://example.edu/issuers/565049",
|
|
215
|
+
"publicKeyMultibase": "zUC72jQrt2BfyE57AVgHgThKCsH6HNo85X9SLNpAJaHb42cNDXhsRWL2KkrFtaiztPbbZjfDVQnQQMw2nMqAPUHnaQ3xEr7kUmcnBgv7S2wQSbRbr7mqsP153nU7yMh3ZN4ZryL",
|
|
216
|
+
"secretKeyMultibase": "z488y1niFCWnaV2i86q1raaa7qwBWZ6WTLeS1W1PrsbcsoNg"
|
|
217
|
+
}
|
|
218
|
+
*/
|
|
219
|
+
|
|
220
|
+
// sample unsigned credential
|
|
221
|
+
const credential = {
|
|
222
|
+
"@context": [
|
|
223
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
224
|
+
"https://www.w3.org/2018/credentials/examples/v1"
|
|
225
|
+
],
|
|
226
|
+
// omit `id` to enable unlinkable disclosure
|
|
227
|
+
"type": ["VerifiableCredential", "AlumniCredential"],
|
|
228
|
+
"issuer": "https://example.edu/issuers/565049",
|
|
229
|
+
// use less precise date that is shared by a sufficiently large group
|
|
230
|
+
// of VCs to enable unlinkable disclosure
|
|
231
|
+
"issuanceDate": "2010-01-01T01:00:00Z",
|
|
232
|
+
"credentialSubject": {
|
|
233
|
+
// omit `id` to enable unlinkable disclosure
|
|
234
|
+
"alumniOf": "Example University"
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// setup bbs-2023 suite for signing unlinkable selective disclosure VCs
|
|
239
|
+
const suite = new DataIntegrityProof({
|
|
240
|
+
signer: bbsKeyPair.signer(),
|
|
241
|
+
cryptosuite: createSignCryptosuite({
|
|
242
|
+
// require the `issuer` and `issuanceDate` fields to always be disclosed
|
|
243
|
+
// by the holder (presenter)
|
|
244
|
+
mandatoryPointers: [
|
|
245
|
+
'/issuanceDate',
|
|
246
|
+
'/issuer'
|
|
247
|
+
]
|
|
248
|
+
})
|
|
249
|
+
});
|
|
250
|
+
// note: do not include a proof ID to enable unlinkable selective disclosure
|
|
251
|
+
|
|
252
|
+
const signedVC = await vc.issue({credential, suite, documentLoader});
|
|
253
|
+
console.log(JSON.stringify(signedVC, null, 2));
|
|
254
|
+
```
|
|
255
|
+
|
|
179
256
|
### Deriving a Selective Disclosure Verifiable Credential
|
|
180
257
|
|
|
181
258
|
Note: This step is performed as a holder of a verifiable credential, not as
|
|
@@ -184,13 +261,16 @@ an issuer.
|
|
|
184
261
|
Pre-requisites:
|
|
185
262
|
|
|
186
263
|
* You have a verifiable credential that was issued using a cryptosuite that
|
|
187
|
-
supports selective disclosure, such as `ecdsa-sd-2023`
|
|
264
|
+
supports selective disclosure, such as `ecdsa-sd-2023` or `bbs-2023`
|
|
188
265
|
* If you're using a custom `@context`, make sure it's resolvable
|
|
189
266
|
|
|
267
|
+
Deriving using `ecdsa-sd-2023`:
|
|
268
|
+
|
|
190
269
|
```js
|
|
191
|
-
import * as
|
|
270
|
+
import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';
|
|
192
271
|
import * as ecdsaSd2023Cryptosuite from
|
|
193
272
|
'@digitalbazaar/ecdsa-sd-2023-cryptosuite';
|
|
273
|
+
import * as vc from '@digitalbazaar/vc';
|
|
194
274
|
import {DataIntegrityProof} from '@digitalbazaar/data-integrity';
|
|
195
275
|
|
|
196
276
|
const {
|
|
@@ -199,8 +279,8 @@ const {
|
|
|
199
279
|
createVerifyCryptosuite
|
|
200
280
|
} = ecdsaSd2023Cryptosuite;
|
|
201
281
|
|
|
202
|
-
// sample
|
|
203
|
-
const
|
|
282
|
+
// sample VC
|
|
283
|
+
const verifiableCredential = {
|
|
204
284
|
"@context": [
|
|
205
285
|
"https://www.w3.org/2018/credentials/v1",
|
|
206
286
|
"https://www.w3.org/2018/credentials/examples/v1",
|
|
@@ -218,13 +298,13 @@ const credential = {
|
|
|
218
298
|
"alumniOf": "<span lang=\"en\">Example University</span>"
|
|
219
299
|
},
|
|
220
300
|
"proof": {
|
|
221
|
-
"id": "urn:uuid:
|
|
301
|
+
"id": "urn:uuid:318d9dce-bc7b-40b9-a956-c9160bf910db",
|
|
222
302
|
"type": "DataIntegrityProof",
|
|
223
|
-
"created": "
|
|
303
|
+
"created": "2024-01-12T21:53:11Z",
|
|
224
304
|
"verificationMethod": "https://example.edu/issuers/keys/2",
|
|
225
305
|
"cryptosuite": "ecdsa-sd-2023",
|
|
226
306
|
"proofPurpose": "assertionMethod",
|
|
227
|
-
"proofValue": "
|
|
307
|
+
"proofValue": "u2V0AhVhAsl6PQKYE15R0O5Qd267ntwHGNH6JRvZ1y8A-fTCQLUoupP8SCZzzmyc0a1AnabHEVKhpHtYV8j9Kapp-fHFBtFgjgCQCIMn2L1R7D5VPnNn_2foxdj8qvsuUTGFqA34YBkguzCpYILfJ-qNQpn6_dJGpkG24FynqbHpnzoHWVJc2kiLqEKHRglhAUmZtstR9MOLrZjcR8J303MXFvRiE6J3bbaPT1_I9-6578-Wj-eydv2TEGBq_dmsjxsOh4_2Va0etw8CXXMAzaVhA9fr7_Sl9D67AfvLhkJTZ0uJCAXcbL2MaS-DmoC7K-ABxroL1_wj119J8yTMlazxzYBwYkihrdp4ZWJZxraX9tIJtL2lzc3VhbmNlRGF0ZWcvaXNzdWVy"
|
|
228
308
|
}
|
|
229
309
|
};
|
|
230
310
|
|
|
@@ -250,6 +330,65 @@ const derivedVC = await vc.derive({
|
|
|
250
330
|
console.log(JSON.stringify(derivedVC, null, 2));
|
|
251
331
|
```
|
|
252
332
|
|
|
333
|
+
Deriving using `bbs-2023`:
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
import * as bbs2023Cryptosuite from '@digitalbazaar/bbs-2023-cryptosuite';
|
|
337
|
+
import * as bls12381Multikey from '@digitalbazaar/bls12-381-multikey';
|
|
338
|
+
import * as vc from '@digitalbazaar/vc';
|
|
339
|
+
import {DataIntegrityProof} from '@digitalbazaar/data-integrity';
|
|
340
|
+
|
|
341
|
+
const {
|
|
342
|
+
createDiscloseCryptosuite,
|
|
343
|
+
createSignCryptosuite,
|
|
344
|
+
createVerifyCryptosuite
|
|
345
|
+
} = bbs2023Cryptosuite;
|
|
346
|
+
|
|
347
|
+
// sample VC
|
|
348
|
+
const verifiableCredential = {
|
|
349
|
+
"@context": [
|
|
350
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
351
|
+
"https://www.w3.org/2018/credentials/examples/v1",
|
|
352
|
+
"https://w3id.org/security/data-integrity/v2"
|
|
353
|
+
],
|
|
354
|
+
"type": [
|
|
355
|
+
"VerifiableCredential",
|
|
356
|
+
"AlumniCredential"
|
|
357
|
+
],
|
|
358
|
+
"issuer": "https://example.edu/issuers/565049",
|
|
359
|
+
"issuanceDate": "2010-01-01T01:00:00Z",
|
|
360
|
+
"credentialSubject": {
|
|
361
|
+
"alumniOf": "<span lang=\"en\">Example University</span>"
|
|
362
|
+
},
|
|
363
|
+
"proof": {
|
|
364
|
+
"type": "DataIntegrityProof",
|
|
365
|
+
"verificationMethod": "https://example.edu/issuers/keys/3",
|
|
366
|
+
"cryptosuite": "bbs-2023",
|
|
367
|
+
"proofPurpose": "assertionMethod",
|
|
368
|
+
"proofValue": "u2V0ChVhQp1smqO-Qmc-1KpNkShjevTeylTdVlpH_RNXeJ_cNniErWPbEWILvsoH5mYjnun5ibZHq0m7BEIaLv8sfMtLfcmgPj6tbAFwDWvEcbRWg7CFYQGWqCAnvTpL_Aao3aVCg5svdzFuvKqnvneA0UwaN0lagvGpWT7fCDGgcYPyNPKaCX94Xo06aTcSwOXgyGUbtN1xYYIU6t5wv20lVdESfzkYOFXTxIZa1HSBAZYWDyEgQ3A3ajzWX5qeFc3cwmnnrGUfJYwawgGLQAY3vBi3LTM2i3jCOPvxCEJALPIjK4tEmWb6uFjT4PWLlIEeTtYj_0yEv91ggsm9vw1PPlK6q8wQiw2i2joZ-OKkvHz7rDSxPYfmQNrqCbS9pc3N1YW5jZURhdGVnL2lzc3Vlcg"
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// note no `signer` needed; the selective disclosure credential will be
|
|
373
|
+
// derived from the base proof already provided by the issuer
|
|
374
|
+
const suite = new DataIntegrityProof({
|
|
375
|
+
cryptosuite: createDiscloseCryptosuite({
|
|
376
|
+
// selectively disclose the entire credential subject; different JSON
|
|
377
|
+
// pointers could be provided to selectively disclose different information;
|
|
378
|
+
// the issuer will have mandatory fields that will be automatically
|
|
379
|
+
// disclosed such as the `issuer` and `issuanceDate` fields
|
|
380
|
+
selectivePointers: [
|
|
381
|
+
'/credentialSubject'
|
|
382
|
+
]
|
|
383
|
+
})
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const derivedVC = await vc.derive({
|
|
387
|
+
verifiableCredential, suite, documentLoader
|
|
388
|
+
});
|
|
389
|
+
console.log(JSON.stringify(derivedVC, null, 2));
|
|
390
|
+
```
|
|
391
|
+
|
|
253
392
|
### Creating a Verifiable Presentation
|
|
254
393
|
|
|
255
394
|
Pre-requisites:
|
package/lib/contexts/index.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2019-
|
|
2
|
+
* Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import {
|
|
5
|
-
contexts as
|
|
6
|
-
} from 'credentials-context';
|
|
5
|
+
contexts as credentialsContexts
|
|
6
|
+
} from '@digitalbazaar/credentials-context';
|
|
7
7
|
|
|
8
|
-
export const contexts = new Map(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
contexts.set(url, context);
|
|
12
|
-
}
|
|
8
|
+
export const contexts = new Map([
|
|
9
|
+
...credentialsContexts
|
|
10
|
+
]);
|
package/lib/helpers.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2023 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import {named as vcNamedContexts} from '@digitalbazaar/credentials-context';
|
|
5
|
+
|
|
6
|
+
// Z and T must be uppercase
|
|
7
|
+
// xml schema date time RegExp
|
|
8
|
+
// @see https://www.w3.org/TR/xmlschema11-2/#dateTime
|
|
9
|
+
export const dateRegex = new RegExp(
|
|
10
|
+
'-?([1-9][0-9]{3,}|0[0-9]{3})' +
|
|
11
|
+
'-(0[1-9]|1[0-2])' +
|
|
12
|
+
'-(0[1-9]|[12][0-9]|3[01])' +
|
|
13
|
+
'T(([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?|(24:00:00(\.0+)?))' +
|
|
14
|
+
'(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?');
|
|
15
|
+
|
|
16
|
+
const CREDENTIALS_CONTEXT_V1_URL = vcNamedContexts.get('v1').id;
|
|
17
|
+
const CREDENTIALS_CONTEXT_V2_URL = vcNamedContexts.get('v2').id;
|
|
18
|
+
|
|
19
|
+
// mappings between credentials contexts and version numbers
|
|
20
|
+
const credentialsContextUrlToVersion = new Map([
|
|
21
|
+
[CREDENTIALS_CONTEXT_V1_URL, 1.0],
|
|
22
|
+
[CREDENTIALS_CONTEXT_V2_URL, 2.0]
|
|
23
|
+
]);
|
|
24
|
+
const credentialsVersionToContextUrl = new Map([
|
|
25
|
+
[1.0, CREDENTIALS_CONTEXT_V1_URL],
|
|
26
|
+
[2.0, CREDENTIALS_CONTEXT_V2_URL]
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Asserts that a context array's first item is a credentials context.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} options - Options.
|
|
33
|
+
* @param {Array} options.context - An array of contexts.
|
|
34
|
+
*
|
|
35
|
+
* @throws {Error} - Throws if the first context
|
|
36
|
+
* is not a credentials context.
|
|
37
|
+
*
|
|
38
|
+
* @returns {undefined}
|
|
39
|
+
*/
|
|
40
|
+
export function assertCredentialContext({context}) {
|
|
41
|
+
// ensure first context is credentials context url
|
|
42
|
+
if(!credentialsContextUrlToVersion.has(context[0])) {
|
|
43
|
+
// throw if the first context is not a credentials context
|
|
44
|
+
throw new Error(
|
|
45
|
+
`"${CREDENTIALS_CONTEXT_V1_URL}" or "${CREDENTIALS_CONTEXT_V2_URL}"` +
|
|
46
|
+
' needs to be first in the list of contexts.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Throws if a Date is not in the correct format.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} options - Options.
|
|
54
|
+
* @param {object} options.credential - A VC.
|
|
55
|
+
* @param {string} options.prop - A prop in the object.
|
|
56
|
+
*
|
|
57
|
+
* @throws {Error} Throws if the date is not a proper date string.
|
|
58
|
+
* @returns {undefined}
|
|
59
|
+
*/
|
|
60
|
+
export function assertDateString({credential, prop}) {
|
|
61
|
+
const value = credential[prop];
|
|
62
|
+
if(!dateRegex.test(value)) {
|
|
63
|
+
throw new Error(`"${prop}" must be a valid date: ${value}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Turns the first context in a VC into a numbered version.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} options - Options.
|
|
71
|
+
* @param {object} options.credential - A VC.
|
|
72
|
+
*
|
|
73
|
+
* @returns {number} A number representing the version.
|
|
74
|
+
*/
|
|
75
|
+
function getContextVersion({credential} = {}) {
|
|
76
|
+
const firstContext = credential?.['@context']?.[0];
|
|
77
|
+
return credentialsContextUrlToVersion.get(firstContext);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Turns the first context in a VC into a numbered version.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} options - Options.
|
|
84
|
+
* @param {number} options.version - A credentials context version.
|
|
85
|
+
*
|
|
86
|
+
* @returns {number} A number representing the version.
|
|
87
|
+
*/
|
|
88
|
+
export function getContextForVersion({version}) {
|
|
89
|
+
return credentialsVersionToContextUrl.get(version);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Checks if a VC is using a specific context version.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} options - Options.
|
|
96
|
+
* @param {object} options.credential - A VC.
|
|
97
|
+
* @param {number} options.version - A VC Context version
|
|
98
|
+
*
|
|
99
|
+
* @returns {boolean} If the first context matches the version.
|
|
100
|
+
*/
|
|
101
|
+
export function checkContextVersion({credential, version}) {
|
|
102
|
+
return getContextVersion({credential}) === version;
|
|
103
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -34,27 +34,22 @@
|
|
|
34
34
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
35
35
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
36
36
|
*/
|
|
37
|
+
import {
|
|
38
|
+
assertCredentialContext,
|
|
39
|
+
assertDateString,
|
|
40
|
+
checkContextVersion,
|
|
41
|
+
getContextForVersion
|
|
42
|
+
} from './helpers.js';
|
|
37
43
|
import {documentLoader as _documentLoader} from './documentLoader.js';
|
|
38
44
|
import {CredentialIssuancePurpose} from './CredentialIssuancePurpose.js';
|
|
39
45
|
import jsigs from 'jsonld-signatures';
|
|
40
46
|
import jsonld from 'jsonld';
|
|
41
|
-
export const defaultDocumentLoader =
|
|
42
|
-
jsigs.extendContextLoader(_documentLoader);
|
|
43
|
-
import * as credentialsContext from 'credentials-context';
|
|
44
47
|
|
|
45
48
|
const {AssertionProofPurpose, AuthenticationProofPurpose} = jsigs.purposes;
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
export {dateRegex} from './helpers.js';
|
|
50
|
+
export const defaultDocumentLoader = jsigs.extendContextLoader(_documentLoader);
|
|
48
51
|
export {CredentialIssuancePurpose};
|
|
49
52
|
|
|
50
|
-
// Z and T can be lowercase
|
|
51
|
-
// RFC3339 regex
|
|
52
|
-
export const dateRegex = new RegExp('^(\\d{4})-(0[1-9]|1[0-2])-' +
|
|
53
|
-
'(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):' +
|
|
54
|
-
'([0-5][0-9]):([0-5][0-9]|60)' +
|
|
55
|
-
'(\\.[0-9]+)?(Z|(\\+|-)([01][0-9]|2[0-3]):' +
|
|
56
|
-
'([0-5][0-9]))$', 'i');
|
|
57
|
-
|
|
58
53
|
/**
|
|
59
54
|
* @typedef {object} LinkedDataSignature
|
|
60
55
|
*/
|
|
@@ -131,9 +126,10 @@ export async function issue({
|
|
|
131
126
|
if(!credential) {
|
|
132
127
|
throw new TypeError('"credential" parameter is required for issuing.');
|
|
133
128
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
if(checkContextVersion({
|
|
130
|
+
credential,
|
|
131
|
+
version: 1.0
|
|
132
|
+
}) && !credential.issuanceDate) {
|
|
137
133
|
const now = (new Date()).toJSON();
|
|
138
134
|
credential.issuanceDate = `${now.slice(0, now.length - 5)}Z`;
|
|
139
135
|
}
|
|
@@ -167,7 +163,8 @@ export async function derive({
|
|
|
167
163
|
documentLoader = defaultDocumentLoader
|
|
168
164
|
} = {}) {
|
|
169
165
|
if(!verifiableCredential) {
|
|
170
|
-
throw new TypeError(
|
|
166
|
+
throw new TypeError(
|
|
167
|
+
'"verifiableCredential" parameter is required for deriving.');
|
|
171
168
|
}
|
|
172
169
|
if(!suite) {
|
|
173
170
|
throw new TypeError('"suite" parameter is required for deriving.');
|
|
@@ -342,7 +339,6 @@ async function _verifyCredential(options = {}) {
|
|
|
342
339
|
result.verified = false;
|
|
343
340
|
}
|
|
344
341
|
}
|
|
345
|
-
|
|
346
342
|
return result;
|
|
347
343
|
}
|
|
348
344
|
|
|
@@ -356,6 +352,7 @@ async function _verifyCredential(options = {}) {
|
|
|
356
352
|
* @param {string} [options.holder] - Optional presentation holder url.
|
|
357
353
|
* @param {string|Date} [options.now] - A string representing date time in
|
|
358
354
|
* ISO 8601 format or an instance of Date. Defaults to current date time.
|
|
355
|
+
* @param {number} [options.version = 2.0] - The VC context version to use.
|
|
359
356
|
*
|
|
360
357
|
* @throws {TypeError} If verifiableCredential param is missing.
|
|
361
358
|
* @throws {Error} If the credential (or the presentation params) are missing
|
|
@@ -365,10 +362,11 @@ async function _verifyCredential(options = {}) {
|
|
|
365
362
|
* VerifiablePresentation.
|
|
366
363
|
*/
|
|
367
364
|
export function createPresentation({
|
|
368
|
-
verifiableCredential, id, holder, now
|
|
365
|
+
verifiableCredential, id, holder, now, version = 2.0
|
|
369
366
|
} = {}) {
|
|
367
|
+
const initialContext = getContextForVersion({version});
|
|
370
368
|
const presentation = {
|
|
371
|
-
'@context': [
|
|
369
|
+
'@context': [initialContext],
|
|
372
370
|
type: ['VerifiablePresentation']
|
|
373
371
|
};
|
|
374
372
|
if(verifiableCredential) {
|
|
@@ -547,13 +545,7 @@ export function _checkPresentation(presentation) {
|
|
|
547
545
|
// normalize to an array to allow the common case of context being a string
|
|
548
546
|
const context = Array.isArray(presentation['@context']) ?
|
|
549
547
|
presentation['@context'] : [presentation['@context']];
|
|
550
|
-
|
|
551
|
-
// ensure first context is 'https://www.w3.org/2018/credentials/v1'
|
|
552
|
-
if(context[0] !== CREDENTIALS_CONTEXT_V1_URL) {
|
|
553
|
-
throw new Error(
|
|
554
|
-
`"${CREDENTIALS_CONTEXT_V1_URL}" needs to be first in the ` +
|
|
555
|
-
'list of contexts.');
|
|
556
|
-
}
|
|
548
|
+
assertCredentialContext({context});
|
|
557
549
|
|
|
558
550
|
const types = jsonld.getValues(presentation, 'type');
|
|
559
551
|
|
|
@@ -563,6 +555,15 @@ export function _checkPresentation(presentation) {
|
|
|
563
555
|
}
|
|
564
556
|
}
|
|
565
557
|
|
|
558
|
+
// these props of a VC must be an object with a type
|
|
559
|
+
// if present in a VC or VP
|
|
560
|
+
const mustHaveType = [
|
|
561
|
+
'proof',
|
|
562
|
+
'credentialStatus',
|
|
563
|
+
'termsOfUse',
|
|
564
|
+
'evidence'
|
|
565
|
+
];
|
|
566
|
+
|
|
566
567
|
// export for testing
|
|
567
568
|
/**
|
|
568
569
|
* @param {object} options - The options.
|
|
@@ -582,12 +583,7 @@ export function _checkCredential({
|
|
|
582
583
|
if(typeof now === 'string') {
|
|
583
584
|
now = new Date(now);
|
|
584
585
|
}
|
|
585
|
-
|
|
586
|
-
if(credential['@context'][0] !== CREDENTIALS_CONTEXT_V1_URL) {
|
|
587
|
-
throw new Error(
|
|
588
|
-
`"${CREDENTIALS_CONTEXT_V1_URL}" needs to be first in the ` +
|
|
589
|
-
'list of contexts.');
|
|
590
|
-
}
|
|
586
|
+
assertCredentialContext({context: credential['@context']});
|
|
591
587
|
|
|
592
588
|
// check type presence and cardinality
|
|
593
589
|
if(!credential.type) {
|
|
@@ -598,47 +594,71 @@ export function _checkCredential({
|
|
|
598
594
|
throw new Error('"type" must include `VerifiableCredential`.');
|
|
599
595
|
}
|
|
600
596
|
|
|
601
|
-
|
|
602
|
-
throw new Error('"credentialSubject" property is required.');
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// If credentialSubject.id is present and is not a URI, reject it
|
|
606
|
-
if(credential.credentialSubject.id) {
|
|
607
|
-
_validateUriId({
|
|
608
|
-
id: credential.credentialSubject.id, propertyName: 'credentialSubject.id'
|
|
609
|
-
});
|
|
610
|
-
}
|
|
597
|
+
_checkCredentialSubjects({credential});
|
|
611
598
|
|
|
612
599
|
if(!credential.issuer) {
|
|
613
600
|
throw new Error('"issuer" property is required.');
|
|
614
601
|
}
|
|
602
|
+
if(checkContextVersion({credential, version: 1.0})) {
|
|
603
|
+
// check issuanceDate exists
|
|
604
|
+
if(!credential.issuanceDate) {
|
|
605
|
+
throw new Error('"issuanceDate" property is required.');
|
|
606
|
+
}
|
|
607
|
+
// check issuanceDate format on issue
|
|
608
|
+
assertDateString({credential, prop: 'issuanceDate'});
|
|
615
609
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
610
|
+
// check issuanceDate cardinality
|
|
611
|
+
if(jsonld.getValues(credential, 'issuanceDate').length > 1) {
|
|
612
|
+
throw new Error('"issuanceDate" property can only have one value.');
|
|
613
|
+
}
|
|
614
|
+
// optionally check expirationDate
|
|
615
|
+
if('expirationDate' in credential) {
|
|
616
|
+
// check if `expirationDate` property is a date
|
|
617
|
+
assertDateString({credential, prop: 'expirationDate'});
|
|
618
|
+
if(mode === 'verify') {
|
|
619
|
+
// check if `now` is after `expirationDate`
|
|
620
|
+
if(now > new Date(credential.expirationDate)) {
|
|
621
|
+
throw new Error('Credential has expired.');
|
|
622
|
+
}
|
|
623
|
+
}
|
|
630
624
|
}
|
|
631
625
|
// check if `now` is before `issuanceDate` on verification
|
|
632
626
|
if(mode === 'verify') {
|
|
633
|
-
issuanceDate = new Date(issuanceDate);
|
|
627
|
+
const issuanceDate = new Date(credential.issuanceDate);
|
|
634
628
|
if(now < issuanceDate) {
|
|
635
629
|
throw new Error(
|
|
636
630
|
`The current date time (${now.toISOString()}) is before the ` +
|
|
637
|
-
`"issuanceDate" (${issuanceDate
|
|
631
|
+
`"issuanceDate" (${credential.issuanceDate}).`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if(checkContextVersion({credential, version: 2.0})) {
|
|
636
|
+
// check if 'validUntil' and 'validFrom'
|
|
637
|
+
let {validUntil, validFrom} = credential;
|
|
638
|
+
if(validUntil) {
|
|
639
|
+
assertDateString({credential, prop: 'validUntil'});
|
|
640
|
+
if(mode === 'verify') {
|
|
641
|
+
validUntil = new Date(credential.validUntil);
|
|
642
|
+
if(now > validUntil) {
|
|
643
|
+
throw new Error(
|
|
644
|
+
`The current date time (${now.toISOString()}) is after ` +
|
|
645
|
+
`"validUntil" (${credential.validUntil}).`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if(validFrom) {
|
|
650
|
+
assertDateString({credential, prop: 'validFrom'});
|
|
651
|
+
if(mode === 'verify') {
|
|
652
|
+
// check if `now` is before `validFrom`
|
|
653
|
+
validFrom = new Date(credential.validFrom);
|
|
654
|
+
if(now < validFrom) {
|
|
655
|
+
throw new Error(
|
|
656
|
+
`The current date time (${now.toISOString()}) is before ` +
|
|
657
|
+
`"validFrom" (${credential.validFrom}).`);
|
|
658
|
+
}
|
|
638
659
|
}
|
|
639
660
|
}
|
|
640
661
|
}
|
|
641
|
-
|
|
642
662
|
// check issuer cardinality
|
|
643
663
|
if(jsonld.getValues(credential, 'issuer').length > 1) {
|
|
644
664
|
throw new Error('"issuer" property can only have one value.');
|
|
@@ -653,14 +673,18 @@ export function _checkCredential({
|
|
|
653
673
|
_validateUriId({id: issuer, propertyName: 'issuer'});
|
|
654
674
|
}
|
|
655
675
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
676
|
+
// check credentialStatus
|
|
677
|
+
jsonld.getValues(credential, 'credentialStatus').forEach(cs => {
|
|
678
|
+
// check if optional "id" is a URL
|
|
679
|
+
if('id' in cs) {
|
|
680
|
+
_validateUriId({id: cs.id, propertyName: 'credentialStatus.id'});
|
|
659
681
|
}
|
|
660
|
-
|
|
682
|
+
|
|
683
|
+
// check "type" present
|
|
684
|
+
if(!cs.type) {
|
|
661
685
|
throw new Error('"credentialStatus" must include a type.');
|
|
662
686
|
}
|
|
663
|
-
}
|
|
687
|
+
});
|
|
664
688
|
|
|
665
689
|
// check evidences are URLs
|
|
666
690
|
jsonld.getValues(credential, 'evidence').forEach(evidence => {
|
|
@@ -670,20 +694,144 @@ export function _checkCredential({
|
|
|
670
694
|
}
|
|
671
695
|
});
|
|
672
696
|
|
|
673
|
-
if
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
697
|
+
// check if properties that require a type are
|
|
698
|
+
// defined, objects, and objects with types
|
|
699
|
+
for(const prop of mustHaveType) {
|
|
700
|
+
if(prop in credential) {
|
|
701
|
+
const _value = credential[prop];
|
|
702
|
+
if(Array.isArray(_value)) {
|
|
703
|
+
_value.forEach(entry => _checkTypedObject(entry, prop));
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
_checkTypedObject(_value, prop);
|
|
683
707
|
}
|
|
684
708
|
}
|
|
685
709
|
}
|
|
686
710
|
|
|
711
|
+
/**
|
|
712
|
+
* @private
|
|
713
|
+
* Checks that a property is non-empty object with
|
|
714
|
+
* property type.
|
|
715
|
+
*
|
|
716
|
+
* @param {object} obj - A potential object.
|
|
717
|
+
* @param {string} name - The name of the property.
|
|
718
|
+
*
|
|
719
|
+
* @throws {Error} if the property is not an object with a type.
|
|
720
|
+
*
|
|
721
|
+
* @returns {undefined} - Returns on success.
|
|
722
|
+
*/
|
|
723
|
+
function _checkTypedObject(obj, name) {
|
|
724
|
+
if(!isObject(obj)) {
|
|
725
|
+
throw new Error(`property "${name}" must be an object.`);
|
|
726
|
+
}
|
|
727
|
+
if(_emptyObject(obj)) {
|
|
728
|
+
throw new Error(`property "${name}" can not be an empty object.`);
|
|
729
|
+
}
|
|
730
|
+
if(!('type' in obj)) {
|
|
731
|
+
throw new Error(`property "${name}" must have property type.`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* @private
|
|
737
|
+
* Takes in a credential and checks the credentialSubject(s)
|
|
738
|
+
*
|
|
739
|
+
* @param {object} options - Options.
|
|
740
|
+
* @param {object} options.credential - The credential to check.
|
|
741
|
+
*
|
|
742
|
+
* @throws {Error} error - Throws on errors in the credential subject.
|
|
743
|
+
*
|
|
744
|
+
* @returns {undefined} - Returns on success.
|
|
745
|
+
*/
|
|
746
|
+
function _checkCredentialSubjects({credential}) {
|
|
747
|
+
if(!credential?.credentialSubject) {
|
|
748
|
+
throw new Error('"credentialSubject" property is required.');
|
|
749
|
+
}
|
|
750
|
+
if(Array.isArray(credential?.credentialSubject)) {
|
|
751
|
+
return credential?.credentialSubject.map(
|
|
752
|
+
subject => _checkCredentialSubject({subject}));
|
|
753
|
+
}
|
|
754
|
+
return _checkCredentialSubject({subject: credential?.credentialSubject});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* @private
|
|
759
|
+
*
|
|
760
|
+
* Checks a credential subject is valid.
|
|
761
|
+
*
|
|
762
|
+
* @param {object} options - Options.
|
|
763
|
+
* @param {object} options.subject - A potential credential subject.
|
|
764
|
+
*
|
|
765
|
+
* @throws {Error} If the credentialSubject is not valid.
|
|
766
|
+
*
|
|
767
|
+
* @returns {undefined} Returns on success.
|
|
768
|
+
*/
|
|
769
|
+
function _checkCredentialSubject({subject}) {
|
|
770
|
+
if(isObject(subject) === false) {
|
|
771
|
+
throw new Error('"credentialSubject" must be a non-null object.');
|
|
772
|
+
}
|
|
773
|
+
if(_emptyObject(subject)) {
|
|
774
|
+
throw new Error('"credentialSubject" must make a claim.');
|
|
775
|
+
}
|
|
776
|
+
// If credentialSubject.id is present and is not a URI, reject it
|
|
777
|
+
if(subject.id) {
|
|
778
|
+
_validateUriId({
|
|
779
|
+
id: subject.id, propertyName: 'credentialSubject.id'
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* @private
|
|
786
|
+
* Checks if parameter is an object.
|
|
787
|
+
*
|
|
788
|
+
* @param {object} obj - A potential object.
|
|
789
|
+
*
|
|
790
|
+
* @returns {boolean} - Returns false if not an object or null.
|
|
791
|
+
*/
|
|
792
|
+
function isObject(obj) {
|
|
793
|
+
// return false for null even though it has type object
|
|
794
|
+
if(obj === null) {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
// if something has type object and is not null return true
|
|
798
|
+
if((typeof obj) === 'object') {
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
// return false for strings, symbols, etc.
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* @private
|
|
807
|
+
* Is it an empty object?
|
|
808
|
+
*
|
|
809
|
+
* @param {object} obj - A potential object.
|
|
810
|
+
*
|
|
811
|
+
* @returns {boolean} - Is it empty?
|
|
812
|
+
*/
|
|
813
|
+
function _emptyObject(obj) {
|
|
814
|
+
// if the parameter is not an object return true
|
|
815
|
+
// as a non-object is an empty object
|
|
816
|
+
if(!isObject(obj)) {
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
return Object.keys(obj).length === 0;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* @private
|
|
824
|
+
*
|
|
825
|
+
* Validates if an ID is a URL.
|
|
826
|
+
*
|
|
827
|
+
* @param {object} options - Options.
|
|
828
|
+
* @param {string} options.id - the id.
|
|
829
|
+
* @param {string} options.propertyName - The property name.
|
|
830
|
+
*
|
|
831
|
+
* @throws {Error} Throws if an id is not a URL.
|
|
832
|
+
*
|
|
833
|
+
* @returns {undefined} Returns on success.
|
|
834
|
+
*/
|
|
687
835
|
function _validateUriId({id, propertyName}) {
|
|
688
836
|
let parsed;
|
|
689
837
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalbazaar/vc",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Verifiable Credentials JavaScript library.",
|
|
5
5
|
"homepage": "https://github.com/digitalbazaar/vc",
|
|
6
6
|
"author": {
|
|
@@ -28,41 +28,45 @@
|
|
|
28
28
|
"lib/**/*.js"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"credentials-context": "^
|
|
31
|
+
"@digitalbazaar/credentials-context": "^3.1.0",
|
|
32
|
+
"ed25519-signature-2018-context": "^1.1.0",
|
|
32
33
|
"jsonld": "^8.3.1",
|
|
33
34
|
"jsonld-signatures": "^11.2.1"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
37
|
+
"@digitalbazaar/bbs-2023-cryptosuite": "^1.2.0",
|
|
38
|
+
"@digitalbazaar/bls12-381-multikey": "^1.3.0",
|
|
36
39
|
"@digitalbazaar/credentials-examples-context": "^1.0.0",
|
|
37
|
-
"@digitalbazaar/data-integrity": "^2.
|
|
38
|
-
"@digitalbazaar/data-integrity-context": "^2.0.
|
|
39
|
-
"@digitalbazaar/ecdsa-multikey": "^1.
|
|
40
|
-
"@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^3.
|
|
40
|
+
"@digitalbazaar/data-integrity": "^2.2.0",
|
|
41
|
+
"@digitalbazaar/data-integrity-context": "^2.0.1",
|
|
42
|
+
"@digitalbazaar/ecdsa-multikey": "^1.7.0",
|
|
43
|
+
"@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^3.2.1",
|
|
41
44
|
"@digitalbazaar/ed25519-signature-2018": "^4.0.0",
|
|
42
45
|
"@digitalbazaar/ed25519-verification-key-2018": "^4.0.0",
|
|
43
|
-
"@digitalbazaar/multikey-context": "^
|
|
46
|
+
"@digitalbazaar/multikey-context": "^2.0.1",
|
|
44
47
|
"@digitalbazaar/odrl-context": "^1.0.0",
|
|
45
|
-
"c8": "^
|
|
46
|
-
"chai": "^4.
|
|
48
|
+
"c8": "^10.1.2",
|
|
49
|
+
"chai": "^4.5.0",
|
|
47
50
|
"cross-env": "^7.0.3",
|
|
48
51
|
"did-context": "^3.1.1",
|
|
49
52
|
"did-veres-one": "^16.0.0",
|
|
50
|
-
"eslint": "^8.
|
|
51
|
-
"eslint-config-digitalbazaar": "^5.0
|
|
52
|
-
"eslint-plugin-jsdoc": "^
|
|
53
|
-
"eslint-plugin-unicorn": "^
|
|
54
|
-
"karma": "^6.4.
|
|
53
|
+
"eslint": "^8.57.0",
|
|
54
|
+
"eslint-config-digitalbazaar": "^5.2.0",
|
|
55
|
+
"eslint-plugin-jsdoc": "^48.10.2",
|
|
56
|
+
"eslint-plugin-unicorn": "^55.0.0",
|
|
57
|
+
"karma": "^6.4.4",
|
|
55
58
|
"karma-chai": "^0.1.0",
|
|
56
|
-
"karma-chrome-launcher": "^3.
|
|
59
|
+
"karma-chrome-launcher": "^3.2.0",
|
|
57
60
|
"karma-mocha": "^2.0.1",
|
|
58
61
|
"karma-mocha-reporter": "^2.2.5",
|
|
59
62
|
"karma-sourcemap-loader": "^0.4.0",
|
|
60
|
-
"karma-webpack": "^5.0.
|
|
61
|
-
"
|
|
63
|
+
"karma-webpack": "^5.0.1",
|
|
64
|
+
"klona": "^2.0.6",
|
|
65
|
+
"mocha": "^10.7.0",
|
|
62
66
|
"mocha-lcov-reporter": "^1.3.0",
|
|
63
|
-
"uuid": "^
|
|
67
|
+
"uuid": "^10.0.0",
|
|
64
68
|
"veres-one-context": "^12.0.0",
|
|
65
|
-
"webpack": "^5.
|
|
69
|
+
"webpack": "^5.93.0"
|
|
66
70
|
},
|
|
67
71
|
"c8": {
|
|
68
72
|
"reporter": [
|
|
@@ -72,7 +76,7 @@
|
|
|
72
76
|
]
|
|
73
77
|
},
|
|
74
78
|
"engines": {
|
|
75
|
-
"node": ">=
|
|
79
|
+
"node": ">=18"
|
|
76
80
|
},
|
|
77
81
|
"keywords": [
|
|
78
82
|
"JSON",
|