@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 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
- const vc = require('@digitalbazaar/vc');
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
  });
@@ -1,12 +1,10 @@
1
1
  /*!
2
- * Copyright (c) 2019-2023 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import {
5
- contexts as credentialContexts
6
- } from 'credentials-context';
5
+ contexts as credentialsContexts
6
+ } from '@digitalbazaar/credentials-context';
7
7
 
8
- export const contexts = new Map();
9
-
10
- for(const [url, context] of credentialContexts.entries()) {
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
- const {constants: {CREDENTIALS_CONTEXT_V1_URL}} = credentialsContext;
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
- // Set the issuance date to now(), if missing
136
- if(!credential.issuanceDate) {
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': [CREDENTIALS_CONTEXT_V1_URL],
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
- // ensure first context is 'https://www.w3.org/2018/credentials/v1'
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
- if(!credential.credentialSubject) {
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
- // check issuanceDate cardinality
618
- if(jsonld.getValues(credential, 'issuanceDate').length > 1) {
619
- throw new Error('"issuanceDate" property can only have one value.');
620
- }
621
-
622
- // check issued is a date
623
- if(!credential.issuanceDate) {
624
- throw new Error('"issuanceDate" property is required.');
625
- }
626
-
627
- if('issuanceDate' in credential) {
628
- let {issuanceDate} = credential;
629
- if(!dateRegex.test(issuanceDate)) {
630
- throw new Error(`"issuanceDate" must be a valid date: ${issuanceDate}`);
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 < issuanceDate) {
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.toISOString()}).`);
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
- if('credentialStatus' in credential) {
658
- const {credentialStatus} = credential;
659
- if(Array.isArray(credentialStatus) ?
660
- credentialStatus.some(cs => !cs.id) : !credentialStatus.id) {
661
- throw new Error('"credentialStatus" must include an id.');
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
- if(Array.isArray(credentialStatus) ?
664
- credentialStatus.some(cs => !cs.type) : !credentialStatus.type) {
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('expirationDate' in credential) {
678
- const {expirationDate} = credential;
679
- // check if `expirationDate` property is a date
680
- if(!dateRegex.test(expirationDate)) {
681
- throw new Error(
682
- `"expirationDate" must be a valid date: ${expirationDate}`);
683
- }
684
- // check if `now` is after `expirationDate`
685
- if(now > new Date(expirationDate)) {
686
- throw new Error('Credential has expired.');
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": "6.3.0",
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": "^2.0.0",
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.0.0",
37
- "@digitalbazaar/bls12-381-multikey": "^1.1.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.0.0",
40
- "@digitalbazaar/data-integrity-context": "^2.0.0",
41
- "@digitalbazaar/ecdsa-multikey": "^1.6.0",
42
- "@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^3.0.0",
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": "^1.0.0",
46
+ "@digitalbazaar/multikey-context": "^2.0.1",
46
47
  "@digitalbazaar/odrl-context": "^1.0.0",
47
- "c8": "^8.0.1",
48
- "chai": "^4.3.7",
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.0",
53
- "eslint-config-digitalbazaar": "^5.0.1",
54
- "eslint-plugin-jsdoc": "^46.9.0",
55
- "eslint-plugin-unicorn": "^49.0.0",
56
- "karma": "^6.4.1",
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.1.1",
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.0",
63
- "mocha": "^10.2.0",
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": "^9.0.0",
67
+ "uuid": "^10.0.0",
66
68
  "veres-one-context": "^12.0.0",
67
- "webpack": "^5.75.0"
69
+ "webpack": "^5.93.0"
68
70
  },
69
71
  "c8": {
70
72
  "reporter": [