@bedrock/vc-delivery 7.3.0 → 7.5.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/constants.js +4 -2
- package/lib/exchanges.js +14 -10
- package/lib/helpers.js +29 -4
- package/lib/http.js +15 -31
- package/lib/index.js +9 -3
- package/lib/oid4/authorizationRequest.js +238 -0
- package/lib/oid4/authorizationResponse.js +117 -0
- package/lib/oid4/clientProfiles.js +36 -0
- package/lib/oid4/http.js +81 -64
- package/lib/oid4/oid4vci.js +38 -10
- package/lib/oid4/oid4vp.js +206 -197
- package/lib/vcapi.js +158 -78
- package/lib/verify.js +2 -2
- package/package.json +5 -4
- package/schemas/bedrock-vc-workflow.js +176 -54
package/lib/constants.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2024 Digital Bazaar, Inc. All rights reserved.
|
|
2
|
+
* Copyright (c) 2024-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
// maximum
|
|
4
|
+
// maximum # of issuer instances that can be associated with a workflow
|
|
5
5
|
export const MAX_ISSUER_INSTANCES = 10;
|
|
6
|
+
// maximum # of OID4VP client profiles that can be associated with a workflow
|
|
7
|
+
export const MAX_OID4VP_CLIENT_PROFILES = 10;
|
package/lib/exchanges.js
CHANGED
|
@@ -87,23 +87,23 @@ export async function insert({workflowId, exchange}) {
|
|
|
87
87
|
assert.object(exchange, 'exchange');
|
|
88
88
|
assert.string(exchange.id, 'exchange.id');
|
|
89
89
|
// optional time to live in seconds
|
|
90
|
-
assert.optionalNumber(exchange.ttl);
|
|
90
|
+
assert.optionalNumber(exchange.ttl, 'exchange.ttl');
|
|
91
91
|
// optional variables to use in VC templates
|
|
92
|
-
assert.optionalObject(exchange.variables);
|
|
92
|
+
assert.optionalObject(exchange.variables, 'exchange.variables');
|
|
93
93
|
// optional current step in the exchange
|
|
94
|
-
assert.optionalString(exchange.step);
|
|
94
|
+
assert.optionalString(exchange.step, 'exchange.step');
|
|
95
|
+
// optional expires in exchange
|
|
96
|
+
assert.optionalString(exchange.expires, 'exchange.expires');
|
|
97
|
+
// optional protocols in exchange
|
|
98
|
+
assert.optionalObject(exchange.protocols, 'exchange.protocols');
|
|
95
99
|
|
|
96
100
|
// build exchange record
|
|
97
101
|
const now = Date.now();
|
|
98
102
|
const meta = {created: now, updated: now};
|
|
99
103
|
// possible states are: `pending`, `active`, `complete`, or `invalid`
|
|
100
104
|
exchange = {...exchange, sequence: 0, state: 'pending'};
|
|
101
|
-
if(exchange.
|
|
102
|
-
|
|
103
|
-
const expires = new Date(now + exchange.ttl * 1000);
|
|
104
|
-
meta.expires = expires;
|
|
105
|
-
exchange.expires = expires.toISOString().replace(/\.\d+Z$/, 'Z');
|
|
106
|
-
delete exchange.ttl;
|
|
105
|
+
if(exchange.expires !== undefined) {
|
|
106
|
+
meta.expires = new Date(exchange.expires);
|
|
107
107
|
}
|
|
108
108
|
const {localId: localWorkflowId} = parseLocalId({id: workflowId});
|
|
109
109
|
const record = {
|
|
@@ -569,7 +569,11 @@ function _buildUpdate({exchange, complete}) {
|
|
|
569
569
|
const now = Date.now();
|
|
570
570
|
const update = {
|
|
571
571
|
$inc: {'exchange.sequence': 1},
|
|
572
|
-
$set: {
|
|
572
|
+
$set: {
|
|
573
|
+
'exchange.state': exchange.state,
|
|
574
|
+
'exchange.secrets': exchange.secrets,
|
|
575
|
+
'meta.updated': now
|
|
576
|
+
},
|
|
573
577
|
$unset: {}
|
|
574
578
|
};
|
|
575
579
|
if(complete && typeof exchange.variables !== 'string') {
|
package/lib/helpers.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import * as vcjwt from './vcjwt.js';
|
|
6
6
|
import {decodeId, generateId} from 'bnid';
|
|
7
|
+
import {compile} from '@bedrock/validation';
|
|
7
8
|
import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
|
|
8
9
|
import {httpClient} from '@digitalbazaar/http-client';
|
|
9
10
|
import {httpsAgent} from '@bedrock/https-agent';
|
|
@@ -86,6 +87,18 @@ export async function evaluateTemplate({
|
|
|
86
87
|
return jsonata(template).evaluate(variables, variables);
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
export async function evaluateExchangeStep({
|
|
91
|
+
workflow, exchange, stepName = exchange.step
|
|
92
|
+
}) {
|
|
93
|
+
let step = workflow.steps[stepName];
|
|
94
|
+
if(step.stepTemplate) {
|
|
95
|
+
step = await evaluateTemplate(
|
|
96
|
+
{workflow, exchange, typedTemplate: step.stepTemplate});
|
|
97
|
+
}
|
|
98
|
+
await validateStep({step});
|
|
99
|
+
return step;
|
|
100
|
+
}
|
|
101
|
+
|
|
89
102
|
export function getTemplateVariables({workflow, exchange} = {}) {
|
|
90
103
|
const {variables = {}} = exchange;
|
|
91
104
|
// always include `globals` as keyword for self-referencing exchange info
|
|
@@ -231,12 +244,16 @@ export function createVerifyOptions({
|
|
|
231
244
|
options.checks = [...checkSet];
|
|
232
245
|
|
|
233
246
|
// update `challenge`
|
|
234
|
-
options.challenge
|
|
235
|
-
|
|
236
|
-
|
|
247
|
+
if(options.challenge === undefined) {
|
|
248
|
+
options.challenge = expectedChallenge ??
|
|
249
|
+
verifiablePresentationRequest.challenge ??
|
|
250
|
+
presentation?.proof?.challenge;
|
|
251
|
+
}
|
|
237
252
|
|
|
238
253
|
// update `domain`
|
|
239
|
-
options.domain
|
|
254
|
+
if(options.domain === undefined) {
|
|
255
|
+
options.domain = domain;
|
|
256
|
+
}
|
|
240
257
|
|
|
241
258
|
return options;
|
|
242
259
|
}
|
|
@@ -352,3 +369,11 @@ function _getEnvelope({envelope, format}) {
|
|
|
352
369
|
details: {httpStatusCode: 400, public: true}
|
|
353
370
|
});
|
|
354
371
|
}
|
|
372
|
+
|
|
373
|
+
export function validateVerifiablePresentation({schema, presentation}) {
|
|
374
|
+
const validate = compile({schema});
|
|
375
|
+
const {valid, error} = validate(presentation);
|
|
376
|
+
if(!valid) {
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
package/lib/http.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2018-
|
|
2
|
+
* Copyright (c) 2018-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 * as oid4 from './oid4/http.js';
|
|
7
|
-
import {createExchange, processExchange} from './vcapi.js';
|
|
7
|
+
import {createExchange, getProtocols, processExchange} from './vcapi.js';
|
|
8
8
|
import {
|
|
9
9
|
createExchangeBody, useExchangeBody
|
|
10
10
|
} from '../schemas/bedrock-vc-workflow.js';
|
|
@@ -18,7 +18,8 @@ import {createValidateMiddleware as validate} from '@bedrock/validation';
|
|
|
18
18
|
|
|
19
19
|
const {util: {BedrockError}} = bedrock;
|
|
20
20
|
|
|
21
|
-
// FIXME: remove and apply
|
|
21
|
+
// FIXME: remove and apply to specific routes via
|
|
22
|
+
// `bedrock.express.bodyParser.routes` + `@bedrock/express@8.4`
|
|
22
23
|
bedrock.events.on('bedrock-express.configure.bodyParser', app => {
|
|
23
24
|
app.use(bodyParser.json({
|
|
24
25
|
// allow json values that are not just objects or arrays
|
|
@@ -66,7 +67,7 @@ export async function addRoutes({app, service} = {}) {
|
|
|
66
67
|
app.post(
|
|
67
68
|
routes.exchanges,
|
|
68
69
|
cors(),
|
|
69
|
-
validate({bodySchema: createExchangeBody}),
|
|
70
|
+
validate({bodySchema: createExchangeBody()}),
|
|
70
71
|
getConfigMiddleware,
|
|
71
72
|
middleware.authorizeServiceObjectRequest(),
|
|
72
73
|
asyncHandler(async (req, res) => {
|
|
@@ -75,11 +76,13 @@ export async function addRoutes({app, service} = {}) {
|
|
|
75
76
|
try {
|
|
76
77
|
const {config: workflow} = req.serviceObject;
|
|
77
78
|
const {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
expires,
|
|
80
|
+
ttl,
|
|
81
|
+
variables = {},
|
|
82
|
+
step,
|
|
83
|
+
openId
|
|
81
84
|
} = req.body;
|
|
82
|
-
const exchange = {
|
|
85
|
+
const exchange = {expires, ttl, variables, step, openId};
|
|
83
86
|
const {id} = await createExchange({workflow, exchange});
|
|
84
87
|
const location = `${workflow.id}/exchanges/${id}`;
|
|
85
88
|
res.status(204).location(location).send();
|
|
@@ -101,8 +104,9 @@ export async function addRoutes({app, service} = {}) {
|
|
|
101
104
|
middleware.authorizeServiceObjectRequest(),
|
|
102
105
|
asyncHandler(async (req, res) => {
|
|
103
106
|
const {exchange} = await req.getExchange();
|
|
104
|
-
// do not return any
|
|
107
|
+
// do not return any secret credentials
|
|
105
108
|
delete exchange.openId?.oauth2?.keyPair?.privateKeyJwk;
|
|
109
|
+
delete exchange.secrets;
|
|
106
110
|
res.json({exchange});
|
|
107
111
|
}));
|
|
108
112
|
|
|
@@ -138,28 +142,8 @@ export async function addRoutes({app, service} = {}) {
|
|
|
138
142
|
details: {httpStatusCode: 406, public: true}
|
|
139
143
|
});
|
|
140
144
|
}
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const {exchange} = await req.getExchange();
|
|
144
|
-
const exchangeId = `${workflow.id}/exchanges/${exchange.id}`;
|
|
145
|
-
const protocols = {
|
|
146
|
-
vcapi: exchangeId
|
|
147
|
-
};
|
|
148
|
-
const openIdRoute = `${exchangeId}/openid`;
|
|
149
|
-
if(oid4.supportsOID4VCI({exchange})) {
|
|
150
|
-
// OID4VCI supported; add credential offer URL
|
|
151
|
-
const searchParams = new URLSearchParams();
|
|
152
|
-
const uri = `${openIdRoute}/credential-offer`;
|
|
153
|
-
searchParams.set('credential_offer_uri', uri);
|
|
154
|
-
protocols.OID4VCI = `openid-credential-offer://?${searchParams}`;
|
|
155
|
-
} else if(await oid4.supportsOID4VP({workflow, exchange})) {
|
|
156
|
-
// OID4VP supported; add openid4vp URL
|
|
157
|
-
const searchParams = new URLSearchParams({
|
|
158
|
-
client_id: `${openIdRoute}/client/authorization/response`,
|
|
159
|
-
request_uri: `${openIdRoute}/client/authorization/request`
|
|
160
|
-
});
|
|
161
|
-
protocols.OID4VP = `openid4vp://?${searchParams}`;
|
|
162
|
-
}
|
|
145
|
+
|
|
146
|
+
const protocols = await getProtocols({req});
|
|
163
147
|
res.json({protocols});
|
|
164
148
|
}));
|
|
165
149
|
|
package/lib/index.js
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import * as workflowSchemas from '../schemas/bedrock-vc-workflow.js';
|
|
6
6
|
import {createService, schemas} from '@bedrock/service-core';
|
|
7
|
+
import {
|
|
8
|
+
MAX_ISSUER_INSTANCES, MAX_OID4VP_CLIENT_PROFILES
|
|
9
|
+
} from './constants.js';
|
|
7
10
|
import {addRoutes} from './http.js';
|
|
8
11
|
import {initializeServiceAgent} from '@bedrock/service-agent';
|
|
9
|
-
import {MAX_ISSUER_INSTANCES} from './constants.js';
|
|
10
12
|
import {parseLocalId} from './helpers.js';
|
|
11
13
|
import '@bedrock/express';
|
|
12
14
|
|
|
@@ -37,8 +39,9 @@ async function _initService({serviceType, routePrefix}) {
|
|
|
37
39
|
schema.properties.issuerInstances = issuerInstances;
|
|
38
40
|
// allow zcaps by custom reference ID
|
|
39
41
|
schema.properties.zcaps = structuredClone(schemas.zcaps);
|
|
40
|
-
// max of
|
|
41
|
-
schema.properties.zcaps.maxProperties =
|
|
42
|
+
// max of 3 basic zcaps + max issuer instances + max OID4VP client profiles
|
|
43
|
+
schema.properties.zcaps.maxProperties =
|
|
44
|
+
3 + MAX_ISSUER_INSTANCES + MAX_OID4VP_CLIENT_PROFILES;
|
|
42
45
|
schema.properties.zcaps.additionalProperties = schemas.delegatedZcap;
|
|
43
46
|
// note: credential templates are not required; if any other properties
|
|
44
47
|
// become required, add them here
|
|
@@ -65,6 +68,9 @@ async function _initService({serviceType, routePrefix}) {
|
|
|
65
68
|
},
|
|
66
69
|
// these zcaps are optional (by reference ID)
|
|
67
70
|
zcapReferenceIds: [{
|
|
71
|
+
// `issue` reference ID is deprecated; use `issuerInstances` option
|
|
72
|
+
// instead to specify issuer options and an `issue` zcap for each
|
|
73
|
+
// issuer instance
|
|
68
74
|
referenceId: 'issue',
|
|
69
75
|
required: false
|
|
70
76
|
}, {
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import {AsymmetricKey, KmsClient} from '@digitalbazaar/webkms-client';
|
|
6
|
+
import {exportJWK, generateKeyPair, UnsecuredJWT} from 'jose';
|
|
7
|
+
import {oid4vp, signJWT} from '@digitalbazaar/oid4-client';
|
|
8
|
+
import {getClientBaseUrl} from './clientProfiles.js';
|
|
9
|
+
import {getZcapClient} from '../helpers.js';
|
|
10
|
+
import {httpsAgent} from '@bedrock/https-agent';
|
|
11
|
+
import {randomUUID} from 'node:crypto';
|
|
12
|
+
|
|
13
|
+
const {util: {BedrockError}} = bedrock;
|
|
14
|
+
|
|
15
|
+
export async function create({
|
|
16
|
+
workflow, exchange,
|
|
17
|
+
clientProfile, clientProfileId,
|
|
18
|
+
verifiablePresentationRequest
|
|
19
|
+
}) {
|
|
20
|
+
const authorizationRequest = oid4vp.fromVpr({verifiablePresentationRequest});
|
|
21
|
+
|
|
22
|
+
// get params from step OID4VP client profile to apply to the AR
|
|
23
|
+
const {
|
|
24
|
+
client_id, client_id_scheme,
|
|
25
|
+
nonce,
|
|
26
|
+
presentation_definition,
|
|
27
|
+
response_mode, response_uri
|
|
28
|
+
} = clientProfile;
|
|
29
|
+
const clientBaseUrl = getClientBaseUrl({workflow, exchange, clientProfileId});
|
|
30
|
+
|
|
31
|
+
// client_id_scheme (draft versions of OID4VP use this param)
|
|
32
|
+
authorizationRequest.client_id_scheme = client_id_scheme ?? 'redirect_uri';
|
|
33
|
+
|
|
34
|
+
// presentation_definition
|
|
35
|
+
authorizationRequest.presentation_definition = presentation_definition ??
|
|
36
|
+
authorizationRequest.presentation_definition;
|
|
37
|
+
|
|
38
|
+
// response_mode
|
|
39
|
+
authorizationRequest.response_mode = response_mode ?? 'direct_post';
|
|
40
|
+
|
|
41
|
+
// response_uri
|
|
42
|
+
authorizationRequest.response_uri = response_uri ??
|
|
43
|
+
`${clientBaseUrl}/authorization/response`;
|
|
44
|
+
|
|
45
|
+
// client_id (defaults to `response_uri`)
|
|
46
|
+
// FIXME: newer versions of OID4VP require a prefix of `redirect_uri:` for
|
|
47
|
+
// the default case -- which is incompatible with some draft versions
|
|
48
|
+
authorizationRequest.client_id = client_id ??
|
|
49
|
+
authorizationRequest.response_uri;
|
|
50
|
+
|
|
51
|
+
// `x509_san_dns` requires the `direct_post.jwt` response mode when using
|
|
52
|
+
// `direct_post`
|
|
53
|
+
if(authorizationRequest.response_mode === 'direct_post' &&
|
|
54
|
+
oid4vp.authzRequest.usesClientIdScheme({
|
|
55
|
+
authorizationRequest, scheme: 'x509_san_dns'
|
|
56
|
+
})) {
|
|
57
|
+
authorizationRequest.response_mode += '.jwt';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// nonce
|
|
61
|
+
if(nonce) {
|
|
62
|
+
authorizationRequest.nonce = nonce;
|
|
63
|
+
} else if(authorizationRequest.nonce === undefined) {
|
|
64
|
+
// if no nonce has been set for the authorization request, use the
|
|
65
|
+
// exchange ID
|
|
66
|
+
authorizationRequest.nonce = exchange.id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// client_metadata; create from the `clientProfile` the rest of the AR and
|
|
70
|
+
// generate any necessary secrets for it
|
|
71
|
+
const {client_metadata, secrets} = await _createClientMetaData({
|
|
72
|
+
authorizationRequest, clientProfile
|
|
73
|
+
});
|
|
74
|
+
authorizationRequest.client_metadata = client_metadata;
|
|
75
|
+
|
|
76
|
+
// only set default `aud` for signed OID4VP authz requests
|
|
77
|
+
if(client_metadata.require_signed_request_object) {
|
|
78
|
+
authorizationRequest.aud = 'https://self-issued.me/v2';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {authorizationRequest, secrets};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function encode({
|
|
85
|
+
workflow, clientProfile, authorizationRequest
|
|
86
|
+
} = {}) {
|
|
87
|
+
// if required, construct authz request as signed JWT
|
|
88
|
+
if(authorizationRequest.client_metadata.require_signed_request_object) {
|
|
89
|
+
return _createJwt({workflow, clientProfile, authorizationRequest});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// construct authz request as unsecured JWT
|
|
93
|
+
return new UnsecuredJWT(authorizationRequest).encode();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function _createClientMetaData({
|
|
97
|
+
authorizationRequest, clientProfile
|
|
98
|
+
} = {}) {
|
|
99
|
+
// for storing client profile secrets
|
|
100
|
+
const secrets = {};
|
|
101
|
+
|
|
102
|
+
// create base `client_metadata` from client profile if given
|
|
103
|
+
const client_metadata = clientProfile.client_metadata ?
|
|
104
|
+
structuredClone(clientProfile.client_metadata) : {};
|
|
105
|
+
|
|
106
|
+
// ensure `vp_formats` exists and track whether it was present or not
|
|
107
|
+
const hasVpFormats = !!client_metadata.vp_formats;
|
|
108
|
+
if(!hasVpFormats) {
|
|
109
|
+
client_metadata.vp_formats = {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// add `mso_mdoc` format if requested and not already present
|
|
113
|
+
if(!client_metadata.vp_formats.mso_mdoc &&
|
|
114
|
+
oid4vp.authzRequest.requestsFormat({
|
|
115
|
+
authorizationRequest, format: 'mso_mdoc'
|
|
116
|
+
})) {
|
|
117
|
+
client_metadata.vp_formats.mso_mdoc = {
|
|
118
|
+
alg: ['EdDSA', 'ES256']
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// add `jwt_vp` format if requested and not already present
|
|
123
|
+
if(!client_metadata.vp_formats.jwt_vp &&
|
|
124
|
+
oid4vp.authzRequest.requestsFormat({
|
|
125
|
+
authorizationRequest, format: 'jwt_vp'
|
|
126
|
+
})) {
|
|
127
|
+
// support various aliases for different versions of OID4VP
|
|
128
|
+
client_metadata.vp_formats.jwt_vp =
|
|
129
|
+
client_metadata.vp_formats.jwt_vp_json = {
|
|
130
|
+
alg: ['EdDSA', 'Ed25519', 'ES256', 'ES384']
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// add `ldp_vp` format if requested and not already present or if no other
|
|
135
|
+
// formats are present
|
|
136
|
+
if(!hasVpFormats || (!client_metadata.vp_formats.ldp_vp &&
|
|
137
|
+
oid4vp.authzRequest.requestsFormat({
|
|
138
|
+
authorizationRequest, format: 'ldp_vp'
|
|
139
|
+
}))) {
|
|
140
|
+
// support various aliases for different versions of OID4VP
|
|
141
|
+
client_metadata.vp_formats.di_vp =
|
|
142
|
+
client_metadata.vp_formats.ldp_vp = {
|
|
143
|
+
proof_type: [
|
|
144
|
+
'ecdsa-rdfc-2019',
|
|
145
|
+
'eddsa-rdfc-2022',
|
|
146
|
+
'Ed25519Signature2020'
|
|
147
|
+
]
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// `x509_san_dns` client ID scheme requires authz request signing;
|
|
152
|
+
// any client ID scheme that requires this
|
|
153
|
+
if(oid4vp.authzRequest.usesClientIdScheme({
|
|
154
|
+
authorizationRequest, scheme: 'x509_san_dns'
|
|
155
|
+
})) {
|
|
156
|
+
client_metadata.require_signed_request_object = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// for response mode `direct_post.jwt`, offer encryption options
|
|
160
|
+
if(authorizationRequest.response_mode === 'direct_post.jwt') {
|
|
161
|
+
// generate ECDH-ES P-256 key
|
|
162
|
+
const kp = await generateKeyPair('ECDH-ES', {
|
|
163
|
+
crv: 'P-256', extractable: true
|
|
164
|
+
});
|
|
165
|
+
const [privateKeyJwk, publicKeyJwk] = await Promise.all([
|
|
166
|
+
exportJWK(kp.privateKey),
|
|
167
|
+
exportJWK(kp.publicKey)
|
|
168
|
+
]);
|
|
169
|
+
publicKeyJwk.use = 'enc';
|
|
170
|
+
publicKeyJwk.alg = 'ECDH-ES';
|
|
171
|
+
privateKeyJwk.kid = publicKeyJwk.kid = `urn:uuid:${randomUUID()}`;
|
|
172
|
+
secrets.keyAgreementKeyPairs = [{privateKeyJwk, publicKeyJwk}];
|
|
173
|
+
|
|
174
|
+
// create / prepend to public JWK key set
|
|
175
|
+
client_metadata.jwks = {
|
|
176
|
+
...client_metadata.jwks,
|
|
177
|
+
keys: [publicKeyJwk].concat(client_metadata.jwks?.keys ?? [])
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {client_metadata, secrets};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function _createJwt({workflow, clientProfile, authorizationRequest}) {
|
|
185
|
+
try {
|
|
186
|
+
// create zcap client
|
|
187
|
+
const {zcapClient, zcaps} = await getZcapClient({workflow});
|
|
188
|
+
|
|
189
|
+
// get any `x5c` and the zcap to use to sign the authz request via
|
|
190
|
+
// the client profile
|
|
191
|
+
const {
|
|
192
|
+
authorizationRequestSigningParameters: {x5c} = {},
|
|
193
|
+
zcapReferenceIds: {signAuthorizationRequest: refId} = {}
|
|
194
|
+
} = clientProfile;
|
|
195
|
+
if(refId === undefined) {
|
|
196
|
+
throw new BedrockError(
|
|
197
|
+
'The OID4VP client profile does not specify which capability in the ' +
|
|
198
|
+
'workflow configuration to use to sign authorization requests.', {
|
|
199
|
+
name: 'DataError',
|
|
200
|
+
details: {httpStatusCode: 500, public: true}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const capability = zcaps[refId];
|
|
204
|
+
if(capability === undefined) {
|
|
205
|
+
throw new BedrockError(
|
|
206
|
+
'The capability specified by the OID4VP client profile for signing ' +
|
|
207
|
+
'authorization requests was not found in the workflow configuration.', {
|
|
208
|
+
name: 'DataError',
|
|
209
|
+
details: {httpStatusCode: 500, public: true}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// create a WebKMS `signer` interface
|
|
214
|
+
const {invocationSigner} = zcapClient;
|
|
215
|
+
const kmsClient = new KmsClient({httpsAgent});
|
|
216
|
+
const signer = await AsymmetricKey.fromCapability({
|
|
217
|
+
capability, invocationSigner, kmsClient
|
|
218
|
+
});
|
|
219
|
+
const keyDescription = await signer.getKeyDescription();
|
|
220
|
+
const kid = keyDescription.id;
|
|
221
|
+
|
|
222
|
+
// create the JWT payload and header to be signed
|
|
223
|
+
const payload = {
|
|
224
|
+
...authorizationRequest
|
|
225
|
+
};
|
|
226
|
+
const protectedHeader = {typ: 'JWT', alg: 'ES256', kid, x5c};
|
|
227
|
+
|
|
228
|
+
// create the JWT
|
|
229
|
+
return signJWT({payload, protectedHeader, signer});
|
|
230
|
+
} catch(cause) {
|
|
231
|
+
throw new BedrockError(
|
|
232
|
+
`Could not sign authorization request: ${cause.message}`, {
|
|
233
|
+
name: cause instanceof BedrockError ? cause.name : 'OperationError',
|
|
234
|
+
cause,
|
|
235
|
+
details: {httpStatusCode: 500, public: true}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import {
|
|
6
|
+
presentationSubmission as presentationSubmissionSchema,
|
|
7
|
+
verifiablePresentation as verifiablePresentationSchema
|
|
8
|
+
} from '../../schemas/bedrock-vc-workflow.js';
|
|
9
|
+
import {compile} from '@bedrock/validation';
|
|
10
|
+
import {oid4vp} from '@digitalbazaar/oid4-client';
|
|
11
|
+
import {unenvelopePresentation} from '../helpers.js';
|
|
12
|
+
|
|
13
|
+
const {util: {BedrockError}} = bedrock;
|
|
14
|
+
|
|
15
|
+
const VALIDATORS = {
|
|
16
|
+
presentation: null,
|
|
17
|
+
presentationSubmission: null
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
|
|
21
|
+
|
|
22
|
+
bedrock.events.on('bedrock.init', () => {
|
|
23
|
+
// create validators for x-www-form-urlencoded parsed data
|
|
24
|
+
VALIDATORS.presentation = compile({schema: verifiablePresentationSchema()});
|
|
25
|
+
VALIDATORS.presentationSubmission = compile({
|
|
26
|
+
schema: presentationSubmissionSchema
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export async function parse({req, exchange, clientProfileId} = {}) {
|
|
31
|
+
try {
|
|
32
|
+
const {body} = req;
|
|
33
|
+
const {
|
|
34
|
+
responseMode, parsed, protectedHeader
|
|
35
|
+
} = await oid4vp.verifier.parseAuthorizationResponse({
|
|
36
|
+
body,
|
|
37
|
+
getDecryptParameters() {
|
|
38
|
+
return _getDecryptParameters({exchange, clientProfileId});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// validate parsed presentation submission
|
|
43
|
+
const {presentationSubmission} = parsed;
|
|
44
|
+
_validate(VALIDATORS.presentationSubmission, presentationSubmission);
|
|
45
|
+
|
|
46
|
+
// obtain `presentation` and optional `envelope` from parsed `vpToken`
|
|
47
|
+
const {vpToken} = parsed;
|
|
48
|
+
let presentation;
|
|
49
|
+
let envelope;
|
|
50
|
+
|
|
51
|
+
if(oid4vp.authzResponse.submitsFormat({
|
|
52
|
+
presentationSubmission, format: 'mso_mdoc'
|
|
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);
|
|
71
|
+
presentation = {
|
|
72
|
+
'@context': VC_CONTEXT_2,
|
|
73
|
+
id: `data:${format},${raw}`,
|
|
74
|
+
type: 'EnvelopedVerifiablePresentation'
|
|
75
|
+
};
|
|
76
|
+
envelope = {raw, contents, format};
|
|
77
|
+
} else {
|
|
78
|
+
// simplest case: `vpToken` is a VP; validate it
|
|
79
|
+
presentation = vpToken;
|
|
80
|
+
_validate(VALIDATORS.presentation, presentation);
|
|
81
|
+
// FIXME: validate VP against presentation submission
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
responseMode,
|
|
86
|
+
presentationSubmission,
|
|
87
|
+
presentation,
|
|
88
|
+
envelope,
|
|
89
|
+
protectedHeader
|
|
90
|
+
};
|
|
91
|
+
} catch(cause) {
|
|
92
|
+
throw new BedrockError(
|
|
93
|
+
`Could not parse authorization response: ${cause.message}`, {
|
|
94
|
+
name: cause.name ?? 'OperationError',
|
|
95
|
+
cause,
|
|
96
|
+
details: {
|
|
97
|
+
httpStatusCode: cause?.details?.httpStatusCode ?? 400,
|
|
98
|
+
public: true
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _getDecryptParameters({exchange, clientProfileId}) {
|
|
105
|
+
// get private key agreement keys in JWT format
|
|
106
|
+
const {keyAgreementKeyPairs} = exchange.secrets?.oid4vp?.clientProfiles
|
|
107
|
+
?.[clientProfileId ?? 'default'] ?? {};
|
|
108
|
+
const keys = keyAgreementKeyPairs.map(({privateKeyJwk}) => privateKeyJwk);
|
|
109
|
+
return {keys};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _validate(validator, data) {
|
|
113
|
+
const result = validator(data);
|
|
114
|
+
if(!result.valid) {
|
|
115
|
+
throw result.error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
|
|
6
|
+
const {util: {BedrockError}} = bedrock;
|
|
7
|
+
|
|
8
|
+
export function getClientBaseUrl({workflow, exchange, clientProfileId}) {
|
|
9
|
+
const openIdBaseUrl = `${workflow.id}/exchanges/${exchange.id}/openid`;
|
|
10
|
+
return openIdBaseUrl + (clientProfileId !== undefined ?
|
|
11
|
+
`/clients/${clientProfileId}` : '/client');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getClientProfile({step, clientProfileId}) {
|
|
15
|
+
const {openId: {clientProfiles}} = step;
|
|
16
|
+
|
|
17
|
+
let clientProfile;
|
|
18
|
+
if(clientProfileId !== undefined) {
|
|
19
|
+
if(clientProfiles) {
|
|
20
|
+
clientProfile = clientProfiles[clientProfileId];
|
|
21
|
+
}
|
|
22
|
+
} else if(!clientProfiles) {
|
|
23
|
+
// legacy step without any client profiles
|
|
24
|
+
clientProfile = step.openId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if(!clientProfile) {
|
|
28
|
+
throw new BedrockError(
|
|
29
|
+
'The selected OID4VP profile is not supported by this exchange.', {
|
|
30
|
+
name: 'NotSupportedError',
|
|
31
|
+
details: {httpStatusCode: 400, public: true}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return clientProfile;
|
|
36
|
+
}
|