@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 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 number of issuer instances that can be associated with a workflow
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.ttl !== undefined) {
102
- // TTL is in seconds, convert to `expires`
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: {'exchange.state': exchange.state, 'meta.updated': now},
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 = expectedChallenge ??
235
- verifiablePresentationRequest.challenge ??
236
- presentation?.proof?.challenge;
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 = 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-2024 Digital Bazaar, Inc. All rights reserved.
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 at top-level application
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
- ttl, openId, variables = {},
79
- // allow steps to be skipped by creator as needed
80
- step = workflow.initialStep
79
+ expires,
80
+ ttl,
81
+ variables = {},
82
+ step,
83
+ openId
81
84
  } = req.body;
82
- const exchange = {ttl, openId, variables, step};
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 oauth2 credentials
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
- // construct and return `protocols` object
142
- const {config: workflow} = req.serviceObject;
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 4 basic zcaps + max issuer instances
41
- schema.properties.zcaps.maxProperties = 4 + MAX_ISSUER_INSTANCES;
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
+ }