@bedrock/vc-verifier 19.1.0 → 20.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 +1 -1
- package/lib/config.js +4 -2
- package/lib/di.js +63 -0
- package/lib/documentLoader.js +77 -5
- package/lib/envelopes.js +51 -0
- package/lib/http.js +12 -57
- package/lib/index.js +15 -2
- package/lib/vcjwt.js +537 -0
- package/lib/verify.js +86 -0
- package/package.json +39 -37
- package/schemas/bedrock-vc-verifier.js +231 -30
package/lib/vcjwt.js
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';
|
|
6
|
+
import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey';
|
|
7
|
+
import {importJWK, jwtVerify} from 'jose';
|
|
8
|
+
import {didIo} from '@bedrock/did-io';
|
|
9
|
+
|
|
10
|
+
const {util: {BedrockError}} = bedrock;
|
|
11
|
+
|
|
12
|
+
// supported JWT algs
|
|
13
|
+
const ECDSA_ALGS = ['ES256', 'ES384'];
|
|
14
|
+
const EDDSA_ALGS = ['Ed25519', 'EdDSA'];
|
|
15
|
+
|
|
16
|
+
const VC_CONTEXT_1 = 'https://www.w3.org/2018/credentials/v1';
|
|
17
|
+
const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
|
|
18
|
+
|
|
19
|
+
export async function verifyEnvelopedCredential({jwt} = {}) {
|
|
20
|
+
try {
|
|
21
|
+
const {
|
|
22
|
+
verified, controller, verificationMethod, verifyResult
|
|
23
|
+
} = await _verifyJwt({jwt, proofPurpose: 'assertionMethod'});
|
|
24
|
+
// if verified, parse credential from payload...
|
|
25
|
+
let credential;
|
|
26
|
+
if(verified) {
|
|
27
|
+
credential = _jwtPayloadToCredential({verifyResult});
|
|
28
|
+
}
|
|
29
|
+
const results = [{
|
|
30
|
+
verified,
|
|
31
|
+
verificationMethod,
|
|
32
|
+
controller,
|
|
33
|
+
verifyResult,
|
|
34
|
+
credential
|
|
35
|
+
}];
|
|
36
|
+
return {verified, controller, results, credential};
|
|
37
|
+
} catch(error) {
|
|
38
|
+
return {verified: false, error};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function verifyEnvelopedPresentation({
|
|
43
|
+
jwt, challenge, domain
|
|
44
|
+
} = {}) {
|
|
45
|
+
try {
|
|
46
|
+
const {
|
|
47
|
+
verified, controller, verificationMethod, verifyResult
|
|
48
|
+
} = await _verifyJwt({
|
|
49
|
+
jwt, proofPurpose: 'authentication', audience: domain
|
|
50
|
+
});
|
|
51
|
+
// if verified, parse presentation from payload...
|
|
52
|
+
let presentation;
|
|
53
|
+
if(verified) {
|
|
54
|
+
presentation = _jwtPayloadToPresentation({
|
|
55
|
+
verifyResult, challenge
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const results = [{
|
|
59
|
+
verified,
|
|
60
|
+
verificationMethod,
|
|
61
|
+
controller,
|
|
62
|
+
verifyResult,
|
|
63
|
+
presentation
|
|
64
|
+
}];
|
|
65
|
+
return {verified, controller, results, presentation};
|
|
66
|
+
} catch(error) {
|
|
67
|
+
return {verified: false, error};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function _verifyJwt({jwt, proofPurpose, audience} = {}) {
|
|
72
|
+
let verificationMethod;
|
|
73
|
+
let controller;
|
|
74
|
+
// `resolveKey` is passed `protectedHeader`
|
|
75
|
+
const resolveKey = async ({alg, kid}) => {
|
|
76
|
+
const isEcdsa = ECDSA_ALGS.includes(alg);
|
|
77
|
+
const isEddsa = !isEcdsa && EDDSA_ALGS.includes(alg);
|
|
78
|
+
if(!(isEcdsa || isEddsa)) {
|
|
79
|
+
throw new BedrockError(
|
|
80
|
+
`Unsupported JWT "alg": "${alg}".`, {
|
|
81
|
+
name: 'DataError',
|
|
82
|
+
details: {
|
|
83
|
+
httpStatusCode: 400,
|
|
84
|
+
public: true
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const vm = await didIo.get({url: kid});
|
|
90
|
+
// `vm.controller` must be the issuer of the JWT; also ensure that
|
|
91
|
+
// the specified controller authorized `vm` for the given proof purpose
|
|
92
|
+
({controller} = vm);
|
|
93
|
+
verificationMethod = vm;
|
|
94
|
+
const didDoc = await didIo.get({url: controller});
|
|
95
|
+
let match = didDoc?.authentication?.find?.(
|
|
96
|
+
e => e === vm.id || e.id === vm.id);
|
|
97
|
+
if(typeof match === 'string') {
|
|
98
|
+
match = didDoc?.verificationMethod?.find?.(e => e.id === vm.id);
|
|
99
|
+
}
|
|
100
|
+
if(!(match && Array.isArray(match.controller) ?
|
|
101
|
+
match.controller.includes(vm.controller) :
|
|
102
|
+
match.controller === vm.controller)) {
|
|
103
|
+
throw new BedrockError(
|
|
104
|
+
`Verification method controller "${controller}" did not authorize ` +
|
|
105
|
+
`verification method "${vm.id}" for the purpose ` +
|
|
106
|
+
`of "${proofPurpose}".`, {
|
|
107
|
+
name: 'DataError',
|
|
108
|
+
details: {
|
|
109
|
+
httpStatusCode: 400,
|
|
110
|
+
public: true
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
let jwk;
|
|
115
|
+
if(isEcdsa) {
|
|
116
|
+
const keyPair = await EcdsaMultikey.from(vm);
|
|
117
|
+
jwk = await EcdsaMultikey.toJwk({keyPair});
|
|
118
|
+
jwk.alg = alg;
|
|
119
|
+
} else {
|
|
120
|
+
const keyPair = await Ed25519Multikey.from(vm);
|
|
121
|
+
jwk = await Ed25519Multikey.toJwk({keyPair});
|
|
122
|
+
jwk.alg = 'EdDSA';
|
|
123
|
+
}
|
|
124
|
+
return importJWK(jwk);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// FIXME: enable allowed algorithms to be configurable per instance
|
|
128
|
+
const allowedAlgorithms = ['EdDSA', 'Ed25519', 'ES256', 'ES256K', 'ES384'];
|
|
129
|
+
// FIXME: enable `maxClockSkew` to be configurable per instance
|
|
130
|
+
// default is 300 secs
|
|
131
|
+
const maxClockSkew = 300;
|
|
132
|
+
|
|
133
|
+
// use `jose` lib (for now) to verify JWT and return `payload`;
|
|
134
|
+
// pass optional supported algorithms as allow list ... note
|
|
135
|
+
// that `jose` *always* prohibits the `none` algorithm
|
|
136
|
+
let verifyResult;
|
|
137
|
+
try {
|
|
138
|
+
// `jwtVerify` checks claims: `aud`, `exp`, `nbf`
|
|
139
|
+
const {payload, protectedHeader} = await jwtVerify(jwt, resolveKey, {
|
|
140
|
+
algorithms: allowedAlgorithms,
|
|
141
|
+
clockTolerance: maxClockSkew,
|
|
142
|
+
audience
|
|
143
|
+
});
|
|
144
|
+
verifyResult = {payload, protectedHeader};
|
|
145
|
+
} catch(e) {
|
|
146
|
+
const details = {
|
|
147
|
+
httpStatusCode: 403,
|
|
148
|
+
public: true,
|
|
149
|
+
code: e.code,
|
|
150
|
+
reason: e.message
|
|
151
|
+
};
|
|
152
|
+
if(e.claim) {
|
|
153
|
+
details.claim = e.claim;
|
|
154
|
+
}
|
|
155
|
+
throw new BedrockError('JWT validation failed.', {
|
|
156
|
+
name: 'DataError',
|
|
157
|
+
details
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// check `iss` claim
|
|
162
|
+
if(!(controller && verifyResult?.payload?.iss === controller)) {
|
|
163
|
+
throw new BedrockError('JWT validation failed.', {
|
|
164
|
+
name: 'DataError',
|
|
165
|
+
details: {
|
|
166
|
+
httpStatusCode: 400,
|
|
167
|
+
public: true,
|
|
168
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
169
|
+
reason: 'unexpected "iss" claim value.',
|
|
170
|
+
claim: 'iss'
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {verified: true, verificationMethod, controller, verifyResult};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function _jwtPayloadToCredential({verifyResult} = {}) {
|
|
179
|
+
/* Example:
|
|
180
|
+
{
|
|
181
|
+
"alg": <signer.algorithm>,
|
|
182
|
+
"kid": <signer.id>
|
|
183
|
+
}.
|
|
184
|
+
{
|
|
185
|
+
"iss": <verifiableCredential.issuer>,
|
|
186
|
+
"jti": <verifiableCredential.id>
|
|
187
|
+
"sub": <verifiableCredential.credentialSubject>
|
|
188
|
+
"nbf": <verifiableCredential.[issuanceDate | validFrom]>
|
|
189
|
+
"exp": <verifiableCredential.[expirationDate | validUntil]>
|
|
190
|
+
"vc": <verifiableCredential>
|
|
191
|
+
}
|
|
192
|
+
*/
|
|
193
|
+
const {vc} = verifyResult.payload;
|
|
194
|
+
if(!(vc && typeof vc === 'object')) {
|
|
195
|
+
throw new BedrockError('JWT validation failed.', {
|
|
196
|
+
name: 'DataError',
|
|
197
|
+
details: {
|
|
198
|
+
httpStatusCode: 400,
|
|
199
|
+
public: true,
|
|
200
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
201
|
+
reason: 'missing or unexpected "vc" claim value.',
|
|
202
|
+
claim: 'vc'
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let {'@context': context = []} = vc;
|
|
208
|
+
if(!Array.isArray(context)) {
|
|
209
|
+
context = [context];
|
|
210
|
+
}
|
|
211
|
+
const isVersion1 = context.includes(VC_CONTEXT_1);
|
|
212
|
+
const isVersion2 = context.includes(VC_CONTEXT_2);
|
|
213
|
+
if(!(isVersion1 ^ isVersion2)) {
|
|
214
|
+
throw new BedrockError(
|
|
215
|
+
'Verifiable credential is neither version "1.x" nor "2.x".', {
|
|
216
|
+
name: 'DataError',
|
|
217
|
+
details: {
|
|
218
|
+
httpStatusCode: 400,
|
|
219
|
+
public: true
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const credential = {...vc};
|
|
225
|
+
const {iss, jti, sub, nbf, exp} = verifyResult.payload;
|
|
226
|
+
|
|
227
|
+
// inject `issuer` value
|
|
228
|
+
if(vc.issuer === undefined) {
|
|
229
|
+
vc.issuer = iss;
|
|
230
|
+
} else if(vc.issuer && typeof vc.issuer === 'object' &&
|
|
231
|
+
vc.issuer.id === undefined) {
|
|
232
|
+
vc.issuer.id = iss;
|
|
233
|
+
} else if(iss !== vc.issuer && iss !== vc.issuer?.id) {
|
|
234
|
+
throw new BedrockError(
|
|
235
|
+
'VC-JWT "iss" claim does not equal nor does it exclusively ' +
|
|
236
|
+
'provide verifiable credential "issuer" / "issuer.id".', {
|
|
237
|
+
name: 'DataError',
|
|
238
|
+
details: {
|
|
239
|
+
httpStatusCode: 400,
|
|
240
|
+
public: true
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if(jti !== undefined && jti !== vc.id) {
|
|
246
|
+
// inject `id` value
|
|
247
|
+
if(vc.id === undefined) {
|
|
248
|
+
vc.id = jti;
|
|
249
|
+
} else {
|
|
250
|
+
throw new BedrockError(
|
|
251
|
+
'VC-JWT "jti" claim does not equal nor does it exclusively ' +
|
|
252
|
+
'provide verifiable credential "id".', {
|
|
253
|
+
name: 'DataError',
|
|
254
|
+
details: {
|
|
255
|
+
httpStatusCode: 400,
|
|
256
|
+
public: true
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if(sub !== undefined && sub !== vc.credentialSubject?.id) {
|
|
263
|
+
// inject `credentialSubject.id` value
|
|
264
|
+
if(!vc.credentialSubject) {
|
|
265
|
+
throw new BedrockError(
|
|
266
|
+
'Verifiable credential has no "credentialSubject".', {
|
|
267
|
+
name: 'DataError',
|
|
268
|
+
details: {
|
|
269
|
+
httpStatusCode: 400,
|
|
270
|
+
public: true
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if(Array.isArray(vc.credentialSubject)) {
|
|
275
|
+
throw new BedrockError(
|
|
276
|
+
'Verifiable credential has multiple credential subjects, which is ' +
|
|
277
|
+
'not supported in VC-JWT.', {
|
|
278
|
+
name: 'DataError',
|
|
279
|
+
details: {
|
|
280
|
+
httpStatusCode: 400,
|
|
281
|
+
public: true
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if(vc.credentialSubject?.id === undefined) {
|
|
286
|
+
vc.credentialSubject.id = sub;
|
|
287
|
+
} else {
|
|
288
|
+
throw new BedrockError(
|
|
289
|
+
'VC-JWT "sub" claim does not equal nor does it exclusively ' +
|
|
290
|
+
'provide verifiable credential "credentialSubject.id".', {
|
|
291
|
+
name: 'DataError',
|
|
292
|
+
details: {
|
|
293
|
+
httpStatusCode: 400,
|
|
294
|
+
public: true
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if(nbf === undefined && isVersion1) {
|
|
301
|
+
throw new BedrockError('JWT validation failed.', {
|
|
302
|
+
name: 'DataError',
|
|
303
|
+
details: {
|
|
304
|
+
httpStatusCode: 400,
|
|
305
|
+
public: true,
|
|
306
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
307
|
+
reason: 'missing "nbf" claim value.',
|
|
308
|
+
claim: 'nbf'
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if(nbf !== undefined) {
|
|
314
|
+
// fuzzy convert `nbf` into `issuanceDate` / `validFrom`, only require
|
|
315
|
+
// second-level precision
|
|
316
|
+
const dateString = new Date(nbf * 1000).toISOString().slice(0, -5);
|
|
317
|
+
const dateProperty = isVersion1 ? 'issuanceDate' : 'validFrom';
|
|
318
|
+
// inject dateProperty value
|
|
319
|
+
if(vc[dateProperty] === undefined) {
|
|
320
|
+
vc[dateProperty] = dateString + 'Z';
|
|
321
|
+
} else if(!(vc[dateProperty].startsWith(dateString) &&
|
|
322
|
+
vc[dateProperty].endsWith('Z'))) {
|
|
323
|
+
throw new BedrockError(
|
|
324
|
+
'VC-JWT "nbf" claim does not equal nor does it exclusively provide ' +
|
|
325
|
+
`verifiable credential "${dateProperty}".`, {
|
|
326
|
+
name: 'DataError',
|
|
327
|
+
details: {
|
|
328
|
+
httpStatusCode: 400,
|
|
329
|
+
public: true
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if(exp !== undefined) {
|
|
336
|
+
// fuzzy convert `exp` into `expirationDate` / `validUntil`, only require
|
|
337
|
+
// second-level precision
|
|
338
|
+
const dateString = new Date(exp * 1000).toISOString().slice(0, -5);
|
|
339
|
+
const dateProperty = isVersion1 ? 'expirationDate' : 'validUntil';
|
|
340
|
+
// inject dateProperty value
|
|
341
|
+
if(vc[dateProperty] === undefined) {
|
|
342
|
+
vc[dateProperty] = dateString + 'Z';
|
|
343
|
+
} else if(!(vc[dateProperty].startsWith(dateString) &&
|
|
344
|
+
vc[dateProperty].endsWith('Z'))) {
|
|
345
|
+
throw new BedrockError(
|
|
346
|
+
'VC-JWT "exp" claim does not equal nor does it exclusively provide ' +
|
|
347
|
+
`verifiable credential "${dateProperty}".`, {
|
|
348
|
+
name: 'DataError',
|
|
349
|
+
details: {
|
|
350
|
+
httpStatusCode: 400,
|
|
351
|
+
public: true
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return credential;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function _jwtPayloadToPresentation({verifyResult, challenge} = {}) {
|
|
361
|
+
/* Example:
|
|
362
|
+
{
|
|
363
|
+
"alg": <signer.algorithm>,
|
|
364
|
+
"kid": <signer.id>
|
|
365
|
+
}.
|
|
366
|
+
{
|
|
367
|
+
"iss": <verifiablePresentation.holder>,
|
|
368
|
+
"aud": <verifiablePresentation.domain>,
|
|
369
|
+
"nonce": <verifiablePresentation.nonce>,
|
|
370
|
+
"jti": <verifiablePresentation.id>
|
|
371
|
+
"nbf": <verifiablePresentation.[validFrom]>
|
|
372
|
+
"exp": <verifiablePresentation.[validUntil]>
|
|
373
|
+
"vp": <verifiablePresentation>
|
|
374
|
+
}
|
|
375
|
+
*/
|
|
376
|
+
const {vp} = verifyResult.payload;
|
|
377
|
+
if(!(vp && typeof vp === 'object')) {
|
|
378
|
+
throw new BedrockError('JWT validation failed.', {
|
|
379
|
+
name: 'DataError',
|
|
380
|
+
details: {
|
|
381
|
+
httpStatusCode: 400,
|
|
382
|
+
public: true,
|
|
383
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
384
|
+
reason: 'missing or unexpected "vp" claim value.',
|
|
385
|
+
claim: 'vp'
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let {'@context': context = []} = vp;
|
|
391
|
+
if(!Array.isArray(context)) {
|
|
392
|
+
context = [context];
|
|
393
|
+
}
|
|
394
|
+
const isVersion1 = context.includes(VC_CONTEXT_1);
|
|
395
|
+
const isVersion2 = context.includes(VC_CONTEXT_2);
|
|
396
|
+
if(!(isVersion1 ^ isVersion2)) {
|
|
397
|
+
throw new BedrockError(
|
|
398
|
+
'Verifiable presentation is not either version "1.x" or "2.x".', {
|
|
399
|
+
name: 'DataError',
|
|
400
|
+
details: {
|
|
401
|
+
httpStatusCode: 400,
|
|
402
|
+
public: true
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const presentation = {...vp};
|
|
408
|
+
const {iss, nonce, jti, nbf, exp} = verifyResult.payload;
|
|
409
|
+
|
|
410
|
+
// inject `holder` value
|
|
411
|
+
if(vp.holder === undefined) {
|
|
412
|
+
vp.holder = iss;
|
|
413
|
+
} else if(vp.holder && typeof vp.holder === 'object' &&
|
|
414
|
+
vp.holder.id === undefined) {
|
|
415
|
+
vp.holder.id = iss;
|
|
416
|
+
} else if(iss !== vp.holder && iss !== vp.holder?.id) {
|
|
417
|
+
throw new BedrockError(
|
|
418
|
+
'VC-JWT "iss" claim does not equal nor does it exclusively ' +
|
|
419
|
+
'provide verifiable presentation "holder" / "holder.id".', {
|
|
420
|
+
name: 'DataError',
|
|
421
|
+
details: {
|
|
422
|
+
httpStatusCode: 400,
|
|
423
|
+
public: true
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if(jti !== undefined && jti !== vp.id) {
|
|
429
|
+
// inject `id` value
|
|
430
|
+
if(vp.id === undefined) {
|
|
431
|
+
vp.id = jti;
|
|
432
|
+
} else {
|
|
433
|
+
throw new BedrockError(
|
|
434
|
+
'VC-JWT "jti" claim does not equal nor does it exclusively ' +
|
|
435
|
+
'provide verifiable presentation "id".', {
|
|
436
|
+
name: 'DataError',
|
|
437
|
+
details: {
|
|
438
|
+
httpStatusCode: 400,
|
|
439
|
+
public: true
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// version 1.x VPs do not support `validFrom`/`validUntil`
|
|
446
|
+
if(nbf !== undefined && isVersion2) {
|
|
447
|
+
// fuzzy convert `nbf` into `validFrom`, only require
|
|
448
|
+
// second-level precision
|
|
449
|
+
const dateString = new Date(nbf * 1000).toISOString().slice(0, -5);
|
|
450
|
+
|
|
451
|
+
// inject `validFrom` value
|
|
452
|
+
if(vp.validFrom === undefined) {
|
|
453
|
+
vp.validFrom = dateString + 'Z';
|
|
454
|
+
} else if(!(vp.validFrom?.startsWith(dateString) &&
|
|
455
|
+
vp.validFrom.endsWith('Z'))) {
|
|
456
|
+
throw new BedrockError(
|
|
457
|
+
'VC-JWT "nbf" claim does not equal nor does it exclusively provide ' +
|
|
458
|
+
'verifiable presentation "validFrom".', {
|
|
459
|
+
name: 'DataError',
|
|
460
|
+
details: {
|
|
461
|
+
httpStatusCode: 400,
|
|
462
|
+
public: true
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if(exp !== undefined && isVersion2) {
|
|
468
|
+
// fuzzy convert `exp` into `validUntil`, only require
|
|
469
|
+
// second-level precision
|
|
470
|
+
const dateString = new Date(exp * 1000).toISOString().slice(0, -5);
|
|
471
|
+
|
|
472
|
+
// inject `validUntil` value
|
|
473
|
+
if(vp.validUntil === undefined) {
|
|
474
|
+
vp.validUntil = dateString + 'Z';
|
|
475
|
+
} else if(!(vp.validUntil?.startsWith(dateString) &&
|
|
476
|
+
vp.validUntil?.endsWith('Z'))) {
|
|
477
|
+
throw new BedrockError(
|
|
478
|
+
'VC-JWT "exp" claim does not equal nor does it exclusively provide ' +
|
|
479
|
+
'verifiable presentation "validUntil".', {
|
|
480
|
+
name: 'DataError',
|
|
481
|
+
details: {
|
|
482
|
+
httpStatusCode: 400,
|
|
483
|
+
public: true
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if(challenge !== undefined && nonce !== challenge) {
|
|
490
|
+
throw new BedrockError('JWT validation failed.', {
|
|
491
|
+
name: 'DataError',
|
|
492
|
+
details: {
|
|
493
|
+
httpStatusCode: 400,
|
|
494
|
+
public: true,
|
|
495
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
496
|
+
reason: 'missing or unexpected "nonce" claim value.',
|
|
497
|
+
claim: 'nonce'
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// do some validation on `verifiableCredential`
|
|
503
|
+
let {verifiableCredential = []} = presentation;
|
|
504
|
+
if(!Array.isArray(verifiableCredential)) {
|
|
505
|
+
verifiableCredential = [verifiableCredential];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ensure version 2 VPs only have objects in `verifiableCredential`
|
|
509
|
+
const hasVCJWTs = verifiableCredential.some(vc => typeof vc !== 'object');
|
|
510
|
+
if(isVersion2 && hasVCJWTs) {
|
|
511
|
+
throw new BedrockError(
|
|
512
|
+
'Version 2.x verifiable presentations must only use objects in the ' +
|
|
513
|
+
'"verifiableCredential" field.', {
|
|
514
|
+
name: 'DataError',
|
|
515
|
+
details: {
|
|
516
|
+
httpStatusCode: 400,
|
|
517
|
+
public: true
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// transform any VC-JWT VCs to enveloped VCs
|
|
523
|
+
if(presentation.verifiableCredential && hasVCJWTs) {
|
|
524
|
+
presentation.verifiableCredential = verifiableCredential.map(vc => {
|
|
525
|
+
if(typeof vc !== 'string') {
|
|
526
|
+
return vc;
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
'@context': VC_CONTEXT_2,
|
|
530
|
+
id: `data:application/jwt,${vc}`,
|
|
531
|
+
type: 'EnvelopedVerifiableCredential',
|
|
532
|
+
};
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return presentation;
|
|
537
|
+
}
|
package/lib/verify.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as di from './di.js';
|
|
5
|
+
import {
|
|
6
|
+
verifyEnvelopedCredential, verifyEnvelopedPresentation
|
|
7
|
+
} from './envelopes.js';
|
|
8
|
+
|
|
9
|
+
export async function verifyCredential({config, credential, checks} = {}) {
|
|
10
|
+
if(credential?.type !== 'EnvelopedVerifiableCredential') {
|
|
11
|
+
return di.verifyCredential({config, credential, checks});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const result = await verifyEnvelopedCredential({
|
|
15
|
+
envelopedCredential: credential, checks
|
|
16
|
+
});
|
|
17
|
+
// if the credential has a `proof` field, do DI verification
|
|
18
|
+
let {verified} = result;
|
|
19
|
+
if(verified && result.credential.proof) {
|
|
20
|
+
const proofResult = await di.verifyCredential({
|
|
21
|
+
config, credential: result.credential, checks
|
|
22
|
+
});
|
|
23
|
+
result.proofResult = proofResult;
|
|
24
|
+
verified = verified && proofResult.verified;
|
|
25
|
+
}
|
|
26
|
+
return {...result, verified};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function verifyPresentation({
|
|
30
|
+
config, presentation, challenge, domain, checks
|
|
31
|
+
} = {}) {
|
|
32
|
+
if(presentation?.type !== 'EnvelopedVerifiablePresentation') {
|
|
33
|
+
return di.verifyPresentation({
|
|
34
|
+
config, presentation, challenge, domain, checks
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const presentationResult = await verifyEnvelopedPresentation({
|
|
39
|
+
envelopedPresentation: presentation, challenge, domain
|
|
40
|
+
});
|
|
41
|
+
// verify each `verifiableCredential` in the resulting VP
|
|
42
|
+
let verified = presentationResult.verified;
|
|
43
|
+
let credentialResults;
|
|
44
|
+
if(!verified) {
|
|
45
|
+
credentialResults = [];
|
|
46
|
+
} else {
|
|
47
|
+
// if the presentation has a `proof` field, do DI verification, but
|
|
48
|
+
// note that the presentation itself may verify but the VCs therein might
|
|
49
|
+
// not because some of them might be enveloped VCs and the underlying
|
|
50
|
+
// `vc` library doesn't support this; therefore only use the presentation
|
|
51
|
+
// result and let the code below check VCs to ensure any enveloped VCs
|
|
52
|
+
// will also be checked
|
|
53
|
+
if(presentationResult.presentation.proof) {
|
|
54
|
+
const proofResult = await di.verifyPresentation({
|
|
55
|
+
config, presentation: presentationResult.presentation,
|
|
56
|
+
challenge, domain, checks
|
|
57
|
+
});
|
|
58
|
+
presentationResult.proofResult = proofResult;
|
|
59
|
+
verified = !!(verified && proofResult.presentationResult?.verified);
|
|
60
|
+
if(proofResult.verified) {
|
|
61
|
+
// the whole VP was verified, so include the credential results, no
|
|
62
|
+
// need to repeat below to ensure enveloped credentials are checked
|
|
63
|
+
// as there aren't any
|
|
64
|
+
credentialResults = proofResult.credentialResults;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if(!credentialResults) {
|
|
69
|
+
// verify each VC in the VP
|
|
70
|
+
let {verifiableCredential = []} = presentationResult.presentation;
|
|
71
|
+
if(!Array.isArray(verifiableCredential)) {
|
|
72
|
+
verifiableCredential = [verifiableCredential];
|
|
73
|
+
}
|
|
74
|
+
credentialResults = await Promise.all(verifiableCredential.map(
|
|
75
|
+
credential => verifyCredential({config, credential, checks})));
|
|
76
|
+
verified = verified && credentialResults.every(
|
|
77
|
+
({verified}) => verified);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
...presentationResult,
|
|
82
|
+
verified,
|
|
83
|
+
presentationResult,
|
|
84
|
+
credentialResults
|
|
85
|
+
};
|
|
86
|
+
}
|