@bedrock/vc-delivery 7.1.1 → 7.3.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 +53 -0
- package/lib/oid4/oid4vci.js +6 -3
- package/lib/oid4/oid4vp.js +15 -4
- package/lib/vcapi.js +14 -3
- package/lib/verify.js +27 -12
- package/package.json +2 -1
- package/schemas/bedrock-vc-workflow.js +34 -1
package/lib/helpers.js
CHANGED
|
@@ -5,8 +5,10 @@ import * as bedrock from '@bedrock/core';
|
|
|
5
5
|
import * as vcjwt from './vcjwt.js';
|
|
6
6
|
import {decodeId, generateId} from 'bnid';
|
|
7
7
|
import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
|
|
8
|
+
import {httpClient} from '@digitalbazaar/http-client';
|
|
8
9
|
import {httpsAgent} from '@bedrock/https-agent';
|
|
9
10
|
import jsonata from 'jsonata';
|
|
11
|
+
import {logger} from './logger.js';
|
|
10
12
|
import {serializeError} from 'serialize-error';
|
|
11
13
|
import {serviceAgents} from '@bedrock/service-agent';
|
|
12
14
|
import {ZcapClient} from '@digitalbazaar/ezcap';
|
|
@@ -53,6 +55,27 @@ export function buildPresentationFromResults({
|
|
|
53
55
|
return vp;
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
export function emitExchangeUpdated({workflow, exchange, step}) {
|
|
59
|
+
if(!step?.callback?.url) {
|
|
60
|
+
// no-op when there is no callback to notify
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const {url} = step.callback;
|
|
65
|
+
const exchangeId = `${workflow.id}/exchanges/${exchange.id}`;
|
|
66
|
+
httpClient.post(url, {
|
|
67
|
+
agent: httpsAgent,
|
|
68
|
+
json: {
|
|
69
|
+
event: {
|
|
70
|
+
data: {exchangeId}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}).catch(
|
|
74
|
+
error => logger.error(
|
|
75
|
+
'Could not send "exchangeUpdated" push notification: ' +
|
|
76
|
+
error.message, {error}));
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
export async function evaluateTemplate({
|
|
57
80
|
workflow, exchange, typedTemplate, variables
|
|
58
81
|
} = {}) {
|
|
@@ -188,6 +211,36 @@ export function deepEqual(obj1, obj2) {
|
|
|
188
211
|
return true;
|
|
189
212
|
}
|
|
190
213
|
|
|
214
|
+
export function createVerifyOptions({
|
|
215
|
+
verifyPresentationOptions,
|
|
216
|
+
expectedChallenge,
|
|
217
|
+
verifiablePresentationRequest,
|
|
218
|
+
presentation,
|
|
219
|
+
domain,
|
|
220
|
+
checks
|
|
221
|
+
}) {
|
|
222
|
+
// start with `verifyPresentationOptions`, then overwrite as needed
|
|
223
|
+
const options = {...verifyPresentationOptions};
|
|
224
|
+
|
|
225
|
+
// update `checks` with anything additional from `verifyPresentationOptions`
|
|
226
|
+
const checkSet = new Set(checks);
|
|
227
|
+
if(verifyPresentationOptions.checks) {
|
|
228
|
+
Object.entries(verifyPresentationOptions.checks)
|
|
229
|
+
.forEach(([check, enabled]) => enabled && checkSet.add(check));
|
|
230
|
+
}
|
|
231
|
+
options.checks = [...checkSet];
|
|
232
|
+
|
|
233
|
+
// update `challenge`
|
|
234
|
+
options.challenge = expectedChallenge ??
|
|
235
|
+
verifiablePresentationRequest.challenge ??
|
|
236
|
+
presentation?.proof?.challenge;
|
|
237
|
+
|
|
238
|
+
// update `domain`
|
|
239
|
+
options.domain = domain;
|
|
240
|
+
|
|
241
|
+
return options;
|
|
242
|
+
}
|
|
243
|
+
|
|
191
244
|
export function stripStacktrace(error) {
|
|
192
245
|
// serialize error and allow-list specific properties
|
|
193
246
|
const serialized = serializeError(error);
|
package/lib/oid4/oid4vci.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022-
|
|
2
|
+
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import * as exchanges from '../exchanges.js';
|
|
6
6
|
import {
|
|
7
|
-
deepEqual,
|
|
7
|
+
deepEqual, emitExchangeUpdated,
|
|
8
|
+
evaluateTemplate, getWorkflowIssuerInstances, validateStep
|
|
8
9
|
} from '../helpers.js';
|
|
9
10
|
import {importJWK, SignJWT} from 'jose';
|
|
10
11
|
import {checkAccessToken} from '@bedrock/oauth2-verifier';
|
|
@@ -397,6 +398,7 @@ function _normalizeCredentialDefinitionTypes({credentialRequests}) {
|
|
|
397
398
|
async function _processExchange({
|
|
398
399
|
req, res, workflow, exchangeRecord, isBatchRequest
|
|
399
400
|
}) {
|
|
401
|
+
let step;
|
|
400
402
|
const {id: workflowId} = workflow;
|
|
401
403
|
const {exchange, meta} = exchangeRecord;
|
|
402
404
|
let {updated: lastUpdated} = meta;
|
|
@@ -428,7 +430,6 @@ async function _processExchange({
|
|
|
428
430
|
});
|
|
429
431
|
|
|
430
432
|
// process exchange step if present
|
|
431
|
-
let step;
|
|
432
433
|
const currentStep = exchange.step;
|
|
433
434
|
if(currentStep) {
|
|
434
435
|
step = workflow.steps[exchange.step];
|
|
@@ -494,6 +495,7 @@ async function _processExchange({
|
|
|
494
495
|
try {
|
|
495
496
|
exchange.sequence++;
|
|
496
497
|
await exchanges.complete({workflowId, exchange});
|
|
498
|
+
emitExchangeUpdated({workflow, exchange, step});
|
|
497
499
|
lastUpdated = Date.now();
|
|
498
500
|
} catch(e) {
|
|
499
501
|
exchange.sequence--;
|
|
@@ -516,6 +518,7 @@ async function _processExchange({
|
|
|
516
518
|
exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
|
|
517
519
|
.catch(error => logger.error(
|
|
518
520
|
'Could not set last exchange error: ' + error.message, {error}));
|
|
521
|
+
emitExchangeUpdated({workflow, exchange, step});
|
|
519
522
|
throw e;
|
|
520
523
|
}
|
|
521
524
|
}
|
package/lib/oid4/oid4vp.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import * as exchanges from '../exchanges.js';
|
|
6
6
|
import {
|
|
7
|
-
buildPresentationFromResults,
|
|
7
|
+
buildPresentationFromResults, emitExchangeUpdated,
|
|
8
|
+
evaluateTemplate, unenvelopePresentation,
|
|
8
9
|
validateStep
|
|
9
10
|
} from '../helpers.js';
|
|
10
11
|
import {
|
|
@@ -141,6 +142,7 @@ export async function getAuthorizationRequest({req}) {
|
|
|
141
142
|
try {
|
|
142
143
|
exchange.sequence++;
|
|
143
144
|
await exchanges.update({workflowId: workflow.id, exchange});
|
|
145
|
+
emitExchangeUpdated({workflow, exchange, step});
|
|
144
146
|
} catch(e) {
|
|
145
147
|
exchange.sequence--;
|
|
146
148
|
if(e.name !== 'InvalidStateError') {
|
|
@@ -168,11 +170,12 @@ export async function processAuthorizationResponse({req}) {
|
|
|
168
170
|
const exchangeRecord = await req.getExchange();
|
|
169
171
|
let {exchange} = exchangeRecord;
|
|
170
172
|
let {meta: {updated: lastUpdated}} = exchangeRecord;
|
|
173
|
+
let step;
|
|
171
174
|
try {
|
|
172
175
|
// get authorization request and updated exchange associated with exchange
|
|
173
176
|
const arRequest = await getAuthorizationRequest({req});
|
|
174
|
-
const {authorizationRequest
|
|
175
|
-
({exchange} = arRequest);
|
|
177
|
+
const {authorizationRequest} = arRequest;
|
|
178
|
+
({exchange, step} = arRequest);
|
|
176
179
|
|
|
177
180
|
// FIXME: check the VP against the presentation submission if requested
|
|
178
181
|
// FIXME: check the VP against "trustedIssuer" in VPR, if provided
|
|
@@ -193,9 +196,15 @@ export async function processAuthorizationResponse({req}) {
|
|
|
193
196
|
// verify the received VP
|
|
194
197
|
const {verifiablePresentationRequest} = await oid4vp.toVpr(
|
|
195
198
|
{authorizationRequest});
|
|
196
|
-
const {
|
|
199
|
+
const {
|
|
200
|
+
allowUnprotectedPresentation = false,
|
|
201
|
+
verifyPresentationOptions = {},
|
|
202
|
+
verifyPresentationResultSchema
|
|
203
|
+
} = step;
|
|
197
204
|
const verifyResult = await verify({
|
|
198
205
|
workflow,
|
|
206
|
+
verifyPresentationOptions,
|
|
207
|
+
verifyPresentationResultSchema,
|
|
199
208
|
verifiablePresentationRequest,
|
|
200
209
|
presentation,
|
|
201
210
|
allowUnprotectedPresentation,
|
|
@@ -244,6 +253,7 @@ export async function processAuthorizationResponse({req}) {
|
|
|
244
253
|
exchange.state = 'complete';
|
|
245
254
|
await exchanges.complete({workflowId: workflow.id, exchange});
|
|
246
255
|
}
|
|
256
|
+
emitExchangeUpdated({workflow, exchange, step});
|
|
247
257
|
lastUpdated = Date.now();
|
|
248
258
|
} catch(e) {
|
|
249
259
|
exchange.sequence--;
|
|
@@ -271,6 +281,7 @@ export async function processAuthorizationResponse({req}) {
|
|
|
271
281
|
exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
|
|
272
282
|
.catch(error => logger.error(
|
|
273
283
|
'Could not set last exchange error: ' + error.message, {error}));
|
|
284
|
+
emitExchangeUpdated({workflow, exchange, step});
|
|
274
285
|
throw e;
|
|
275
286
|
}
|
|
276
287
|
}
|
package/lib/vcapi.js
CHANGED
|
@@ -5,7 +5,8 @@ import * as bedrock from '@bedrock/core';
|
|
|
5
5
|
import * as exchanges from './exchanges.js';
|
|
6
6
|
import {createChallenge as _createChallenge, verify} from './verify.js';
|
|
7
7
|
import {
|
|
8
|
-
buildPresentationFromResults,
|
|
8
|
+
buildPresentationFromResults, emitExchangeUpdated,
|
|
9
|
+
evaluateTemplate, generateRandom,
|
|
9
10
|
unenvelopePresentation, validateStep
|
|
10
11
|
} from './helpers.js';
|
|
11
12
|
import {exportJWK, generateKeyPair, importJWK} from 'jose';
|
|
@@ -93,6 +94,8 @@ export async function createExchange({workflow, exchange}) {
|
|
|
93
94
|
export async function processExchange({req, res, workflow, exchangeRecord}) {
|
|
94
95
|
const {exchange, meta} = exchangeRecord;
|
|
95
96
|
let {updated: lastUpdated} = meta;
|
|
97
|
+
let step;
|
|
98
|
+
|
|
96
99
|
try {
|
|
97
100
|
// get any `verifiablePresentation` from the body...
|
|
98
101
|
let receivedPresentation = req?.body?.verifiablePresentation;
|
|
@@ -100,7 +103,6 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
|
|
|
100
103
|
// process exchange step(s)
|
|
101
104
|
let i = 0;
|
|
102
105
|
let currentStep = exchange.step;
|
|
103
|
-
let step;
|
|
104
106
|
while(true) {
|
|
105
107
|
if(i++ > MAXIMUM_STEPS) {
|
|
106
108
|
throw new BedrockError('Maximum steps exceeded.', {
|
|
@@ -193,9 +195,15 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
|
|
|
193
195
|
|
|
194
196
|
// verify the received VP
|
|
195
197
|
const expectedChallenge = isInitialStep ? exchange.id : undefined;
|
|
196
|
-
const {
|
|
198
|
+
const {
|
|
199
|
+
allowUnprotectedPresentation = false,
|
|
200
|
+
verifyPresentationOptions = {},
|
|
201
|
+
verifyPresentationResultSchema
|
|
202
|
+
} = step;
|
|
197
203
|
const verifyResult = await verify({
|
|
198
204
|
workflow,
|
|
205
|
+
verifyPresentationOptions,
|
|
206
|
+
verifyPresentationResultSchema,
|
|
199
207
|
verifiablePresentationRequest: step.verifiablePresentationRequest,
|
|
200
208
|
presentation: receivedPresentation,
|
|
201
209
|
allowUnprotectedPresentation,
|
|
@@ -249,6 +257,7 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
|
|
|
249
257
|
try {
|
|
250
258
|
exchange.sequence++;
|
|
251
259
|
await exchanges.update({workflowId: workflow.id, exchange});
|
|
260
|
+
emitExchangeUpdated({workflow, exchange, step});
|
|
252
261
|
lastUpdated = Date.now();
|
|
253
262
|
} catch(e) {
|
|
254
263
|
exchange.sequence--;
|
|
@@ -273,6 +282,7 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
|
|
|
273
282
|
try {
|
|
274
283
|
exchange.sequence++;
|
|
275
284
|
await exchanges.complete({workflowId: workflow.id, exchange});
|
|
285
|
+
emitExchangeUpdated({workflow, exchange, step});
|
|
276
286
|
} catch(e) {
|
|
277
287
|
exchange.sequence--;
|
|
278
288
|
throw e;
|
|
@@ -305,6 +315,7 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
|
|
|
305
315
|
exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
|
|
306
316
|
.catch(error => logger.error(
|
|
307
317
|
'Could not set last exchange error: ' + error.message, {error}));
|
|
318
|
+
emitExchangeUpdated({workflow, exchange, step});
|
|
308
319
|
throw e;
|
|
309
320
|
}
|
|
310
321
|
}
|
package/lib/verify.js
CHANGED
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';
|
|
6
6
|
import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
createVerifyOptions,
|
|
9
|
+
getZcapClient,
|
|
10
|
+
stripStacktrace
|
|
11
|
+
} from './helpers.js';
|
|
8
12
|
import {importJWK, jwtVerify} from 'jose';
|
|
13
|
+
import {compile} from '@bedrock/validation';
|
|
9
14
|
import {didIo} from '@bedrock/did-io';
|
|
10
15
|
|
|
11
16
|
const {util: {BedrockError}} = bedrock;
|
|
@@ -25,8 +30,9 @@ export async function createChallenge({workflow} = {}) {
|
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export async function verify({
|
|
28
|
-
workflow,
|
|
29
|
-
allowUnprotectedPresentation = false, expectedChallenge
|
|
33
|
+
workflow, verifyPresentationOptions, verifiablePresentationRequest,
|
|
34
|
+
presentation, allowUnprotectedPresentation = false, expectedChallenge,
|
|
35
|
+
verifyPresentationResultSchema
|
|
30
36
|
} = {}) {
|
|
31
37
|
// create zcap client for verifying
|
|
32
38
|
const {zcapClient, zcaps} = await getZcapClient({workflow});
|
|
@@ -46,17 +52,18 @@ export async function verify({
|
|
|
46
52
|
new URL(workflow.id).origin;
|
|
47
53
|
let result;
|
|
48
54
|
try {
|
|
55
|
+
const options = createVerifyOptions({
|
|
56
|
+
verifyPresentationOptions,
|
|
57
|
+
expectedChallenge,
|
|
58
|
+
verifiablePresentationRequest,
|
|
59
|
+
presentation,
|
|
60
|
+
domain,
|
|
61
|
+
checks
|
|
62
|
+
});
|
|
49
63
|
result = await zcapClient.write({
|
|
50
64
|
capability,
|
|
51
65
|
json: {
|
|
52
|
-
options
|
|
53
|
-
// FIXME: support multi-proof presentations?
|
|
54
|
-
challenge: expectedChallenge ??
|
|
55
|
-
verifiablePresentationRequest.challenge ??
|
|
56
|
-
presentation?.proof?.challenge,
|
|
57
|
-
domain,
|
|
58
|
-
checks
|
|
59
|
-
},
|
|
66
|
+
options,
|
|
60
67
|
verifiablePresentation: presentation
|
|
61
68
|
}
|
|
62
69
|
});
|
|
@@ -120,7 +127,15 @@ export async function verify({
|
|
|
120
127
|
const verificationMethod = presentationResult?.results[0]
|
|
121
128
|
.verificationMethod ?? null;
|
|
122
129
|
|
|
123
|
-
//
|
|
130
|
+
// validate against the verify presentation result schema, if applicable
|
|
131
|
+
if(verifyPresentationResultSchema) {
|
|
132
|
+
const {jsonSchema: schema} = verifyPresentationResultSchema;
|
|
133
|
+
const validate = compile({schema});
|
|
134
|
+
const {valid, error} = validate(result.data);
|
|
135
|
+
if(!valid) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
124
139
|
|
|
125
140
|
return {
|
|
126
141
|
verified,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock/vc-delivery",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bedrock Verifiable Credential Delivery",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"@digitalbazaar/ed25519-multikey": "^1.3.1",
|
|
40
40
|
"@digitalbazaar/ed25519-signature-2020": "^5.4.0",
|
|
41
41
|
"@digitalbazaar/ezcap": "^4.1.0",
|
|
42
|
+
"@digitalbazaar/http-client": "^4.2.0",
|
|
42
43
|
"@digitalbazaar/oid4-client": "^4.3.0",
|
|
43
44
|
"@digitalbazaar/vc": "^7.2.0",
|
|
44
45
|
"assert-plus": "^1.0.0",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022-
|
|
2
|
+
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import {MAX_ISSUER_INSTANCES} from '../lib/constants.js';
|
|
5
5
|
import {schemas} from '@bedrock/validation';
|
|
@@ -381,6 +381,7 @@ const step = {
|
|
|
381
381
|
not: {
|
|
382
382
|
required: [
|
|
383
383
|
'allowUnprotectedPresentation',
|
|
384
|
+
'callback',
|
|
384
385
|
'createChallenge',
|
|
385
386
|
'issueRequests',
|
|
386
387
|
'jwtDidProofRequest',
|
|
@@ -400,6 +401,16 @@ const step = {
|
|
|
400
401
|
allowUnprotectedPresentation: {
|
|
401
402
|
type: 'boolean'
|
|
402
403
|
},
|
|
404
|
+
callback: {
|
|
405
|
+
type: 'object',
|
|
406
|
+
required: ['url'],
|
|
407
|
+
additionalProperties: false,
|
|
408
|
+
properties: {
|
|
409
|
+
url: {
|
|
410
|
+
type: 'string'
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
},
|
|
403
414
|
createChallenge: {
|
|
404
415
|
type: 'boolean'
|
|
405
416
|
},
|
|
@@ -486,6 +497,28 @@ const step = {
|
|
|
486
497
|
stepTemplate: typedTemplate,
|
|
487
498
|
verifiablePresentationRequest: {
|
|
488
499
|
type: 'object'
|
|
500
|
+
},
|
|
501
|
+
verifyPresentationOptions: {
|
|
502
|
+
type: 'object',
|
|
503
|
+
properties: {
|
|
504
|
+
checks: {
|
|
505
|
+
type: 'object'
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
additionalProperties: true
|
|
509
|
+
},
|
|
510
|
+
verifyPresentationResultSchema: {
|
|
511
|
+
type: 'object',
|
|
512
|
+
required: ['type', 'jsonSchema'],
|
|
513
|
+
additionalProperties: false,
|
|
514
|
+
properties: {
|
|
515
|
+
type: {
|
|
516
|
+
type: 'string'
|
|
517
|
+
},
|
|
518
|
+
jsonSchema: {
|
|
519
|
+
type: 'object'
|
|
520
|
+
}
|
|
521
|
+
}
|
|
489
522
|
}
|
|
490
523
|
}
|
|
491
524
|
};
|