@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/helpers.js +60 -23
- package/lib/http.js +10 -73
- package/lib/oid4/http.js +314 -0
- package/lib/oid4/oid4vci.js +530 -0
- package/lib/oid4/oid4vp.js +330 -0
- package/lib/vcapi.js +88 -5
- package/lib/vcjwt.js +375 -0
- package/lib/verify.js +5 -1
- package/package.json +1 -1
- package/schemas/bedrock-vc-workflow.js +1 -1
- package/lib/openId.js +0 -1026
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'
|
|
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