@digitalbazaar/vc 6.3.0 → 7.1.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 +9 -3
- package/lib/contexts/index.js +6 -8
- package/lib/helpers.js +121 -0
- package/lib/index.js +259 -84
- package/package.json +23 -21
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ For signing, when setting up a signature suite, you will need to pass in
|
|
|
72
72
|
a key pair containing a private key.
|
|
73
73
|
|
|
74
74
|
```js
|
|
75
|
-
import vc from '@digitalbazaar/vc';
|
|
75
|
+
import * as vc from '@digitalbazaar/vc';
|
|
76
76
|
|
|
77
77
|
// Required to set up a suite instance with private key
|
|
78
78
|
import {Ed25519VerificationKey2020} from
|
|
@@ -94,7 +94,7 @@ Pre-requisites:
|
|
|
94
94
|
Document and Public Key
|
|
95
95
|
|
|
96
96
|
```js
|
|
97
|
-
|
|
97
|
+
import * as vc from '@digitalbazaar/vc';
|
|
98
98
|
|
|
99
99
|
// Sample unsigned credential
|
|
100
100
|
const credential = {
|
|
@@ -477,8 +477,8 @@ Pre-requisites:
|
|
|
477
477
|
// by requiring this first you ensure security
|
|
478
478
|
// contexts are loaded from jsonld-signatures
|
|
479
479
|
// and not an insecure source.
|
|
480
|
+
import * as vc from '@digitalbazaar/vc';
|
|
480
481
|
const {extendContextLoader} = require('jsonld-signatures');
|
|
481
|
-
const vc = require('@digitalbazaar/vc');
|
|
482
482
|
// @digitalbazaar/vc exports its own secure documentLoader.
|
|
483
483
|
const {defaultDocumentLoader} = vc;
|
|
484
484
|
// a valid json-ld @context.
|
|
@@ -516,6 +516,8 @@ Once you've created the presentation (either via `createPresentation()` or
|
|
|
516
516
|
manually), you can sign it using `signPresentation()`:
|
|
517
517
|
|
|
518
518
|
```js
|
|
519
|
+
import * as vc from '@digitalbazaar/vc';
|
|
520
|
+
|
|
519
521
|
const vp = await vc.signPresentation({
|
|
520
522
|
presentation, suite, challenge, documentLoader
|
|
521
523
|
});
|
|
@@ -578,6 +580,8 @@ Pre-requisites:
|
|
|
578
580
|
To verify a verifiable presentation:
|
|
579
581
|
|
|
580
582
|
```js
|
|
583
|
+
import * as vc from '@digitalbazaar/vc';
|
|
584
|
+
|
|
581
585
|
// challenge has been received from the requesting party - see 'challenge'
|
|
582
586
|
// section below
|
|
583
587
|
|
|
@@ -590,6 +594,8 @@ To verify an unsigned presentation, you must set the `unsignedPresentation`
|
|
|
590
594
|
flag:
|
|
591
595
|
|
|
592
596
|
```js
|
|
597
|
+
import * as vc from '@digitalbazaar/vc';
|
|
598
|
+
|
|
593
599
|
const result = await vc.verify({
|
|
594
600
|
presentation, suite, documentLoader, unsignedPresentation: true
|
|
595
601
|
});
|
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,121 @@
|
|
|
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
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Compares two times with consideration of max clock skew
|
|
107
|
+
*
|
|
108
|
+
* @param {object} options - Options.
|
|
109
|
+
* @param {number} options.t1 - time 1
|
|
110
|
+
* @param {number} options.t2 - time 2
|
|
111
|
+
* @param {number} options.maxClockSkew - number of seconds
|
|
112
|
+
* @returns {number} - A number greater or less than zero
|
|
113
|
+
*/
|
|
114
|
+
export function compareTime({t1, t2, maxClockSkew}) {
|
|
115
|
+
// `maxClockSkew` is in seconds, so transform to milliseconds
|
|
116
|
+
if(Math.abs(t1 - t2) < (maxClockSkew * 1000)) {
|
|
117
|
+
// times are equal within the max clock skew
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
return t1 < t2 ? -1 : 1;
|
|
121
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -34,27 +34,23 @@
|
|
|
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
|
+
compareTime,
|
|
42
|
+
getContextForVersion
|
|
43
|
+
} from './helpers.js';
|
|
37
44
|
import {documentLoader as _documentLoader} from './documentLoader.js';
|
|
38
45
|
import {CredentialIssuancePurpose} from './CredentialIssuancePurpose.js';
|
|
39
46
|
import jsigs from 'jsonld-signatures';
|
|
40
47
|
import jsonld from 'jsonld';
|
|
41
|
-
export const defaultDocumentLoader =
|
|
42
|
-
jsigs.extendContextLoader(_documentLoader);
|
|
43
|
-
import * as credentialsContext from 'credentials-context';
|
|
44
48
|
|
|
45
49
|
const {AssertionProofPurpose, AuthenticationProofPurpose} = jsigs.purposes;
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
export {dateRegex} from './helpers.js';
|
|
51
|
+
export const defaultDocumentLoader = jsigs.extendContextLoader(_documentLoader);
|
|
48
52
|
export {CredentialIssuancePurpose};
|
|
49
53
|
|
|
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
54
|
/**
|
|
59
55
|
* @typedef {object} LinkedDataSignature
|
|
60
56
|
*/
|
|
@@ -108,6 +104,10 @@ export const dateRegex = new RegExp('^(\\d{4})-(0[1-9]|1[0-2])-' +
|
|
|
108
104
|
* @param {object} [options.documentLoader] - A document loader.
|
|
109
105
|
* @param {string|Date} [options.now] - A string representing date time in
|
|
110
106
|
* ISO 8601 format or an instance of Date. Defaults to current date time.
|
|
107
|
+
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
|
|
108
|
+
* that clocks may be skewed when checking capability expiration date-times
|
|
109
|
+
* against `date` and when comparing invocation proof creation time against
|
|
110
|
+
* delegation proof creation time.
|
|
111
111
|
*
|
|
112
112
|
* @throws {Error} If missing required properties.
|
|
113
113
|
*
|
|
@@ -117,7 +117,8 @@ export async function issue({
|
|
|
117
117
|
credential, suite,
|
|
118
118
|
purpose = new CredentialIssuancePurpose(),
|
|
119
119
|
documentLoader = defaultDocumentLoader,
|
|
120
|
-
now
|
|
120
|
+
now,
|
|
121
|
+
maxClockSkew = 300
|
|
121
122
|
} = {}) {
|
|
122
123
|
// check to make sure the `suite` has required params
|
|
123
124
|
// Note: verificationMethod defaults to publicKey.id, in suite constructor
|
|
@@ -131,15 +132,16 @@ export async function issue({
|
|
|
131
132
|
if(!credential) {
|
|
132
133
|
throw new TypeError('"credential" parameter is required for issuing.');
|
|
133
134
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
if(checkContextVersion({
|
|
136
|
+
credential,
|
|
137
|
+
version: 1.0
|
|
138
|
+
}) && !credential.issuanceDate) {
|
|
137
139
|
const now = (new Date()).toJSON();
|
|
138
140
|
credential.issuanceDate = `${now.slice(0, now.length - 5)}Z`;
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
// run common credential checks
|
|
142
|
-
_checkCredential({credential, now, mode: 'issue'});
|
|
144
|
+
_checkCredential({credential, now, mode: 'issue', maxClockSkew});
|
|
143
145
|
|
|
144
146
|
return jsigs.sign(credential, {purpose, documentLoader, suite});
|
|
145
147
|
}
|
|
@@ -223,6 +225,10 @@ export async function derive({
|
|
|
223
225
|
* credential status if `credentialStatus` is present on the credential.
|
|
224
226
|
* @param {string|Date} [options.now] - A string representing date time in
|
|
225
227
|
* ISO 8601 format or an instance of Date. Defaults to current date time.
|
|
228
|
+
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
|
|
229
|
+
* that clocks may be skewed when checking capability expiration date-times
|
|
230
|
+
* against `date` and when comparing invocation proof creation time against
|
|
231
|
+
* delegation proof creation time.
|
|
226
232
|
*
|
|
227
233
|
* @returns {Promise<VerifyPresentationResult>} The verification result.
|
|
228
234
|
*/
|
|
@@ -268,6 +274,10 @@ export async function verify(options = {}) {
|
|
|
268
274
|
* credential status if `credentialStatus` is present on the credential.
|
|
269
275
|
* @param {string|Date} [options.now] - A string representing date time in
|
|
270
276
|
* ISO 8601 format or an instance of Date. Defaults to current date time.
|
|
277
|
+
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
|
|
278
|
+
* that clocks may be skewed when checking capability expiration date-times
|
|
279
|
+
* against `date` and when comparing invocation proof creation time against
|
|
280
|
+
* delegation proof creation time.
|
|
271
281
|
*
|
|
272
282
|
* @returns {Promise<VerifyCredentialResult>} The verification result.
|
|
273
283
|
*/
|
|
@@ -299,6 +309,10 @@ export async function verifyCredential(options = {}) {
|
|
|
299
309
|
* definition in the `verify()` docstring, for this param.
|
|
300
310
|
* @param {string|Date} [options.now] - A string representing date time in
|
|
301
311
|
* ISO 8601 format or an instance of Date. Defaults to current date time.
|
|
312
|
+
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
|
|
313
|
+
* that clocks may be skewed when checking capability expiration date-times
|
|
314
|
+
* against `date` and when comparing invocation proof creation time against
|
|
315
|
+
* delegation proof creation time.
|
|
302
316
|
*
|
|
303
317
|
* @throws {Error} If required parameters are missing (in `_checkCredential`).
|
|
304
318
|
*
|
|
@@ -310,10 +324,10 @@ export async function verifyCredential(options = {}) {
|
|
|
310
324
|
* @returns {Promise<VerifyCredentialResult>} The verification result.
|
|
311
325
|
*/
|
|
312
326
|
async function _verifyCredential(options = {}) {
|
|
313
|
-
const {credential, checkStatus, now} = options;
|
|
327
|
+
const {credential, checkStatus, now, maxClockSkew} = options;
|
|
314
328
|
|
|
315
329
|
// run common credential checks
|
|
316
|
-
_checkCredential({credential, now});
|
|
330
|
+
_checkCredential({credential, now, maxClockSkew});
|
|
317
331
|
|
|
318
332
|
// if credential status is provided, a `checkStatus` function must be given
|
|
319
333
|
if(credential.credentialStatus && typeof options.checkStatus !== 'function') {
|
|
@@ -343,7 +357,6 @@ async function _verifyCredential(options = {}) {
|
|
|
343
357
|
result.verified = false;
|
|
344
358
|
}
|
|
345
359
|
}
|
|
346
|
-
|
|
347
360
|
return result;
|
|
348
361
|
}
|
|
349
362
|
|
|
@@ -357,6 +370,11 @@ async function _verifyCredential(options = {}) {
|
|
|
357
370
|
* @param {string} [options.holder] - Optional presentation holder url.
|
|
358
371
|
* @param {string|Date} [options.now] - A string representing date time in
|
|
359
372
|
* ISO 8601 format or an instance of Date. Defaults to current date time.
|
|
373
|
+
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
|
|
374
|
+
* that clocks may be skewed when checking capability expiration date-times
|
|
375
|
+
* against `date` and when comparing invocation proof creation time against
|
|
376
|
+
* delegation proof creation time.
|
|
377
|
+
* @param {number} [options.version = 2.0] - The VC context version to use.
|
|
360
378
|
*
|
|
361
379
|
* @throws {TypeError} If verifiableCredential param is missing.
|
|
362
380
|
* @throws {Error} If the credential (or the presentation params) are missing
|
|
@@ -366,17 +384,18 @@ async function _verifyCredential(options = {}) {
|
|
|
366
384
|
* VerifiablePresentation.
|
|
367
385
|
*/
|
|
368
386
|
export function createPresentation({
|
|
369
|
-
verifiableCredential, id, holder, now
|
|
387
|
+
verifiableCredential, id, holder, now, version = 2.0, maxClockSkew = 300
|
|
370
388
|
} = {}) {
|
|
389
|
+
const initialContext = getContextForVersion({version});
|
|
371
390
|
const presentation = {
|
|
372
|
-
'@context': [
|
|
391
|
+
'@context': [initialContext],
|
|
373
392
|
type: ['VerifiablePresentation']
|
|
374
393
|
};
|
|
375
394
|
if(verifiableCredential) {
|
|
376
395
|
const credentials = [].concat(verifiableCredential);
|
|
377
396
|
// ensure all credentials are valid
|
|
378
397
|
for(const credential of credentials) {
|
|
379
|
-
_checkCredential({credential, now});
|
|
398
|
+
_checkCredential({credential, now, maxClockSkew});
|
|
380
399
|
}
|
|
381
400
|
presentation.verifiableCredential = credentials;
|
|
382
401
|
}
|
|
@@ -459,6 +478,10 @@ export async function signPresentation(options = {}) {
|
|
|
459
478
|
* credential status if `credentialStatus` is present on the credential.
|
|
460
479
|
* @param {string|Date} [options.now] - A string representing date time in
|
|
461
480
|
* ISO 8601 format or an instance of Date. Defaults to current date time.
|
|
481
|
+
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
|
|
482
|
+
* that clocks may be skewed when checking capability expiration date-times
|
|
483
|
+
* against `date` and when comparing invocation proof creation time against
|
|
484
|
+
* delegation proof creation time.
|
|
462
485
|
*
|
|
463
486
|
* @throws {Error} If presentation is missing required params.
|
|
464
487
|
*
|
|
@@ -548,13 +571,7 @@ export function _checkPresentation(presentation) {
|
|
|
548
571
|
// normalize to an array to allow the common case of context being a string
|
|
549
572
|
const context = Array.isArray(presentation['@context']) ?
|
|
550
573
|
presentation['@context'] : [presentation['@context']];
|
|
551
|
-
|
|
552
|
-
// ensure first context is 'https://www.w3.org/2018/credentials/v1'
|
|
553
|
-
if(context[0] !== CREDENTIALS_CONTEXT_V1_URL) {
|
|
554
|
-
throw new Error(
|
|
555
|
-
`"${CREDENTIALS_CONTEXT_V1_URL}" needs to be first in the ` +
|
|
556
|
-
'list of contexts.');
|
|
557
|
-
}
|
|
574
|
+
assertCredentialContext({context});
|
|
558
575
|
|
|
559
576
|
const types = jsonld.getValues(presentation, 'type');
|
|
560
577
|
|
|
@@ -564,6 +581,15 @@ export function _checkPresentation(presentation) {
|
|
|
564
581
|
}
|
|
565
582
|
}
|
|
566
583
|
|
|
584
|
+
// these props of a VC must be an object with a type
|
|
585
|
+
// if present in a VC or VP
|
|
586
|
+
const mustHaveType = [
|
|
587
|
+
'proof',
|
|
588
|
+
'credentialStatus',
|
|
589
|
+
'termsOfUse',
|
|
590
|
+
'evidence'
|
|
591
|
+
];
|
|
592
|
+
|
|
567
593
|
// export for testing
|
|
568
594
|
/**
|
|
569
595
|
* @param {object} options - The options.
|
|
@@ -571,6 +597,10 @@ export function _checkPresentation(presentation) {
|
|
|
571
597
|
* VerifiableCredential.
|
|
572
598
|
* @param {string|Date} [options.now] - A string representing date time in
|
|
573
599
|
* ISO 8601 format or an instance of Date. Defaults to current date time.
|
|
600
|
+
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
|
|
601
|
+
* that clocks may be skewed when checking capability expiration date-times
|
|
602
|
+
* against `date` and when comparing invocation proof creation time against
|
|
603
|
+
* delegation proof creation time.
|
|
574
604
|
* @param {string} [options.mode] - The mode of operation for this
|
|
575
605
|
* validation function, either `issue` or `verify`.
|
|
576
606
|
*
|
|
@@ -578,17 +608,12 @@ export function _checkPresentation(presentation) {
|
|
|
578
608
|
* @private
|
|
579
609
|
*/
|
|
580
610
|
export function _checkCredential({
|
|
581
|
-
credential, now = new Date(), mode = 'verify'
|
|
611
|
+
credential, now = new Date(), mode = 'verify', maxClockSkew = 300
|
|
582
612
|
} = {}) {
|
|
583
613
|
if(typeof now === 'string') {
|
|
584
614
|
now = new Date(now);
|
|
585
615
|
}
|
|
586
|
-
|
|
587
|
-
if(credential['@context'][0] !== CREDENTIALS_CONTEXT_V1_URL) {
|
|
588
|
-
throw new Error(
|
|
589
|
-
`"${CREDENTIALS_CONTEXT_V1_URL}" needs to be first in the ` +
|
|
590
|
-
'list of contexts.');
|
|
591
|
-
}
|
|
616
|
+
assertCredentialContext({context: credential['@context']});
|
|
592
617
|
|
|
593
618
|
// check type presence and cardinality
|
|
594
619
|
if(!credential.type) {
|
|
@@ -599,47 +624,72 @@ export function _checkCredential({
|
|
|
599
624
|
throw new Error('"type" must include `VerifiableCredential`.');
|
|
600
625
|
}
|
|
601
626
|
|
|
602
|
-
|
|
603
|
-
throw new Error('"credentialSubject" property is required.');
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// If credentialSubject.id is present and is not a URI, reject it
|
|
607
|
-
if(credential.credentialSubject.id) {
|
|
608
|
-
_validateUriId({
|
|
609
|
-
id: credential.credentialSubject.id, propertyName: 'credentialSubject.id'
|
|
610
|
-
});
|
|
611
|
-
}
|
|
627
|
+
_checkCredentialSubjects({credential});
|
|
612
628
|
|
|
613
629
|
if(!credential.issuer) {
|
|
614
630
|
throw new Error('"issuer" property is required.');
|
|
615
631
|
}
|
|
632
|
+
if(checkContextVersion({credential, version: 1.0})) {
|
|
633
|
+
// check issuanceDate exists
|
|
634
|
+
if(!credential.issuanceDate) {
|
|
635
|
+
throw new Error('"issuanceDate" property is required.');
|
|
636
|
+
}
|
|
637
|
+
// check issuanceDate format on issue
|
|
638
|
+
assertDateString({credential, prop: 'issuanceDate'});
|
|
616
639
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
640
|
+
// check issuanceDate cardinality
|
|
641
|
+
if(jsonld.getValues(credential, 'issuanceDate').length > 1) {
|
|
642
|
+
throw new Error('"issuanceDate" property can only have one value.');
|
|
643
|
+
}
|
|
644
|
+
// optionally check expirationDate
|
|
645
|
+
if('expirationDate' in credential) {
|
|
646
|
+
// check if `expirationDate` property is a date
|
|
647
|
+
assertDateString({credential, prop: 'expirationDate'});
|
|
648
|
+
if(mode === 'verify') {
|
|
649
|
+
// check if `now` is after `expirationDate`
|
|
650
|
+
const expirationDate = new Date(credential.expirationDate);
|
|
651
|
+
if(compareTime({t1: now, t2: expirationDate, maxClockSkew}) > 0) {
|
|
652
|
+
throw new Error('Credential has expired.');
|
|
653
|
+
}
|
|
654
|
+
}
|
|
631
655
|
}
|
|
632
656
|
// check if `now` is before `issuanceDate` on verification
|
|
633
657
|
if(mode === 'verify') {
|
|
634
|
-
issuanceDate = new Date(issuanceDate);
|
|
635
|
-
if(now
|
|
658
|
+
const issuanceDate = new Date(credential.issuanceDate);
|
|
659
|
+
if(compareTime({t1: issuanceDate, t2: now, maxClockSkew}) > 0) {
|
|
636
660
|
throw new Error(
|
|
637
661
|
`The current date time (${now.toISOString()}) is before the ` +
|
|
638
|
-
`"issuanceDate" (${issuanceDate
|
|
662
|
+
`"issuanceDate" (${credential.issuanceDate}).`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if(checkContextVersion({credential, version: 2.0})) {
|
|
667
|
+
// check if 'validUntil' and 'validFrom'
|
|
668
|
+
let {validUntil, validFrom} = credential;
|
|
669
|
+
if(validUntil) {
|
|
670
|
+
assertDateString({credential, prop: 'validUntil'});
|
|
671
|
+
if(mode === 'verify') {
|
|
672
|
+
validUntil = new Date(credential.validUntil);
|
|
673
|
+
if(compareTime({t1: now, t2: validUntil, maxClockSkew}) > 0) {
|
|
674
|
+
throw new Error(
|
|
675
|
+
`The current date time (${now.toISOString()}) is after ` +
|
|
676
|
+
`"validUntil" (${credential.validUntil}).`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if(validFrom) {
|
|
681
|
+
assertDateString({credential, prop: 'validFrom'});
|
|
682
|
+
if(mode === 'verify') {
|
|
683
|
+
// check if `now` is before `validFrom`
|
|
684
|
+
validFrom = new Date(credential.validFrom);
|
|
685
|
+
if(compareTime({t1: validFrom, t2: now, maxClockSkew}) > 0) {
|
|
686
|
+
throw new Error(
|
|
687
|
+
`The current date time (${now.toISOString()}) is before ` +
|
|
688
|
+
`"validFrom" (${credential.validFrom}).`);
|
|
689
|
+
}
|
|
639
690
|
}
|
|
640
691
|
}
|
|
641
692
|
}
|
|
642
|
-
|
|
643
693
|
// check issuer cardinality
|
|
644
694
|
if(jsonld.getValues(credential, 'issuer').length > 1) {
|
|
645
695
|
throw new Error('"issuer" property can only have one value.');
|
|
@@ -654,17 +704,18 @@ export function _checkCredential({
|
|
|
654
704
|
_validateUriId({id: issuer, propertyName: 'issuer'});
|
|
655
705
|
}
|
|
656
706
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if
|
|
660
|
-
|
|
661
|
-
|
|
707
|
+
// check credentialStatus
|
|
708
|
+
jsonld.getValues(credential, 'credentialStatus').forEach(cs => {
|
|
709
|
+
// check if optional "id" is a URL
|
|
710
|
+
if('id' in cs) {
|
|
711
|
+
_validateUriId({id: cs.id, propertyName: 'credentialStatus.id'});
|
|
662
712
|
}
|
|
663
|
-
|
|
664
|
-
|
|
713
|
+
|
|
714
|
+
// check "type" present
|
|
715
|
+
if(!cs.type) {
|
|
665
716
|
throw new Error('"credentialStatus" must include a type.');
|
|
666
717
|
}
|
|
667
|
-
}
|
|
718
|
+
});
|
|
668
719
|
|
|
669
720
|
// check evidences are URLs
|
|
670
721
|
jsonld.getValues(credential, 'evidence').forEach(evidence => {
|
|
@@ -674,20 +725,144 @@ export function _checkCredential({
|
|
|
674
725
|
}
|
|
675
726
|
});
|
|
676
727
|
|
|
677
|
-
if
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if(
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
728
|
+
// check if properties that require a type are
|
|
729
|
+
// defined, objects, and objects with types
|
|
730
|
+
for(const prop of mustHaveType) {
|
|
731
|
+
if(prop in credential) {
|
|
732
|
+
const _value = credential[prop];
|
|
733
|
+
if(Array.isArray(_value)) {
|
|
734
|
+
_value.forEach(entry => _checkTypedObject(entry, prop));
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
_checkTypedObject(_value, prop);
|
|
687
738
|
}
|
|
688
739
|
}
|
|
689
740
|
}
|
|
690
741
|
|
|
742
|
+
/**
|
|
743
|
+
* @private
|
|
744
|
+
* Checks that a property is non-empty object with
|
|
745
|
+
* property type.
|
|
746
|
+
*
|
|
747
|
+
* @param {object} obj - A potential object.
|
|
748
|
+
* @param {string} name - The name of the property.
|
|
749
|
+
*
|
|
750
|
+
* @throws {Error} if the property is not an object with a type.
|
|
751
|
+
*
|
|
752
|
+
* @returns {undefined} - Returns on success.
|
|
753
|
+
*/
|
|
754
|
+
function _checkTypedObject(obj, name) {
|
|
755
|
+
if(!isObject(obj)) {
|
|
756
|
+
throw new Error(`property "${name}" must be an object.`);
|
|
757
|
+
}
|
|
758
|
+
if(_emptyObject(obj)) {
|
|
759
|
+
throw new Error(`property "${name}" can not be an empty object.`);
|
|
760
|
+
}
|
|
761
|
+
if(!('type' in obj)) {
|
|
762
|
+
throw new Error(`property "${name}" must have property type.`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* @private
|
|
768
|
+
* Takes in a credential and checks the credentialSubject(s)
|
|
769
|
+
*
|
|
770
|
+
* @param {object} options - Options.
|
|
771
|
+
* @param {object} options.credential - The credential to check.
|
|
772
|
+
*
|
|
773
|
+
* @throws {Error} error - Throws on errors in the credential subject.
|
|
774
|
+
*
|
|
775
|
+
* @returns {undefined} - Returns on success.
|
|
776
|
+
*/
|
|
777
|
+
function _checkCredentialSubjects({credential}) {
|
|
778
|
+
if(!credential?.credentialSubject) {
|
|
779
|
+
throw new Error('"credentialSubject" property is required.');
|
|
780
|
+
}
|
|
781
|
+
if(Array.isArray(credential?.credentialSubject)) {
|
|
782
|
+
return credential?.credentialSubject.map(
|
|
783
|
+
subject => _checkCredentialSubject({subject}));
|
|
784
|
+
}
|
|
785
|
+
return _checkCredentialSubject({subject: credential?.credentialSubject});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* @private
|
|
790
|
+
*
|
|
791
|
+
* Checks a credential subject is valid.
|
|
792
|
+
*
|
|
793
|
+
* @param {object} options - Options.
|
|
794
|
+
* @param {object} options.subject - A potential credential subject.
|
|
795
|
+
*
|
|
796
|
+
* @throws {Error} If the credentialSubject is not valid.
|
|
797
|
+
*
|
|
798
|
+
* @returns {undefined} Returns on success.
|
|
799
|
+
*/
|
|
800
|
+
function _checkCredentialSubject({subject}) {
|
|
801
|
+
if(isObject(subject) === false) {
|
|
802
|
+
throw new Error('"credentialSubject" must be a non-null object.');
|
|
803
|
+
}
|
|
804
|
+
if(_emptyObject(subject)) {
|
|
805
|
+
throw new Error('"credentialSubject" must make a claim.');
|
|
806
|
+
}
|
|
807
|
+
// If credentialSubject.id is present and is not a URI, reject it
|
|
808
|
+
if(subject.id) {
|
|
809
|
+
_validateUriId({
|
|
810
|
+
id: subject.id, propertyName: 'credentialSubject.id'
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* @private
|
|
817
|
+
* Checks if parameter is an object.
|
|
818
|
+
*
|
|
819
|
+
* @param {object} obj - A potential object.
|
|
820
|
+
*
|
|
821
|
+
* @returns {boolean} - Returns false if not an object or null.
|
|
822
|
+
*/
|
|
823
|
+
function isObject(obj) {
|
|
824
|
+
// return false for null even though it has type object
|
|
825
|
+
if(obj === null) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
// if something has type object and is not null return true
|
|
829
|
+
if((typeof obj) === 'object') {
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
// return false for strings, symbols, etc.
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* @private
|
|
838
|
+
* Is it an empty object?
|
|
839
|
+
*
|
|
840
|
+
* @param {object} obj - A potential object.
|
|
841
|
+
*
|
|
842
|
+
* @returns {boolean} - Is it empty?
|
|
843
|
+
*/
|
|
844
|
+
function _emptyObject(obj) {
|
|
845
|
+
// if the parameter is not an object return true
|
|
846
|
+
// as a non-object is an empty object
|
|
847
|
+
if(!isObject(obj)) {
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
return Object.keys(obj).length === 0;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* @private
|
|
855
|
+
*
|
|
856
|
+
* Validates if an ID is a URL.
|
|
857
|
+
*
|
|
858
|
+
* @param {object} options - Options.
|
|
859
|
+
* @param {string} options.id - the id.
|
|
860
|
+
* @param {string} options.propertyName - The property name.
|
|
861
|
+
*
|
|
862
|
+
* @throws {Error} Throws if an id is not a URL.
|
|
863
|
+
*
|
|
864
|
+
* @returns {undefined} Returns on success.
|
|
865
|
+
*/
|
|
691
866
|
function _validateUriId({id, propertyName}) {
|
|
692
867
|
let parsed;
|
|
693
868
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalbazaar/vc",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "Verifiable Credentials JavaScript library.",
|
|
5
5
|
"homepage": "https://github.com/digitalbazaar/vc",
|
|
6
6
|
"author": {
|
|
@@ -28,43 +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": {
|
|
36
|
-
"@digitalbazaar/bbs-2023-cryptosuite": "^1.
|
|
37
|
-
"@digitalbazaar/bls12-381-multikey": "^1.
|
|
37
|
+
"@digitalbazaar/bbs-2023-cryptosuite": "^1.2.0",
|
|
38
|
+
"@digitalbazaar/bls12-381-multikey": "^1.3.0",
|
|
38
39
|
"@digitalbazaar/credentials-examples-context": "^1.0.0",
|
|
39
|
-
"@digitalbazaar/data-integrity": "^2.
|
|
40
|
-
"@digitalbazaar/data-integrity-context": "^2.0.
|
|
41
|
-
"@digitalbazaar/ecdsa-multikey": "^1.
|
|
42
|
-
"@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",
|
|
43
44
|
"@digitalbazaar/ed25519-signature-2018": "^4.0.0",
|
|
44
45
|
"@digitalbazaar/ed25519-verification-key-2018": "^4.0.0",
|
|
45
|
-
"@digitalbazaar/multikey-context": "^
|
|
46
|
+
"@digitalbazaar/multikey-context": "^2.0.1",
|
|
46
47
|
"@digitalbazaar/odrl-context": "^1.0.0",
|
|
47
|
-
"c8": "^
|
|
48
|
-
"chai": "^4.
|
|
48
|
+
"c8": "^10.1.2",
|
|
49
|
+
"chai": "^4.5.0",
|
|
49
50
|
"cross-env": "^7.0.3",
|
|
50
51
|
"did-context": "^3.1.1",
|
|
51
52
|
"did-veres-one": "^16.0.0",
|
|
52
|
-
"eslint": "^8.
|
|
53
|
-
"eslint-config-digitalbazaar": "^5.0
|
|
54
|
-
"eslint-plugin-jsdoc": "^
|
|
55
|
-
"eslint-plugin-unicorn": "^
|
|
56
|
-
"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",
|
|
57
58
|
"karma-chai": "^0.1.0",
|
|
58
|
-
"karma-chrome-launcher": "^3.
|
|
59
|
+
"karma-chrome-launcher": "^3.2.0",
|
|
59
60
|
"karma-mocha": "^2.0.1",
|
|
60
61
|
"karma-mocha-reporter": "^2.2.5",
|
|
61
62
|
"karma-sourcemap-loader": "^0.4.0",
|
|
62
|
-
"karma-webpack": "^5.0.
|
|
63
|
-
"
|
|
63
|
+
"karma-webpack": "^5.0.1",
|
|
64
|
+
"klona": "^2.0.6",
|
|
65
|
+
"mocha": "^10.7.0",
|
|
64
66
|
"mocha-lcov-reporter": "^1.3.0",
|
|
65
|
-
"uuid": "^
|
|
67
|
+
"uuid": "^10.0.0",
|
|
66
68
|
"veres-one-context": "^12.0.0",
|
|
67
|
-
"webpack": "^5.
|
|
69
|
+
"webpack": "^5.93.0"
|
|
68
70
|
},
|
|
69
71
|
"c8": {
|
|
70
72
|
"reporter": [
|