@bedrock/vc-delivery 7.11.3 → 7.12.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/ExchangeProcessor.js +52 -31
- package/lib/helpers.js +0 -75
- package/lib/oid4/authorizationRequest.js +10 -2
- package/lib/oid4/authorizationResponse.js +20 -31
- package/lib/oid4/http.js +1 -1
- package/lib/oid4/oid4vp.js +67 -37
- package/package.json +2 -2
- package/schemas/bedrock-vc-workflow.js +8 -0
package/lib/ExchangeProcessor.js
CHANGED
|
@@ -288,10 +288,21 @@ export class ExchangeProcessor {
|
|
|
288
288
|
// 4.2. Call subalgorithm `prepareStep`, passing `workflow`,
|
|
289
289
|
// `exchange`, `step`, `receivedPresentation`, and
|
|
290
290
|
// `receivedPresentationRequest` to perform any protocol-specific
|
|
291
|
-
// custom step preparation.
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
// custom step preparation. If `prepareStep` returns a `prepareResult`
|
|
292
|
+
// with `receivedPresentation` and/or `receivedPresentationRequest` set,
|
|
293
|
+
// then update `receivedPresentation` and/or
|
|
294
|
+
// `receivedPresentationRequest` accordingly.
|
|
295
|
+
const prepareResult = await prepareStep?.({
|
|
296
|
+
workflow, exchange, step,
|
|
297
|
+
receivedPresentation, receivedPresentationRequest
|
|
294
298
|
});
|
|
299
|
+
if(prepareResult?.receivedPresentation !== undefined) {
|
|
300
|
+
receivedPresentation = prepareResult.receivedPresentation;
|
|
301
|
+
}
|
|
302
|
+
if(prepareResult?.receivedPresentationRequest) {
|
|
303
|
+
receivedPresentationRequest =
|
|
304
|
+
prepareResult.receivedPresentationRequest;
|
|
305
|
+
}
|
|
295
306
|
|
|
296
307
|
// 4.3. If `receivedPresentation` is set, then call the
|
|
297
308
|
// `validateReceivedPresentation` sub-algorithm, passing `workflow`,
|
|
@@ -312,20 +323,27 @@ export class ExchangeProcessor {
|
|
|
312
323
|
});
|
|
313
324
|
}
|
|
314
325
|
|
|
315
|
-
// 4.5.
|
|
326
|
+
// 4.5. If the implementation supports blocking callbacks that can
|
|
327
|
+
// return results to be added to exchange variables (or return errors),
|
|
328
|
+
// call the callback and store its results in
|
|
329
|
+
// `exchange.variables.results[exchange.step].callbackResults` or
|
|
330
|
+
// throw any error received.
|
|
331
|
+
// FIXME: to be implemented
|
|
332
|
+
|
|
333
|
+
// 4.6. Set `isInputRequired` to the result of calling
|
|
316
334
|
// `inputRequired({step, receivedPresentation})`.
|
|
317
335
|
const isInputRequired = await inputRequired?.({
|
|
318
336
|
workflow, exchange, step, receivedPresentation
|
|
319
337
|
}) ?? false;
|
|
320
338
|
|
|
321
|
-
// 4.
|
|
339
|
+
// 4.7. If `isInputRequired` is true:
|
|
322
340
|
if(isInputRequired) {
|
|
323
|
-
// 4.
|
|
341
|
+
// 4.7.1. If `response` is `null`, set it to an empty object.
|
|
324
342
|
if(!response) {
|
|
325
343
|
response = {};
|
|
326
344
|
}
|
|
327
345
|
|
|
328
|
-
// 4.
|
|
346
|
+
// 4.7.2. If `step.verifiablePresentationRequest` is set, call
|
|
329
347
|
// the `createVerifiablePresentationRequest` sub-algorithm, passing
|
|
330
348
|
// `workflow`, `exchange`, `step`, and `response`.
|
|
331
349
|
if(step.verifiablePresentationRequest) {
|
|
@@ -334,12 +352,13 @@ export class ExchangeProcessor {
|
|
|
334
352
|
});
|
|
335
353
|
}
|
|
336
354
|
|
|
337
|
-
// 4.
|
|
355
|
+
// 4.7.3. Save the exchange (and call any non-blocking callback
|
|
356
|
+
// in the step) and return `response`.
|
|
338
357
|
await _updateExchange({workflow, exchange, meta, step});
|
|
339
358
|
return response;
|
|
340
359
|
}
|
|
341
360
|
|
|
342
|
-
// 4.
|
|
361
|
+
// 4.8. Set `issueToClient` to `true` if `step.issueRequests` includes
|
|
343
362
|
// any issuer requests for VCs that are to be sent to the client
|
|
344
363
|
// (`issueRequest.result` is NOT set), otherwise set it to `false`.
|
|
345
364
|
const issueRequestsParams = getIssueRequestsParams({
|
|
@@ -347,27 +366,28 @@ export class ExchangeProcessor {
|
|
|
347
366
|
});
|
|
348
367
|
const issueToClient = issueRequestsParams.some(p => !p.result);
|
|
349
368
|
|
|
350
|
-
// 4.
|
|
369
|
+
// 4.9. If `step.verifiablePresentation` is set or `issueToClient` is
|
|
351
370
|
// `true`:
|
|
352
371
|
if(step.verifiablePresentation || issueToClient) {
|
|
353
|
-
// 4.
|
|
372
|
+
// 4.9.1. If `response` is not `null`
|
|
354
373
|
if(response) {
|
|
355
|
-
// 4.
|
|
374
|
+
// 4.9.1.1. If `response.verifiablePresentationRequest` is not set,
|
|
356
375
|
// set it to an empty object (to indicate that the exchange is
|
|
357
376
|
// not yet complete).
|
|
358
377
|
if(!response.verifiablePresentationRequest) {
|
|
359
378
|
response.verifiablePresentationRequest = {};
|
|
360
379
|
}
|
|
361
|
-
// 4.
|
|
380
|
+
// 4.9.1.2. Save the exchange (and call any non-blocking callback
|
|
381
|
+
// in the step).
|
|
362
382
|
await _updateExchange({workflow, exchange, meta, step});
|
|
363
|
-
// 4.
|
|
383
|
+
// 4.9.1.3. Return `response`.
|
|
364
384
|
return response;
|
|
365
385
|
}
|
|
366
386
|
|
|
367
|
-
// 4.
|
|
387
|
+
// 4.9.2. Set `response` to an empty object.
|
|
368
388
|
response = {};
|
|
369
389
|
|
|
370
|
-
// 4.
|
|
390
|
+
// 4.9.3. If `step.verifiablePresentation` is set, set
|
|
371
391
|
// `response.verifiablePresentation` to a copy of it, otherwise
|
|
372
392
|
// set `response.verifiablePresentation` to a new, empty,
|
|
373
393
|
// Verifiable Presentation (using VCDM 2.0 by default, but a custom
|
|
@@ -380,14 +400,14 @@ export class ExchangeProcessor {
|
|
|
380
400
|
// issuance has been triggered
|
|
381
401
|
issuanceTriggered = true;
|
|
382
402
|
|
|
383
|
-
// 4.
|
|
384
|
-
// an error response to the client if any fails (note:
|
|
385
|
-
// can optionally implement failure recovery or retry
|
|
386
|
-
// their own discretion):
|
|
387
|
-
// 4.
|
|
403
|
+
// 4.10. Perform every issue request (optionally in parallel),
|
|
404
|
+
// returning an error response to the client if any fails (note:
|
|
405
|
+
// implementations can optionally implement failure recovery or retry
|
|
406
|
+
// issue requests at their own discretion):
|
|
407
|
+
// 4.10.1. For each issue request where `result` is set to an exchange
|
|
388
408
|
// variable path or name, save the issued credential in the referenced
|
|
389
409
|
// exchange variable.
|
|
390
|
-
// 4.
|
|
410
|
+
// 4.10.2. For each issue request where `result` is not specified, save
|
|
391
411
|
// the issued credential in `response.verifiablePresentation`, i.e.,
|
|
392
412
|
// for a VCDM presentation, append the issued credential to
|
|
393
413
|
// `response.verifiablePresentation.verifiableCredential`.
|
|
@@ -396,44 +416,45 @@ export class ExchangeProcessor {
|
|
|
396
416
|
verifiablePresentation: response?.verifiablePresentation
|
|
397
417
|
});
|
|
398
418
|
|
|
399
|
-
// 4.
|
|
419
|
+
// 4.11. If `response.verifiablePresentation` is set and the step
|
|
400
420
|
// configuration indicates it should be signed, sign the presentation
|
|
401
421
|
// (e.g., by using a VCALM holder instance's `/presentations/create`
|
|
402
422
|
// endpoint).
|
|
403
423
|
// FIXME: implement
|
|
404
424
|
//if(response?.verifiablePresentation) {}
|
|
405
425
|
|
|
406
|
-
// 4.
|
|
426
|
+
// 4.12. If `step.redirectUrl` is set:
|
|
407
427
|
if(step.redirectUrl) {
|
|
408
|
-
// 4.
|
|
428
|
+
// 4.12.1. If `response` is `null` then set it to an empty object.
|
|
409
429
|
if(!response) {
|
|
410
430
|
response = {};
|
|
411
431
|
}
|
|
412
|
-
// 4.
|
|
432
|
+
// 4.12.2. Set `response.redirectUrl` to `step.redirectUrl`.
|
|
413
433
|
response.redirectUrl = step.redirectUrl;
|
|
414
434
|
}
|
|
415
435
|
|
|
416
|
-
// 4.
|
|
436
|
+
// 4.13. If `step.nextStep` is not set then set `exchange.state` to
|
|
417
437
|
// `complete`.
|
|
418
438
|
if(!step.nextStep) {
|
|
419
439
|
exchange.state = 'complete';
|
|
420
440
|
} else {
|
|
421
|
-
// 4.
|
|
441
|
+
// 4.14. Otherwise, delete `exchange.variables.results[step.nextStep]`
|
|
422
442
|
// if it exists, and set `exchange.step` to `step.nextStep`.
|
|
423
443
|
delete exchange.variables.results[step.nextStep];
|
|
424
444
|
exchange.step = step.nextStep;
|
|
425
445
|
}
|
|
426
446
|
|
|
427
|
-
// 4.
|
|
447
|
+
// 4.15. Save the exchange (and call any non-blocking callback in
|
|
448
|
+
// the step).
|
|
428
449
|
await _updateExchange({workflow, exchange, meta, step});
|
|
429
450
|
|
|
430
|
-
// 4.
|
|
451
|
+
// 4.16. If `exchange.state` is `complete`, return `response` if it is
|
|
431
452
|
// not `null`, otherwise return an empty object.
|
|
432
453
|
if(exchange.state === 'complete') {
|
|
433
454
|
return response ?? {};
|
|
434
455
|
}
|
|
435
456
|
|
|
436
|
-
// 4.
|
|
457
|
+
// 4.17. Set `receivedPresentation` to `null`.
|
|
437
458
|
receivedPresentation = null;
|
|
438
459
|
}
|
|
439
460
|
} catch(e) {
|
package/lib/helpers.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
|
-
import * as vcjwt from './vcjwt.js';
|
|
6
5
|
import {decodeId, generateId} from 'bnid';
|
|
7
6
|
import {compile} from '@bedrock/validation';
|
|
8
7
|
import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
|
|
@@ -22,16 +21,6 @@ const ALLOWED_ERROR_KEYS = [
|
|
|
22
21
|
'status'
|
|
23
22
|
];
|
|
24
23
|
|
|
25
|
-
const JWT_FORMAT_ALIASES = new Set([
|
|
26
|
-
'application/jwt',
|
|
27
|
-
'application/vc+jwt',
|
|
28
|
-
'application/vp+jwt',
|
|
29
|
-
'jwt_vp',
|
|
30
|
-
'jwt_vp_json',
|
|
31
|
-
'jwt_vc_json-ld',
|
|
32
|
-
'jwt_vc_json'
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
24
|
export function buildPresentationFromResults({
|
|
36
25
|
presentation, verifyResult
|
|
37
26
|
}) {
|
|
@@ -358,44 +347,6 @@ export function stripStacktrace(error) {
|
|
|
358
347
|
return error;
|
|
359
348
|
}
|
|
360
349
|
|
|
361
|
-
export async function unenvelopeCredential({
|
|
362
|
-
envelopedCredential, format
|
|
363
|
-
} = {}) {
|
|
364
|
-
const result = _getEnvelope({envelope: envelopedCredential, format});
|
|
365
|
-
|
|
366
|
-
// only supported format is VC-JWT at this time
|
|
367
|
-
const credential = vcjwt.decodeVCJWTCredential({jwt: result.envelope});
|
|
368
|
-
return {credential, ...result};
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
export async function unenvelopePresentation({
|
|
372
|
-
envelopedPresentation, format
|
|
373
|
-
} = {}) {
|
|
374
|
-
const result = _getEnvelope({envelope: envelopedPresentation, format});
|
|
375
|
-
|
|
376
|
-
// only supported format is VC-JWT at this time
|
|
377
|
-
const presentation = vcjwt.decodeVCJWTPresentation({jwt: result.envelope});
|
|
378
|
-
|
|
379
|
-
// unenvelope any VCs in the presentation
|
|
380
|
-
let {verifiableCredential = []} = presentation;
|
|
381
|
-
if(!Array.isArray(verifiableCredential)) {
|
|
382
|
-
verifiableCredential = [verifiableCredential];
|
|
383
|
-
}
|
|
384
|
-
if(verifiableCredential.length > 0) {
|
|
385
|
-
presentation.verifiableCredential = await Promise.all(
|
|
386
|
-
verifiableCredential.map(async vc => {
|
|
387
|
-
if(vc?.type !== 'EnvelopedVerifiableCredential') {
|
|
388
|
-
return vc;
|
|
389
|
-
}
|
|
390
|
-
const {credential} = await unenvelopeCredential({
|
|
391
|
-
envelopedCredential: vc
|
|
392
|
-
});
|
|
393
|
-
return credential;
|
|
394
|
-
}));
|
|
395
|
-
}
|
|
396
|
-
return {presentation, ...result};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
350
|
export async function validateStep({step} = {}) {
|
|
400
351
|
// FIXME: use `ajv` and do JSON schema check
|
|
401
352
|
if(Object.keys(step).length === 0) {
|
|
@@ -423,32 +374,6 @@ export async function validateStep({step} = {}) {
|
|
|
423
374
|
}
|
|
424
375
|
}
|
|
425
376
|
|
|
426
|
-
function _getEnvelope({envelope, format}) {
|
|
427
|
-
const isString = typeof envelope === 'string';
|
|
428
|
-
if(isString) {
|
|
429
|
-
// supported formats
|
|
430
|
-
if(JWT_FORMAT_ALIASES.has(format)) {
|
|
431
|
-
format = 'application/jwt';
|
|
432
|
-
}
|
|
433
|
-
} else {
|
|
434
|
-
const {id} = envelope;
|
|
435
|
-
if(id?.startsWith('data:application/jwt,')) {
|
|
436
|
-
format = 'application/jwt';
|
|
437
|
-
envelope = id.slice('data:application/jwt,'.length);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if(format === 'application/jwt' && envelope !== undefined) {
|
|
442
|
-
return {envelope, format};
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
throw new BedrockError(
|
|
446
|
-
`Unsupported credential or presentation envelope format "${format}".`, {
|
|
447
|
-
name: 'NotSupportedError',
|
|
448
|
-
details: {httpStatusCode: 400, public: true}
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
|
|
452
377
|
export function validateVerifiablePresentation({schema, presentation}) {
|
|
453
378
|
const validate = compile({schema});
|
|
454
379
|
const {valid, error} = validate(presentation);
|
|
@@ -12,6 +12,9 @@ import {randomUUID} from 'node:crypto';
|
|
|
12
12
|
|
|
13
13
|
const {util: {BedrockError}} = bedrock;
|
|
14
14
|
|
|
15
|
+
const ENCRYPTED_RESPONSE_MODES = new Set([
|
|
16
|
+
'direct_post.jwt', 'dc_api.jwt', 'dc_api'
|
|
17
|
+
]);
|
|
15
18
|
const OID4VP_JWT_TYP = 'oauth-authz-req+jwt';
|
|
16
19
|
const TEXT_ENCODER = new TextEncoder();
|
|
17
20
|
|
|
@@ -28,6 +31,7 @@ export async function create({
|
|
|
28
31
|
// get params from step OID4VP client profile to apply to the AR
|
|
29
32
|
const {
|
|
30
33
|
client_id, client_id_scheme,
|
|
34
|
+
dcql_query,
|
|
31
35
|
nonce,
|
|
32
36
|
presentation_definition,
|
|
33
37
|
response_mode, response_uri
|
|
@@ -37,6 +41,10 @@ export async function create({
|
|
|
37
41
|
// client_id_scheme (draft versions of OID4VP use this param)
|
|
38
42
|
authorizationRequest.client_id_scheme = client_id_scheme ?? 'redirect_uri';
|
|
39
43
|
|
|
44
|
+
// dcql_query
|
|
45
|
+
authorizationRequest.dcql_query = dcql_query ??
|
|
46
|
+
authorizationRequest.dcql_query;
|
|
47
|
+
|
|
40
48
|
// presentation_definition
|
|
41
49
|
authorizationRequest.presentation_definition = presentation_definition ??
|
|
42
50
|
authorizationRequest.presentation_definition;
|
|
@@ -175,8 +183,8 @@ async function _createClientMetaData({
|
|
|
175
183
|
client_metadata.require_signed_request_object = true;
|
|
176
184
|
}
|
|
177
185
|
|
|
178
|
-
//
|
|
179
|
-
if(authorizationRequest.response_mode
|
|
186
|
+
// offer encryption options for encrypted response modes
|
|
187
|
+
if(ENCRYPTED_RESPONSE_MODES.has(authorizationRequest.response_mode)) {
|
|
180
188
|
// generate ECDH-ES P-256 key
|
|
181
189
|
const kp = await generateKeyPair('ECDH-ES', {
|
|
182
190
|
crv: 'P-256', extractable: true
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
} from '../../schemas/bedrock-vc-workflow.js';
|
|
9
9
|
import {compile} from '@bedrock/validation';
|
|
10
10
|
import {oid4vp} from '@digitalbazaar/oid4-client';
|
|
11
|
-
import {unenvelopePresentation} from '../helpers.js';
|
|
12
11
|
|
|
13
12
|
const {util: {BedrockError}} = bedrock;
|
|
14
13
|
|
|
@@ -27,58 +26,46 @@ bedrock.events.on('bedrock.init', () => {
|
|
|
27
26
|
});
|
|
28
27
|
});
|
|
29
28
|
|
|
30
|
-
export async function parse({
|
|
29
|
+
export async function parse({
|
|
30
|
+
req, exchange, clientProfileId, authorizationRequest
|
|
31
|
+
} = {}) {
|
|
31
32
|
try {
|
|
32
33
|
const {body} = req;
|
|
33
34
|
const {
|
|
34
|
-
responseMode, parsed, protectedHeader
|
|
35
|
+
responseMode, parsed, protectedHeader,
|
|
36
|
+
recipientPublicJwk, recipientPublicJwkThumbprint,
|
|
37
|
+
vpTokenMediaType
|
|
35
38
|
} = await oid4vp.verifier.parseAuthorizationResponse({
|
|
36
39
|
body,
|
|
37
40
|
getDecryptParameters() {
|
|
38
41
|
return _getDecryptParameters({exchange, clientProfileId});
|
|
39
|
-
}
|
|
42
|
+
},
|
|
43
|
+
authorizationRequest
|
|
40
44
|
});
|
|
41
45
|
|
|
42
|
-
// validate parsed presentation submission
|
|
46
|
+
// validate parsed presentation submission if given
|
|
43
47
|
const {presentationSubmission} = parsed;
|
|
44
|
-
|
|
48
|
+
if(presentationSubmission) {
|
|
49
|
+
_validate(VALIDATORS.presentationSubmission, presentationSubmission);
|
|
50
|
+
}
|
|
45
51
|
|
|
46
52
|
// obtain `presentation` and optional `envelope` from parsed `vpToken`
|
|
47
53
|
const {vpToken} = parsed;
|
|
48
54
|
let presentation;
|
|
49
55
|
let envelope;
|
|
50
56
|
|
|
51
|
-
if(
|
|
52
|
-
|
|
53
|
-
})) {
|
|
54
|
-
// `vp_token` is declared to be a base64url-encoded mDL device response
|
|
55
|
-
presentation = {
|
|
56
|
-
'@context': VC_CONTEXT_2,
|
|
57
|
-
id: `data:application/mdl-vp-token,${vpToken}`,
|
|
58
|
-
type: 'EnvelopedVerifiablePresentation'
|
|
59
|
-
};
|
|
60
|
-
} else if(typeof vpToken === 'string') {
|
|
61
|
-
// FIXME: remove unenveloping here and delegate it to VC API verifier;
|
|
62
|
-
// FIXME: check if envelope matches submission once verified
|
|
63
|
-
const {
|
|
64
|
-
envelope: raw, presentation: contents, format
|
|
65
|
-
} = await unenvelopePresentation({
|
|
66
|
-
envelopedPresentation: vpToken,
|
|
67
|
-
// FIXME: check `presentationSubmission` for VP format
|
|
68
|
-
format: 'application/jwt'
|
|
69
|
-
});
|
|
70
|
-
_validate(VALIDATORS.presentation, contents);
|
|
57
|
+
if(vpTokenMediaType !== 'application/vp') {
|
|
58
|
+
// `vp_token` contains some enveloped format
|
|
71
59
|
presentation = {
|
|
72
60
|
'@context': VC_CONTEXT_2,
|
|
73
|
-
id: `data:${
|
|
61
|
+
id: `data:${vpTokenMediaType},${vpToken}`,
|
|
74
62
|
type: 'EnvelopedVerifiablePresentation'
|
|
75
63
|
};
|
|
76
|
-
envelope = {
|
|
64
|
+
envelope = {mediaType: vpTokenMediaType};
|
|
77
65
|
} else {
|
|
78
|
-
// simplest case: `vpToken` is a VP; validate it
|
|
66
|
+
// simplest case: `vpToken` is a VP; validate it against basic schema
|
|
79
67
|
presentation = vpToken;
|
|
80
68
|
_validate(VALIDATORS.presentation, presentation);
|
|
81
|
-
// FIXME: validate VP against presentation submission
|
|
82
69
|
}
|
|
83
70
|
|
|
84
71
|
return {
|
|
@@ -86,7 +73,9 @@ export async function parse({req, exchange, clientProfileId} = {}) {
|
|
|
86
73
|
presentationSubmission,
|
|
87
74
|
presentation,
|
|
88
75
|
envelope,
|
|
89
|
-
protectedHeader
|
|
76
|
+
protectedHeader,
|
|
77
|
+
recipientPublicJwk,
|
|
78
|
+
recipientPublicJwkThumbprint
|
|
90
79
|
};
|
|
91
80
|
} catch(cause) {
|
|
92
81
|
throw new BedrockError(
|
package/lib/oid4/http.js
CHANGED
|
@@ -405,7 +405,7 @@ function _normalizeCredentials({verifiablePresentation}) {
|
|
|
405
405
|
// use raw format for each credential
|
|
406
406
|
const {verifiableCredential} = verifiablePresentation;
|
|
407
407
|
return verifiableCredential.map(vc => {
|
|
408
|
-
// parse any enveloped VC into its non-VC format
|
|
408
|
+
// parse any JWT-enveloped VC into its non-VC format
|
|
409
409
|
if(vc.type === 'EnvelopedVerifiableCredential' &&
|
|
410
410
|
vc.id?.startsWith('data:application/jwt,')) {
|
|
411
411
|
return vc.id.slice('data:application/jwt,'.length);
|
package/lib/oid4/oid4vp.js
CHANGED
|
@@ -138,15 +138,8 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
138
138
|
const {config: workflow} = req.serviceObject;
|
|
139
139
|
const exchangeRecord = await req.getExchange();
|
|
140
140
|
|
|
141
|
-
// ensure authz response can be parsed
|
|
142
|
-
const {
|
|
143
|
-
presentation, envelope, presentationSubmission,
|
|
144
|
-
responseMode, protectedHeader
|
|
145
|
-
} = await parseAuthorizationResponse({
|
|
146
|
-
req, exchange: exchangeRecord.exchange, clientProfileId
|
|
147
|
-
});
|
|
148
|
-
|
|
149
141
|
// process exchange and produce result
|
|
142
|
+
let parseResponseResult;
|
|
150
143
|
const result = {};
|
|
151
144
|
const exchangeProcessor = new ExchangeProcessor({
|
|
152
145
|
workflow, exchangeRecord,
|
|
@@ -156,15 +149,11 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
156
149
|
});
|
|
157
150
|
result.authorizationRequest = authorizationRequest;
|
|
158
151
|
|
|
159
|
-
// ensure response
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
name: 'ConstraintError',
|
|
165
|
-
details: {httpStatusCode: 400, public: true}
|
|
166
|
-
});
|
|
167
|
-
}
|
|
152
|
+
// ensure authz response can be parsed
|
|
153
|
+
parseResponseResult = await parseAuthorizationResponse({
|
|
154
|
+
req, exchange: exchangeRecord.exchange, clientProfileId,
|
|
155
|
+
authorizationRequest
|
|
156
|
+
});
|
|
168
157
|
|
|
169
158
|
// only mark exchange complete if there is nothing to be issued; this
|
|
170
159
|
// handles same-step OID4VCI+OID4VP case
|
|
@@ -178,6 +167,9 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
178
167
|
if(redirect_uri) {
|
|
179
168
|
result.redirect_uri = redirect_uri;
|
|
180
169
|
}
|
|
170
|
+
|
|
171
|
+
const {presentation: receivedPresentation} = parseResponseResult;
|
|
172
|
+
return {receivedPresentation};
|
|
181
173
|
},
|
|
182
174
|
inputRequired({step}) {
|
|
183
175
|
// indicate input always required to avoid automatically advancing
|
|
@@ -199,26 +191,61 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
199
191
|
verifyPresentationOptions.challenge = authorizationRequest.nonce;
|
|
200
192
|
verifyPresentationOptions.domain = authorizationRequest.response_uri;
|
|
201
193
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
194
|
+
// FIXME: OID4VP 1.0+ does not have a presentation submission
|
|
195
|
+
// handle mDL submission
|
|
196
|
+
const {envelope} = parseResponseResult;
|
|
197
|
+
if(envelope?.mediaType === 'application/mdl-vp-token') {
|
|
198
|
+
// generate `handover` for mDL verification
|
|
199
|
+
let handover;
|
|
200
|
+
|
|
201
|
+
// common `handover` parameters:
|
|
202
|
+
const origin = authorizationRequest?.expected_origins?.[0] ??
|
|
203
|
+
new URL(authorizationRequest.response_uri).origin;
|
|
204
|
+
const nonce = authorizationRequest.nonce;
|
|
205
|
+
|
|
206
|
+
// `direct_post.jwt` => ISO18013-7 Annex B
|
|
207
|
+
// FIXME: same response mode is also used for OID4VP 1.0 with
|
|
208
|
+
// `OpenID4VPHandover` where `presentationSubmission` will be absent;
|
|
209
|
+
// this is not yet supported
|
|
210
|
+
// https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-invocation-via-redirects
|
|
211
|
+
const {responseMode} = parseResponseResult;
|
|
212
|
+
if(responseMode === 'direct_post.jwt') {
|
|
213
|
+
handover = {
|
|
214
|
+
type: 'AnnexBHandover',
|
|
215
|
+
// per ISO 18013-7 B the `mdocGeneratedNonce` is base64url-encoded
|
|
216
|
+
// and put into the `apu` protected header parameter, so parse that
|
|
217
|
+
// here and convert it to a UTF-8 string instead
|
|
218
|
+
mdocGeneratedNonce: Buffer
|
|
219
|
+
.from(parseResponseResult.protectedHeader?.apu ?? '', 'base64url')
|
|
220
|
+
.toString('utf8'),
|
|
221
|
+
clientId: authorizationRequest.client_id,
|
|
222
|
+
responseUri: authorizationRequest.response_uri,
|
|
223
|
+
verifierGeneratedNonce: nonce
|
|
224
|
+
};
|
|
225
|
+
} else if(responseMode === 'dc_api') {
|
|
226
|
+
// `dc_api` => ISO18013-7 Annex C
|
|
227
|
+
handover = {
|
|
228
|
+
type: 'dcapi',
|
|
229
|
+
origin,
|
|
230
|
+
nonce,
|
|
231
|
+
recipientPublicJwk: parseResponseResult.recipientPublicJwk
|
|
232
|
+
};
|
|
233
|
+
} else if(responseMode === 'dc_api.jwt') {
|
|
234
|
+
// `dc_api.jwt` => ISO18013-7 Annex D
|
|
235
|
+
handover = {
|
|
236
|
+
type: 'OpenID4VPDCAPIHandover',
|
|
237
|
+
origin,
|
|
238
|
+
nonce,
|
|
239
|
+
jwkThumbprint: parseResponseResult.recipientPublicJwkThumbprint
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
207
243
|
verifyPresentationOptions.mdl = {
|
|
208
244
|
...verifyPresentationOptions.mdl,
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
sessionTranscript: {
|
|
214
|
-
// per ISO 18013-7 the `mdocGeneratedNonce` is base64url-encoded
|
|
215
|
-
// and put into the `apu` protected header parameter -- and the
|
|
216
|
-
// VC API `mdl.sessionTranscript` option expects the
|
|
217
|
-
// `mdocGeneratedNonce` to be base64url-encoded, so we can pass
|
|
218
|
-
// it straight through
|
|
219
|
-
mdocGeneratedNonce: protectedHeader.apu,
|
|
220
|
-
clientId: authorizationRequest.client_id
|
|
221
|
-
}
|
|
245
|
+
// send encoded mDL `sessionTranscript`
|
|
246
|
+
sessionTranscript: Buffer
|
|
247
|
+
.from(await oid4vp.mdl.encodeSessionTranscript({handover}))
|
|
248
|
+
.toString('base64url')
|
|
222
249
|
};
|
|
223
250
|
}
|
|
224
251
|
|
|
@@ -234,6 +261,7 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
234
261
|
});
|
|
235
262
|
|
|
236
263
|
// save OID4VP results in exchange
|
|
264
|
+
const {presentationSubmission} = parseResponseResult;
|
|
237
265
|
exchange.variables.results[exchange.step] = {
|
|
238
266
|
...exchange.variables.results[exchange.step],
|
|
239
267
|
openId: {
|
|
@@ -242,7 +270,9 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
242
270
|
presentationSubmission
|
|
243
271
|
}
|
|
244
272
|
};
|
|
245
|
-
|
|
273
|
+
// FIXME: do w/o `parseResponseResult.envelope` to eliminate envelope
|
|
274
|
+
// parsing (let verifier do it)
|
|
275
|
+
if(parseResponseResult.envelope) {
|
|
246
276
|
// include enveloped VP in step result
|
|
247
277
|
exchange.variables.results[exchange.step]
|
|
248
278
|
.envelopedPresentation = presentation;
|
|
@@ -251,7 +281,7 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
251
281
|
return verifyResult;
|
|
252
282
|
}
|
|
253
283
|
});
|
|
254
|
-
await exchangeProcessor.process(
|
|
284
|
+
await exchangeProcessor.process();
|
|
255
285
|
|
|
256
286
|
return result;
|
|
257
287
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock/vc-delivery",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bedrock Verifiable Credential Delivery",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@digitalbazaar/ed25519-signature-2020": "^5.4.0",
|
|
41
41
|
"@digitalbazaar/ezcap": "^4.1.0",
|
|
42
42
|
"@digitalbazaar/http-client": "^4.2.0",
|
|
43
|
-
"@digitalbazaar/oid4-client": "^5.
|
|
43
|
+
"@digitalbazaar/oid4-client": "^5.8.0",
|
|
44
44
|
"@digitalbazaar/vc": "^7.2.0",
|
|
45
45
|
"@digitalbazaar/webkms-client": "^14.2.0",
|
|
46
46
|
"assert-plus": "^1.0.0",
|
|
@@ -460,6 +460,14 @@ const oid4vpClientProfile = {
|
|
|
460
460
|
client_id: {type: 'string'},
|
|
461
461
|
client_id_scheme: {type: 'string'},
|
|
462
462
|
client_metadata: {type: 'object'},
|
|
463
|
+
dcql_query: {type: 'object'},
|
|
464
|
+
expected_origins: {
|
|
465
|
+
type: 'array',
|
|
466
|
+
minItems: 1,
|
|
467
|
+
items: {
|
|
468
|
+
type: 'string'
|
|
469
|
+
}
|
|
470
|
+
},
|
|
463
471
|
nonce: {type: 'string'},
|
|
464
472
|
presentation_definition: {type: 'object'},
|
|
465
473
|
response_mode: {type: 'string'},
|