@bedrock/vc-delivery 5.0.0 → 5.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/lib/vcjwt.js ADDED
@@ -0,0 +1,375 @@
1
+ /*!
2
+ * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+ import {decodeJwt} from 'jose';
6
+
7
+ const {util: {BedrockError}} = bedrock;
8
+
9
+ const VC_CONTEXT_1 = 'https://www.w3.org/2018/credentials/v1';
10
+ const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
11
+
12
+ export function decodeVCJWTCredential({jwt} = {}) {
13
+ const payload = decodeJwt(jwt);
14
+
15
+ /* Example:
16
+ {
17
+ "alg": <signer.algorithm>,
18
+ "kid": <signer.id>
19
+ }.
20
+ {
21
+ "iss": <verifiableCredential.issuer>,
22
+ "jti": <verifiableCredential.id>
23
+ "sub": <verifiableCredential.credentialSubject>
24
+ "nbf": <verifiableCredential.[issuanceDate | validFrom]>
25
+ "exp": <verifiableCredential.[expirationDate | validUntil]>
26
+ "vc": <verifiableCredential>
27
+ }
28
+ */
29
+ const {vc} = payload;
30
+ if(!(vc && typeof vc === 'object')) {
31
+ throw new BedrockError('JWT validation failed.', {
32
+ name: 'DataError',
33
+ details: {
34
+ httpStatusCode: 400,
35
+ public: true,
36
+ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
37
+ reason: 'missing or unexpected "vc" claim value.',
38
+ claim: 'vc'
39
+ }
40
+ });
41
+ }
42
+
43
+ let {'@context': context = []} = vc;
44
+ if(!Array.isArray(context)) {
45
+ context = [context];
46
+ }
47
+ const isVersion1 = context.includes(VC_CONTEXT_1);
48
+ const isVersion2 = context.includes(VC_CONTEXT_2);
49
+ if(!(isVersion1 ^ isVersion2)) {
50
+ throw new BedrockError(
51
+ 'Verifiable credential is neither version "1.x" nor "2.x".', {
52
+ name: 'DataError',
53
+ details: {
54
+ httpStatusCode: 400,
55
+ public: true
56
+ }
57
+ });
58
+ }
59
+
60
+ const credential = {...vc};
61
+ const {iss, jti, sub, nbf, exp} = payload;
62
+
63
+ // inject `issuer` value
64
+ if(vc.issuer === undefined) {
65
+ vc.issuer = iss;
66
+ } else if(vc.issuer && typeof vc.issuer === 'object' &&
67
+ vc.issuer.id === undefined) {
68
+ vc.issuer = {id: iss, ...vc.issuer};
69
+ } else if(iss !== vc.issuer && iss !== vc.issuer?.id) {
70
+ throw new BedrockError(
71
+ 'VC-JWT "iss" claim does not equal nor does it exclusively ' +
72
+ 'provide verifiable credential "issuer" / "issuer.id".', {
73
+ name: 'DataError',
74
+ details: {
75
+ httpStatusCode: 400,
76
+ public: true
77
+ }
78
+ });
79
+ }
80
+
81
+ if(jti !== undefined && jti !== vc.id) {
82
+ // inject `id` value
83
+ if(vc.id === undefined) {
84
+ vc.id = jti;
85
+ } else {
86
+ throw new BedrockError(
87
+ 'VC-JWT "jti" claim does not equal nor does it exclusively ' +
88
+ 'provide verifiable credential "id".', {
89
+ name: 'DataError',
90
+ details: {
91
+ httpStatusCode: 400,
92
+ public: true
93
+ }
94
+ });
95
+ }
96
+ }
97
+
98
+ if(sub !== undefined && sub !== vc.credentialSubject?.id) {
99
+ // inject `credentialSubject.id` value
100
+ if(!vc.credentialSubject) {
101
+ throw new BedrockError(
102
+ 'Verifiable credential has no "credentialSubject".', {
103
+ name: 'DataError',
104
+ details: {
105
+ httpStatusCode: 400,
106
+ public: true
107
+ }
108
+ });
109
+ }
110
+ if(Array.isArray(vc.credentialSubject)) {
111
+ throw new BedrockError(
112
+ 'Verifiable credential has multiple credential subjects, which is ' +
113
+ 'not supported in VC-JWT.', {
114
+ name: 'DataError',
115
+ details: {
116
+ httpStatusCode: 400,
117
+ public: true
118
+ }
119
+ });
120
+ }
121
+ if(vc.credentialSubject?.id === undefined) {
122
+ vc.credentialSubject = {id: sub, ...vc.credentialSubject};
123
+ } else {
124
+ throw new BedrockError(
125
+ 'VC-JWT "sub" claim does not equal nor does it exclusively ' +
126
+ 'provide verifiable credential "credentialSubject.id".', {
127
+ name: 'DataError',
128
+ details: {
129
+ httpStatusCode: 400,
130
+ public: true
131
+ }
132
+ });
133
+ }
134
+ }
135
+
136
+ if(nbf === undefined && isVersion1) {
137
+ throw new BedrockError('JWT validation failed.', {
138
+ name: 'DataError',
139
+ details: {
140
+ httpStatusCode: 400,
141
+ public: true,
142
+ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
143
+ reason: 'missing "nbf" claim value.',
144
+ claim: 'nbf'
145
+ }
146
+ });
147
+ }
148
+
149
+ if(nbf !== undefined) {
150
+ // fuzzy convert `nbf` into `issuanceDate` / `validFrom`, only require
151
+ // second-level precision
152
+ const dateString = new Date(nbf * 1000).toISOString().slice(0, -5);
153
+ const dateProperty = isVersion1 ? 'issuanceDate' : 'validFrom';
154
+ // inject dateProperty value
155
+ if(vc[dateProperty] === undefined) {
156
+ vc[dateProperty] = dateString + 'Z';
157
+ } else if(!(vc[dateProperty].startsWith(dateString) &&
158
+ vc[dateProperty].endsWith('Z'))) {
159
+ throw new BedrockError(
160
+ 'VC-JWT "nbf" claim does not equal nor does it exclusively provide ' +
161
+ `verifiable credential "${dateProperty}".`, {
162
+ name: 'DataError',
163
+ details: {
164
+ httpStatusCode: 400,
165
+ public: true
166
+ }
167
+ });
168
+ }
169
+ }
170
+
171
+ if(exp !== undefined) {
172
+ // fuzzy convert `exp` into `expirationDate` / `validUntil`, only require
173
+ // second-level precision
174
+ const dateString = new Date(exp * 1000).toISOString().slice(0, -5);
175
+ const dateProperty = isVersion1 ? 'expirationDate' : 'validUntil';
176
+ // inject dateProperty value
177
+ if(vc[dateProperty] === undefined) {
178
+ vc[dateProperty] = dateString + 'Z';
179
+ } else if(!(vc[dateProperty].startsWith(dateString) &&
180
+ vc[dateProperty].endsWith('Z'))) {
181
+ throw new BedrockError(
182
+ 'VC-JWT "exp" claim does not equal nor does it exclusively provide ' +
183
+ `verifiable credential "${dateProperty}".`, {
184
+ name: 'DataError',
185
+ details: {
186
+ httpStatusCode: 400,
187
+ public: true
188
+ }
189
+ });
190
+ }
191
+ }
192
+
193
+ return credential;
194
+ }
195
+
196
+ export function decodeVCJWTPresentation({jwt, challenge} = {}) {
197
+ /* Example:
198
+ {
199
+ "alg": <signer.algorithm>,
200
+ "kid": <signer.id>
201
+ }.
202
+ {
203
+ "iss": <verifiablePresentation.holder>,
204
+ "aud": <verifiablePresentation.domain>,
205
+ "nonce": <verifiablePresentation.nonce>,
206
+ "jti": <verifiablePresentation.id>
207
+ "nbf": <verifiablePresentation.[validFrom]>
208
+ "exp": <verifiablePresentation.[validUntil]>
209
+ "vp": <verifiablePresentation>
210
+ }
211
+ */
212
+ const payload = decodeJwt(jwt);
213
+
214
+ const {vp} = payload;
215
+ if(!(vp && typeof vp === 'object')) {
216
+ throw new BedrockError('JWT validation failed.', {
217
+ name: 'DataError',
218
+ details: {
219
+ httpStatusCode: 400,
220
+ public: true,
221
+ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
222
+ reason: 'missing or unexpected "vp" claim value.',
223
+ claim: 'vp'
224
+ }
225
+ });
226
+ }
227
+
228
+ let {'@context': context = []} = vp;
229
+ if(!Array.isArray(context)) {
230
+ context = [context];
231
+ }
232
+ const isVersion1 = context.includes(VC_CONTEXT_1);
233
+ const isVersion2 = context.includes(VC_CONTEXT_2);
234
+ if(!(isVersion1 ^ isVersion2)) {
235
+ throw new BedrockError(
236
+ 'Verifiable presentation is not either version "1.x" or "2.x".', {
237
+ name: 'DataError',
238
+ details: {
239
+ httpStatusCode: 400,
240
+ public: true
241
+ }
242
+ });
243
+ }
244
+
245
+ const presentation = {...vp};
246
+ const {iss, nonce, jti, nbf, exp} = payload;
247
+
248
+ // inject `holder` value
249
+ if(vp.holder === undefined) {
250
+ vp.holder = iss;
251
+ } else if(vp.holder && typeof vp.holder === 'object' &&
252
+ vp.holder.id === undefined) {
253
+ vp.holder = {id: iss, ...vp.holder};
254
+ } else if(iss !== vp.holder && iss !== vp.holder?.id) {
255
+ throw new BedrockError(
256
+ 'VC-JWT "iss" claim does not equal nor does it exclusively ' +
257
+ 'provide verifiable presentation "holder" / "holder.id".', {
258
+ name: 'DataError',
259
+ details: {
260
+ httpStatusCode: 400,
261
+ public: true
262
+ }
263
+ });
264
+ }
265
+
266
+ if(jti !== undefined && jti !== vp.id) {
267
+ // inject `id` value
268
+ if(vp.id === undefined) {
269
+ vp.id = jti;
270
+ } else {
271
+ throw new BedrockError(
272
+ 'VC-JWT "jti" claim does not equal nor does it exclusively ' +
273
+ 'provide verifiable presentation "id".', {
274
+ name: 'DataError',
275
+ details: {
276
+ httpStatusCode: 400,
277
+ public: true
278
+ }
279
+ });
280
+ }
281
+ }
282
+
283
+ // version 1.x VPs do not support `validFrom`/`validUntil`
284
+ if(nbf !== undefined && isVersion2) {
285
+ // fuzzy convert `nbf` into `validFrom`, only require
286
+ // second-level precision
287
+ const dateString = new Date(nbf * 1000).toISOString().slice(0, -5);
288
+
289
+ // inject `validFrom` value
290
+ if(vp.validFrom === undefined) {
291
+ vp.validFrom = dateString + 'Z';
292
+ } else if(!(vp.validFrom?.startsWith(dateString) &&
293
+ vp.validFrom.endsWith('Z'))) {
294
+ throw new BedrockError(
295
+ 'VC-JWT "nbf" claim does not equal nor does it exclusively provide ' +
296
+ 'verifiable presentation "validFrom".', {
297
+ name: 'DataError',
298
+ details: {
299
+ httpStatusCode: 400,
300
+ public: true
301
+ }
302
+ });
303
+ }
304
+ }
305
+ if(exp !== undefined && isVersion2) {
306
+ // fuzzy convert `exp` into `validUntil`, only require
307
+ // second-level precision
308
+ const dateString = new Date(exp * 1000).toISOString().slice(0, -5);
309
+
310
+ // inject `validUntil` value
311
+ if(vp.validUntil === undefined) {
312
+ vp.validUntil = dateString + 'Z';
313
+ } else if(!(vp.validUntil?.startsWith(dateString) &&
314
+ vp.validUntil?.endsWith('Z'))) {
315
+ throw new BedrockError(
316
+ 'VC-JWT "exp" claim does not equal nor does it exclusively provide ' +
317
+ 'verifiable presentation "validUntil".', {
318
+ name: 'DataError',
319
+ details: {
320
+ httpStatusCode: 400,
321
+ public: true
322
+ }
323
+ });
324
+ }
325
+ }
326
+
327
+ if(challenge !== undefined && nonce !== challenge) {
328
+ throw new BedrockError('JWT validation failed.', {
329
+ name: 'DataError',
330
+ details: {
331
+ httpStatusCode: 400,
332
+ public: true,
333
+ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
334
+ reason: 'missing or unexpected "nonce" claim value.',
335
+ claim: 'nonce'
336
+ }
337
+ });
338
+ }
339
+
340
+ // do some validation on `verifiableCredential`
341
+ let {verifiableCredential = []} = presentation;
342
+ if(!Array.isArray(verifiableCredential)) {
343
+ verifiableCredential = [verifiableCredential];
344
+ }
345
+
346
+ // ensure version 2 VPs only have objects in `verifiableCredential`
347
+ const hasVCJWTs = verifiableCredential.some(vc => typeof vc !== 'object');
348
+ if(isVersion2 && hasVCJWTs) {
349
+ throw new BedrockError(
350
+ 'Version 2.x verifiable presentations must only use objects in the ' +
351
+ '"verifiableCredential" field.', {
352
+ name: 'DataError',
353
+ details: {
354
+ httpStatusCode: 400,
355
+ public: true
356
+ }
357
+ });
358
+ }
359
+
360
+ // transform any VC-JWT VCs to enveloped VCs
361
+ if(presentation.verifiableCredential && hasVCJWTs) {
362
+ presentation.verifiableCredential = verifiableCredential.map(vc => {
363
+ if(typeof vc !== 'string') {
364
+ return vc;
365
+ }
366
+ return {
367
+ '@context': VC_CONTEXT_2,
368
+ id: `data:application/jwt,${vc}`,
369
+ type: 'EnvelopedVerifiableCredential',
370
+ };
371
+ });
372
+ }
373
+
374
+ return presentation;
375
+ }
package/lib/verify.js CHANGED
@@ -87,9 +87,13 @@ export async function verify({
87
87
 
88
88
  // generate useful error to return to client
89
89
  const {name, errors, message} = cause.data.error;
90
+ const causeError = _stripStacktrace({...cause.data.error});
91
+ delete causeError.errors;
90
92
  const error = new BedrockError(message ?? 'Verification error.', {
91
- name: name === 'VerificationError' ? 'DataError' : 'OperationError',
93
+ name: (name === 'VerificationError' || name === 'DataError') ?
94
+ 'DataError' : 'OperationError',
92
95
  details: {
96
+ error: causeError,
93
97
  verified,
94
98
  credentialResults,
95
99
  presentationResult,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "type": "module",
5
5
  "description": "Bedrock Verifiable Credential Delivery",
6
6
  "main": "./lib/index.js",
@@ -303,7 +303,7 @@ const vcFormats = {
303
303
  const issuerInstance = {
304
304
  title: 'Issuer Instance',
305
305
  type: 'object',
306
- required: ['zcapReferenceIds'],
306
+ required: ['supportedFormats', 'zcapReferenceIds'],
307
307
  additionalProperties: false,
308
308
  properties: {
309
309
  id: {