@bedrock/vc-delivery 7.11.1 → 7.11.3
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/ExchangeProcessor.js +556 -0
- package/lib/helpers.js +14 -3
- package/lib/inviteRequest/inviteRequest.js +22 -72
- package/lib/issue.js +35 -23
- package/lib/oid4/authorizationRequest.js +1 -1
- package/lib/oid4/oid4vci.js +59 -81
- package/lib/oid4/oid4vp.js +146 -226
- package/lib/vcapi.js +15 -230
- package/lib/verify.js +10 -8
- package/package.json +1 -1
package/lib/vcapi.js
CHANGED
|
@@ -3,17 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import * as exchanges from './storage/exchanges.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
buildPresentationFromResults,
|
|
9
|
-
buildVerifyPresentationResults,
|
|
10
|
-
emitExchangeUpdated,
|
|
11
|
-
evaluateExchangeStep,
|
|
12
|
-
generateRandom, validateVerifiablePresentation
|
|
13
|
-
} from './helpers.js';
|
|
6
|
+
import {evaluateExchangeStep, generateRandom} from './helpers.js';
|
|
14
7
|
import {exportJWK, generateKeyPair, importJWK} from 'jose';
|
|
15
|
-
import {
|
|
16
|
-
import {logger} from './logger.js';
|
|
8
|
+
import {ExchangeProcessor} from './ExchangeProcessor.js';
|
|
17
9
|
|
|
18
10
|
// supported protocols
|
|
19
11
|
import * as inviteRequest from './inviteRequest/inviteRequest.js';
|
|
@@ -22,7 +14,6 @@ import * as oid4vp from './oid4/oid4vp.js';
|
|
|
22
14
|
|
|
23
15
|
const {util: {BedrockError}} = bedrock;
|
|
24
16
|
|
|
25
|
-
const MAXIMUM_STEPS = 100;
|
|
26
17
|
const FIFTEEN_MINUTES = 60 * 15;
|
|
27
18
|
|
|
28
19
|
// 48 hours; make configurable
|
|
@@ -112,36 +103,13 @@ export async function getProtocols({req} = {}) {
|
|
|
112
103
|
}
|
|
113
104
|
|
|
114
105
|
export async function processExchange({req, res, workflow, exchangeRecord}) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
let step;
|
|
106
|
+
// get any `verifiablePresentation` from the body...
|
|
107
|
+
const receivedPresentation = req?.body?.verifiablePresentation;
|
|
118
108
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// process exchange step(s)
|
|
124
|
-
let i = 0;
|
|
125
|
-
let currentStep = exchange.step;
|
|
126
|
-
while(true) {
|
|
127
|
-
if(i++ > MAXIMUM_STEPS) {
|
|
128
|
-
throw new BedrockError('Maximum steps exceeded.', {
|
|
129
|
-
name: 'DataError',
|
|
130
|
-
details: {httpStatusCode: 500, public: true}
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// no step present, break out to complete exchange
|
|
135
|
-
if(!currentStep) {
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// get current step details
|
|
140
|
-
step = await evaluateExchangeStep({
|
|
141
|
-
workflow, exchange, stepName: currentStep
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// if step does not support VCAPI, throw
|
|
109
|
+
// use exchange processor to generate a response
|
|
110
|
+
const exchangeProcessor = new ExchangeProcessor({
|
|
111
|
+
workflow, exchangeRecord,
|
|
112
|
+
prepareStep({workflow, step}) {
|
|
145
113
|
if(!_supportsVcApi({workflow, step})) {
|
|
146
114
|
throw new BedrockError(
|
|
147
115
|
'VC API protocol not supported by this exchange.', {
|
|
@@ -149,198 +117,15 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
|
|
|
149
117
|
details: {httpStatusCode: 400, public: true}
|
|
150
118
|
});
|
|
151
119
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
throw new BedrockError('Cyclical step detected.', {
|
|
156
|
-
name: 'DataError',
|
|
157
|
-
details: {httpStatusCode: 500, public: true}
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// handle VPR: if step requires it, then `verifiablePresentation` must
|
|
162
|
-
// be in the request
|
|
163
|
-
if(step.verifiablePresentationRequest) {
|
|
164
|
-
const {createChallenge} = step;
|
|
165
|
-
const isInitialStep = exchange.step === workflow.initialStep;
|
|
166
|
-
|
|
167
|
-
// if no presentation was received in the body...
|
|
168
|
-
if(!receivedPresentation) {
|
|
169
|
-
const verifiablePresentationRequest = structuredClone(
|
|
170
|
-
step.verifiablePresentationRequest);
|
|
171
|
-
if(createChallenge) {
|
|
172
|
-
/* Note: When creating a challenge, the initial step always
|
|
173
|
-
uses the local exchange ID because the initial step itself
|
|
174
|
-
is one-time use. Subsequent steps, which only VC-API (as opposed
|
|
175
|
-
to other protocols) supports creating additional challenges via
|
|
176
|
-
the VC-API verifier API. */
|
|
177
|
-
let challenge;
|
|
178
|
-
if(isInitialStep) {
|
|
179
|
-
challenge = exchange.id;
|
|
180
|
-
} else {
|
|
181
|
-
// generate a new challenge using verifier API
|
|
182
|
-
({challenge} = await _createChallenge({workflow}));
|
|
183
|
-
}
|
|
184
|
-
verifiablePresentationRequest.challenge = challenge;
|
|
185
|
-
}
|
|
186
|
-
// send VPR and return
|
|
187
|
-
res.json({verifiablePresentationRequest});
|
|
188
|
-
// if exchange is pending, mark it as active out-of-band
|
|
189
|
-
if(exchange.state === 'pending') {
|
|
190
|
-
exchange.state = 'active';
|
|
191
|
-
exchange.sequence++;
|
|
192
|
-
exchanges.update({workflowId: workflow.id, exchange}).catch(
|
|
193
|
-
error => logger.error(
|
|
194
|
-
'Could not mark exchange active: ' + error.message, {error}));
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const {presentationSchema} = step;
|
|
200
|
-
|
|
201
|
-
const isEnvelopedVP =
|
|
202
|
-
receivedPresentation?.type === 'EnvelopedVerifiablePresentation';
|
|
203
|
-
|
|
204
|
-
if(presentationSchema && !isEnvelopedVP) {
|
|
205
|
-
validateVerifiablePresentation({
|
|
206
|
-
schema: presentationSchema.jsonSchema,
|
|
207
|
-
presentation: receivedPresentation
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// verify the received VP
|
|
212
|
-
const expectedChallenge = isInitialStep ? exchange.id : undefined;
|
|
213
|
-
const {
|
|
214
|
-
allowUnprotectedPresentation = false,
|
|
215
|
-
verifyPresentationOptions = {},
|
|
216
|
-
verifyPresentationResultSchema
|
|
217
|
-
} = step;
|
|
218
|
-
const verifyResult = await verify({
|
|
219
|
-
workflow,
|
|
220
|
-
verifyPresentationOptions,
|
|
221
|
-
verifyPresentationResultSchema,
|
|
222
|
-
verifiablePresentationRequest: step.verifiablePresentationRequest,
|
|
223
|
-
presentation: receivedPresentation,
|
|
224
|
-
allowUnprotectedPresentation,
|
|
225
|
-
expectedChallenge
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// validate enveloped VP after verification
|
|
229
|
-
if(presentationSchema && isEnvelopedVP) {
|
|
230
|
-
validateVerifiablePresentation({
|
|
231
|
-
schema: presentationSchema.jsonSchema,
|
|
232
|
-
presentation: verifyResult?.presentationResult?.presentation ?? {}
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// store VP results in variables associated with current step
|
|
237
|
-
if(!exchange.variables.results) {
|
|
238
|
-
exchange.variables.results = {};
|
|
239
|
-
}
|
|
240
|
-
const {verificationMethod} = verifyResult;
|
|
241
|
-
const result = {
|
|
242
|
-
// common use case of DID Authentication; provide `did` for ease
|
|
243
|
-
// of use in templates and consistency with OID4VCI which only
|
|
244
|
-
// receives `did` not verification method nor VP
|
|
245
|
-
did: verificationMethod?.controller || null,
|
|
246
|
-
verificationMethod,
|
|
247
|
-
verifiablePresentation: buildPresentationFromResults({
|
|
248
|
-
presentation: receivedPresentation,
|
|
249
|
-
verifyResult
|
|
250
|
-
}),
|
|
251
|
-
verifyPresentationResults: buildVerifyPresentationResults({
|
|
252
|
-
verifyResult
|
|
253
|
-
})
|
|
254
|
-
};
|
|
255
|
-
exchange.variables.results[currentStep] = result;
|
|
256
|
-
|
|
257
|
-
// clear received presentation as it has been processed
|
|
258
|
-
receivedPresentation = null;
|
|
259
|
-
|
|
260
|
-
// if there is no next step, break out to complete exchange
|
|
261
|
-
if(!step.nextStep) {
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// FIXME: remove this once the other FIXME below is implemented
|
|
266
|
-
// and provides support for issuance in non-last step
|
|
267
|
-
if(step.verifiablePresentation || step.issueRequests?.length > 0) {
|
|
268
|
-
throw new BedrockError(
|
|
269
|
-
'Invalid step detected; continuing exchanges currently must ' +
|
|
270
|
-
'only issue in the final step.', {
|
|
271
|
-
name: 'DataError',
|
|
272
|
-
details: {httpStatusCode: 500, public: true}
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// update the exchange to go to the next step, then loop to send
|
|
277
|
-
// next VPR
|
|
278
|
-
exchange.step = step.nextStep;
|
|
279
|
-
// ensure exchange state is active
|
|
280
|
-
if(exchange.state === 'pending') {
|
|
281
|
-
exchange.state = 'active';
|
|
282
|
-
}
|
|
283
|
-
try {
|
|
284
|
-
exchange.sequence++;
|
|
285
|
-
await exchanges.update({workflowId: workflow.id, exchange});
|
|
286
|
-
await emitExchangeUpdated({workflow, exchange, step});
|
|
287
|
-
lastUpdated = Date.now();
|
|
288
|
-
} catch(e) {
|
|
289
|
-
exchange.sequence--;
|
|
290
|
-
throw e;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// FIXME: there may be VCs to issue during this step, do so before
|
|
294
|
-
// sending the VPR above and remove error that prevents continuing
|
|
295
|
-
// exchanges that issue
|
|
296
|
-
}
|
|
297
|
-
currentStep = step.nextStep;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// issue any VCs that are to be stored in the exchange (`result` is set)
|
|
301
|
-
await issue({workflow, exchange, step, filter: params => params.result});
|
|
302
|
-
|
|
303
|
-
// mark exchange complete
|
|
304
|
-
exchange.state = 'complete';
|
|
305
|
-
try {
|
|
306
|
-
exchange.sequence++;
|
|
307
|
-
await exchanges.complete({workflowId: workflow.id, exchange});
|
|
308
|
-
await emitExchangeUpdated({workflow, exchange, step});
|
|
309
|
-
} catch(e) {
|
|
310
|
-
exchange.sequence--;
|
|
311
|
-
throw e;
|
|
312
|
-
}
|
|
313
|
-
lastUpdated = Date.now();
|
|
314
|
-
|
|
315
|
-
// FIXME: decide what the best recovery path is if delivery fails (but no
|
|
316
|
-
// replay attack detected) after exchange has been marked complete
|
|
317
|
-
|
|
318
|
-
// issue any VCs; may return an empty response if the step defines no
|
|
319
|
-
// VCs to issue
|
|
320
|
-
const {response} = await issue({workflow, exchange, step});
|
|
321
|
-
|
|
322
|
-
// if last `step` has a redirect URL, include it in the response
|
|
323
|
-
if(step?.redirectUrl) {
|
|
324
|
-
response.redirectUrl = step.redirectUrl;
|
|
120
|
+
},
|
|
121
|
+
inputRequired({step}) {
|
|
122
|
+
return step.verifiablePresentationRequest && !receivedPresentation;
|
|
325
123
|
}
|
|
124
|
+
});
|
|
125
|
+
const response = await exchangeProcessor.process({receivedPresentation});
|
|
326
126
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
} catch(e) {
|
|
330
|
-
if(e.name === 'InvalidStateError') {
|
|
331
|
-
throw e;
|
|
332
|
-
}
|
|
333
|
-
// write last error if exchange hasn't been frequently updated
|
|
334
|
-
const {id: workflowId} = workflow;
|
|
335
|
-
const copy = {...exchange};
|
|
336
|
-
copy.sequence++;
|
|
337
|
-
copy.lastError = e;
|
|
338
|
-
await exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
|
|
339
|
-
.catch(error => logger.error(
|
|
340
|
-
'Could not set last exchange error: ' + error.message, {error}));
|
|
341
|
-
await emitExchangeUpdated({workflow, exchange, step});
|
|
342
|
-
throw e;
|
|
343
|
-
}
|
|
127
|
+
// send response
|
|
128
|
+
res.json(response);
|
|
344
129
|
}
|
|
345
130
|
|
|
346
131
|
async function _initOAuth2({oauth2}) {
|
package/lib/verify.js
CHANGED
|
@@ -31,27 +31,28 @@ export async function createChallenge({workflow} = {}) {
|
|
|
31
31
|
|
|
32
32
|
export async function verify({
|
|
33
33
|
workflow, verifyPresentationOptions, verifiablePresentationRequest,
|
|
34
|
-
presentation, allowUnprotectedPresentation = false,
|
|
34
|
+
presentation, allowUnprotectedPresentation = false,
|
|
35
|
+
expectedChallenge, expectedDomain,
|
|
35
36
|
verifyPresentationResultSchema
|
|
36
37
|
} = {}) {
|
|
37
38
|
// create zcap client for verifying
|
|
38
39
|
const {zcapClient, zcaps} = await getZcapClient({workflow});
|
|
39
40
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
41
|
+
// determine if presentation is secured or not
|
|
42
|
+
const isSecured = presentation?.proof ||
|
|
42
43
|
presentation?.type === 'EnvelopedVerifiablePresentation';
|
|
43
44
|
|
|
44
|
-
const checks = (!
|
|
45
|
-
[] : ['proof'];
|
|
45
|
+
const checks = (!isSecured && allowUnprotectedPresentation) ? [] : ['proof'];
|
|
46
46
|
if(!expectedChallenge) {
|
|
47
47
|
// if no expected challenge, rely on verifier for challenge management
|
|
48
48
|
checks.push('challenge');
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
new URL(workflow.id).origin;
|
|
50
|
+
|
|
51
|
+
// verify presentation
|
|
53
52
|
let result;
|
|
54
53
|
try {
|
|
54
|
+
const domain = expectedDomain ??
|
|
55
|
+
verifiablePresentationRequest?.domain ?? new URL(workflow.id).origin;
|
|
55
56
|
const options = createVerifyOptions({
|
|
56
57
|
verifyPresentationOptions,
|
|
57
58
|
expectedChallenge,
|
|
@@ -60,6 +61,7 @@ export async function verify({
|
|
|
60
61
|
domain,
|
|
61
62
|
checks
|
|
62
63
|
});
|
|
64
|
+
const capability = zcaps.verifyPresentation;
|
|
63
65
|
result = await zcapClient.write({
|
|
64
66
|
capability,
|
|
65
67
|
json: {
|