@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 CHANGED
@@ -48,7 +48,7 @@ the following:
48
48
 
49
49
  ## Install
50
50
 
51
- - Browsers and Node.js 14+ are supported.
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 vc from '@digitalbazaar/vc';
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 vc from '@digitalbazaar/vc';
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 signed credential
203
- const credential = {
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:2ef8c7ce-a4da-44b4-ba7f-3d43eaf1e50c",
301
+ "id": "urn:uuid:318d9dce-bc7b-40b9-a956-c9160bf910db",
222
302
  "type": "DataIntegrityProof",
223
- "created": "2023-11-13T22:58:06Z",
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": "u2V0AhVhAtYPKUQxwULXzMdsAfqtipsiX6YEPURYSBFYxoFY-v0vCPyAs1Ckyy61Wtk3xZWyBGNaEr3w0wQiJHHd5B9uR-1gjgCQCVtFPMk-ECi0CJFYv_GTjCChf8St0FQjuExTAnwP0-ipYIOHSun3YqabOfNe2DYFkHBTZa0Csf1a7YUDW8hhsOHqTglhA8aqnyanT-Ybo2-aHBTcI-UmHX0iluGb2IxoHLLhQoOPm2rDW0eB04Fa2Dh6WMKoOl_Bz3wZZDGQ31XoGrQvgIlhAo8qspvC-QQ-xI3KADiA12sO5LRsZ7hl9ozoJEECVsDOKlxWd-dhices5b2ZQIiiRE9XxxJx8YuwCMoD2bRLbOIJtL2lzc3VhbmNlRGF0ZWcvaXNzdWVy"
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:
@@ -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,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
- const {constants: {CREDENTIALS_CONTEXT_V1_URL}} = credentialsContext;
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
- // Set the issuance date to now(), if missing
136
- if(!credential.issuanceDate) {
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('"credential" parameter is required for deriving.');
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': [CREDENTIALS_CONTEXT_V1_URL],
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
- // ensure first context is 'https://www.w3.org/2018/credentials/v1'
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
- if(!credential.credentialSubject) {
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
- // check issuanceDate cardinality
617
- if(jsonld.getValues(credential, 'issuanceDate').length > 1) {
618
- throw new Error('"issuanceDate" property can only have one value.');
619
- }
620
-
621
- // check issued is a date
622
- if(!credential.issuanceDate) {
623
- throw new Error('"issuanceDate" property is required.');
624
- }
625
-
626
- if('issuanceDate' in credential) {
627
- let {issuanceDate} = credential;
628
- if(!dateRegex.test(issuanceDate)) {
629
- throw new Error(`"issuanceDate" must be a valid date: ${issuanceDate}`);
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.toISOString()}).`);
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
- if('credentialStatus' in credential) {
657
- if(Array.isArray(credential.credentialStatus) ? credential.credentialStatus.some(cs => !cs.id) : !credential.credentialStatus.id) {
658
- throw new Error('"credentialStatus" must include an id.');
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
- if(Array.isArray(credential.credentialStatus) ? credential.credentialStatus.some(cs => !cs.type) : !credential.credentialStatus.type) {
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('expirationDate' in credential) {
674
- const {expirationDate} = credential;
675
- // check if `expirationDate` property is a date
676
- if(!dateRegex.test(expirationDate)) {
677
- throw new Error(
678
- `"expirationDate" must be a valid date: ${expirationDate}`);
679
- }
680
- // check if `now` is after `expirationDate`
681
- if(now > new Date(expirationDate)) {
682
- throw new Error('Credential has expired.');
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": "6.2.0",
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": "^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": {
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.0.0",
38
- "@digitalbazaar/data-integrity-context": "^2.0.0",
39
- "@digitalbazaar/ecdsa-multikey": "^1.6.0",
40
- "@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",
41
44
  "@digitalbazaar/ed25519-signature-2018": "^4.0.0",
42
45
  "@digitalbazaar/ed25519-verification-key-2018": "^4.0.0",
43
- "@digitalbazaar/multikey-context": "^1.0.0",
46
+ "@digitalbazaar/multikey-context": "^2.0.1",
44
47
  "@digitalbazaar/odrl-context": "^1.0.0",
45
- "c8": "^8.0.1",
46
- "chai": "^4.3.7",
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.53.0",
51
- "eslint-config-digitalbazaar": "^5.0.1",
52
- "eslint-plugin-jsdoc": "^46.9.0",
53
- "eslint-plugin-unicorn": "^49.0.0",
54
- "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",
55
58
  "karma-chai": "^0.1.0",
56
- "karma-chrome-launcher": "^3.1.1",
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.0",
61
- "mocha": "^10.2.0",
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": "^9.0.0",
67
+ "uuid": "^10.0.0",
64
68
  "veres-one-context": "^12.0.0",
65
- "webpack": "^5.75.0"
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": ">=14"
79
+ "node": ">=18"
76
80
  },
77
81
  "keywords": [
78
82
  "JSON",