@bedrock/vc-delivery 7.18.1 → 7.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/helpers.js CHANGED
@@ -175,6 +175,27 @@ export async function generateRandom() {
175
175
  });
176
176
  }
177
177
 
178
+ export function getAlgFromJwk({jwk} = {}) {
179
+ if(jwk.alg) {
180
+ return jwk.alg;
181
+ }
182
+ if(jwk.kty === 'EC' && jwk.crv) {
183
+ if(jwk.crv.startsWith('P-')) {
184
+ return `ES${jwk.crv.slice(2)}`;
185
+ }
186
+ if(jwk.crv === 'secp256k1') {
187
+ return 'ES256K';
188
+ }
189
+ }
190
+ if(jwk.kty === 'OKP' && jwk.crv?.startsWith('Ed')) {
191
+ return 'EdDSA';
192
+ }
193
+ if(jwk.kty === 'RSA') {
194
+ return 'PS256';
195
+ }
196
+ return 'invalid';
197
+ }
198
+
178
199
  export function getWorkflowIssuerInstances({workflow} = {}) {
179
200
  let {issuerInstances} = workflow;
180
201
  if(!issuerInstances && workflow.zcaps.issue) {
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import {AsymmetricKey, KmsClient} from '@digitalbazaar/webkms-client';
6
- import {exportJWK, generateKeyPair} from 'jose';
6
+ import {exportJWK, generateKeyPair, importJWK} from 'jose';
7
7
  import {oid4vp, signJWT} from '@digitalbazaar/oid4-client';
8
8
  import {getClientBaseUrl} from './clientProfiles.js';
9
9
  import {getZcapClient} from '../helpers.js';
@@ -237,42 +237,65 @@ async function _createJwt({workflow, clientProfile, authorizationRequest}) {
237
237
  // create zcap client
238
238
  const {zcapClient, zcaps} = await getZcapClient({workflow});
239
239
 
240
- // get any `x5c` and the zcap to use to sign the authz request via
241
- // the client profile
240
+ // get any `x5c` and either `privateKeyJwk` or the zcap to use to sign the
241
+ // authz request via the client profile
242
242
  const {
243
- authorizationRequestSigningParameters: {x5c} = {},
243
+ authorizationRequestSigningParameters: {privateKeyJwk, x5c} = {},
244
244
  zcapReferenceIds: {signAuthorizationRequest: refId} = {}
245
245
  } = clientProfile;
246
- if(refId === undefined) {
246
+ if(privateKeyJwk === undefined && refId === undefined) {
247
247
  throw new BedrockError(
248
- 'The OID4VP client profile does not specify which capability in the ' +
249
- 'workflow configuration to use to sign authorization requests.', {
250
- name: 'DataError',
251
- details: {httpStatusCode: 500, public: true}
252
- });
253
- }
254
- const capability = zcaps[refId];
255
- if(capability === undefined) {
256
- throw new BedrockError(
257
- 'The capability specified by the OID4VP client profile for signing ' +
258
- 'authorization requests was not found in the workflow configuration.', {
248
+ 'The OID4VP client profile does not specify the private key or ' +
249
+ 'capability in the workflow configuration to use to sign ' +
250
+ 'authorization requests.', {
259
251
  name: 'DataError',
260
252
  details: {httpStatusCode: 500, public: true}
261
253
  });
262
254
  }
263
255
 
264
- // create a WebKMS `signer` interface
265
- const {invocationSigner} = zcapClient;
266
- const kmsClient = new KmsClient({httpsAgent});
267
- const signer = await AsymmetricKey.fromCapability({
268
- capability, invocationSigner, kmsClient
269
- });
270
- const keyDescription = await signer.getKeyDescription();
271
- const kid = keyDescription.id;
256
+ // code assumed ES256 (ECDSA P-256 + SHA-256) support only
257
+ const alg = 'ES256';
258
+ let kid;
259
+ let signer;
260
+ if(privateKeyJwk) {
261
+ kid = privateKeyJwk.kid;
262
+
263
+ // import `privateKeyJwk`
264
+ const privateKey = await importJWK(privateKeyJwk, alg);
265
+
266
+ // create signer API
267
+ const algorithm = {name: 'ECDSA', hash: {name: 'SHA-256'}};
268
+ signer = {
269
+ async sign({data}) {
270
+ return new Uint8Array(
271
+ await crypto.subtle.sign(algorithm, privateKey, data));
272
+ }
273
+ };
274
+ } else {
275
+ const capability = zcaps[refId];
276
+ if(capability === undefined) {
277
+ throw new BedrockError(
278
+ 'The capability specified by the OID4VP client profile for signing ' +
279
+ 'authorization requests was not found in the workflow ' +
280
+ 'configuration.', {
281
+ name: 'DataError',
282
+ details: {httpStatusCode: 500, public: true}
283
+ });
284
+ }
285
+
286
+ // create a WebKMS `signer` interface
287
+ const {invocationSigner} = zcapClient;
288
+ const kmsClient = new KmsClient({httpsAgent});
289
+ signer = await AsymmetricKey.fromCapability({
290
+ capability, invocationSigner, kmsClient
291
+ });
292
+ const keyDescription = await signer.getKeyDescription();
293
+ kid = keyDescription.id;
294
+ }
272
295
 
273
296
  // create the JWT payload and header to be signed
274
297
  const payload = {...authorizationRequest};
275
- const protectedHeader = {typ: OID4VP_JWT_TYP, alg: 'ES256', kid, x5c};
298
+ const protectedHeader = {typ: OID4VP_JWT_TYP, alg, kid, x5c};
276
299
 
277
300
  // create the JWT
278
301
  return signJWT({payload, protectedHeader, signer});
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2026 Digital Bazaar, Inc.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import {
@@ -16,8 +16,6 @@ const VALIDATORS = {
16
16
  presentationSubmission: null
17
17
  };
18
18
 
19
- const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
20
-
21
19
  bedrock.events.on('bedrock.init', () => {
22
20
  VALIDATORS.presentation = compile({schema: verifiablePresentationSchema()});
23
21
  VALIDATORS.presentationSubmission = compile({
@@ -33,11 +31,14 @@ export async function parse({
33
31
  const {
34
32
  responseMode, parsed, protectedHeader,
35
33
  recipientPublicJwk, recipientPublicJwkThumbprint,
36
- vpTokenMediaType
34
+ vpTokenMediaType,
35
+ envelope, presentation, mdocOptions
37
36
  } = await oid4vp.verifier.parseAuthorizationResponse({
38
37
  body,
39
- getDecryptParameters() {
40
- return _getDecryptParameters({exchange, clientProfileId});
38
+ getDecryptParameters({mdocOptions} = {}) {
39
+ return _getDecryptParameters({
40
+ exchange, clientProfileId, authorizationRequest, mdocOptions
41
+ });
41
42
  },
42
43
  authorizationRequest
43
44
  });
@@ -48,22 +49,8 @@ export async function parse({
48
49
  _validate(VALIDATORS.presentationSubmission, presentationSubmission);
49
50
  }
50
51
 
51
- // obtain `presentation` and optional `envelope` from parsed `vpToken`
52
- const {vpToken} = parsed;
53
- let presentation;
54
- let envelope;
55
-
56
- if(vpTokenMediaType !== 'application/vp') {
57
- // `vp_token` contains some enveloped format
58
- presentation = {
59
- '@context': VC_CONTEXT_2,
60
- id: `data:${vpTokenMediaType},${vpToken}`,
61
- type: 'EnvelopedVerifiablePresentation'
62
- };
63
- envelope = {mediaType: vpTokenMediaType};
64
- } else {
65
- // simplest case: `vpToken` is a VP; validate it against basic schema
66
- presentation = vpToken;
52
+ // validate `presentation` against basic schema
53
+ if(vpTokenMediaType === 'application/vp') {
67
54
  _validate(VALIDATORS.presentation, presentation);
68
55
  }
69
56
 
@@ -74,7 +61,8 @@ export async function parse({
74
61
  envelope,
75
62
  protectedHeader,
76
63
  recipientPublicJwk,
77
- recipientPublicJwkThumbprint
64
+ recipientPublicJwkThumbprint,
65
+ mdocOptions
78
66
  };
79
67
  } catch(cause) {
80
68
  throw new BedrockError(
@@ -89,12 +77,28 @@ export async function parse({
89
77
  }
90
78
  }
91
79
 
92
- function _getDecryptParameters({exchange, clientProfileId}) {
80
+ function _getDecryptParameters({
81
+ exchange, clientProfileId, authorizationRequest, mdocOptions
82
+ }) {
93
83
  // get private key agreement keys in JWT format
94
84
  const {keyAgreementKeyPairs} = exchange.secrets?.oid4vp?.clientProfiles
95
85
  ?.[clientProfileId ?? 'default'] ?? {};
96
86
  const keys = keyAgreementKeyPairs.map(({privateKeyJwk}) => privateKeyJwk);
97
- return {keys};
87
+
88
+ // include decryption AAD info if `response_mode` calls for it
89
+ let getInfo;
90
+ if(authorizationRequest.response_mode === 'dc_api') {
91
+ // include `recipientPublicJwk` in expected mdoc handover
92
+ getInfo = ({recipientPublicJwk}) => {
93
+ const handover = {
94
+ ...mdocOptions?.expectedHandover,
95
+ recipientPublicJwk
96
+ };
97
+ return oid4vp.mdl.encodeSessionTranscript({handover});
98
+ };
99
+ }
100
+
101
+ return {keys, getInfo};
98
102
  }
99
103
 
100
104
  function _validate(validator, data) {
@@ -4,7 +4,9 @@
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as draft13 from './oid4vciDraft13.js';
6
6
  import {issue as defaultIssue, getIssueRequestsParams} from '../issue.js';
7
- import {getWorkflowIssuerInstances, setVariable} from '../helpers.js';
7
+ import {
8
+ getAlgFromJwk, getWorkflowIssuerInstances, setVariable
9
+ } from '../helpers.js';
8
10
  import {importJWK, SignJWT} from 'jose';
9
11
  import {timingSafeEqual, randomUUID as uuid} from 'node:crypto';
10
12
  import {
@@ -648,7 +650,7 @@ function _createSupportedCredentialRequests({
648
650
  async function _createOAuth2AccessToken({
649
651
  privateKeyJwk, audience, action, target, exp, iss, nbf, typ = 'at+jwt'
650
652
  }) {
651
- const alg = _getAlgFromPrivateKey({privateKeyJwk});
653
+ const alg = getAlgFromJwk({jwk: privateKeyJwk});
652
654
  const scope = `${action}:${target}`;
653
655
  const builder = new SignJWT({scope})
654
656
  .setProtectedHeader({alg, typ})
@@ -671,27 +673,6 @@ async function _createOAuth2AccessToken({
671
673
  return {accessToken, ttl};
672
674
  }
673
675
 
674
- function _getAlgFromPrivateKey({privateKeyJwk}) {
675
- if(privateKeyJwk.alg) {
676
- return privateKeyJwk.alg;
677
- }
678
- if(privateKeyJwk.kty === 'EC' && privateKeyJwk.crv) {
679
- if(privateKeyJwk.crv.startsWith('P-')) {
680
- return `ES${privateKeyJwk.crv.slice(2)}`;
681
- }
682
- if(privateKeyJwk.crv === 'secp256k1') {
683
- return 'ES256K';
684
- }
685
- }
686
- if(privateKeyJwk.kty === 'OKP' && privateKeyJwk.crv?.startsWith('Ed')) {
687
- return 'EdDSA';
688
- }
689
- if(privateKeyJwk.kty === 'RSA') {
690
- return 'PS256';
691
- }
692
- return 'invalid';
693
- }
694
-
695
676
  function _getSupportedCredentialConfigurations({workflow, exchange, step}) {
696
677
  // get all OID4VCI credential configuration IDs from issue requests in step
697
678
  const issueRequestsParams = getIssueRequestsParams({
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2026 Digital Bazaar, Inc.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import {
@@ -199,52 +199,9 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
199
199
  expectedDomain = verifyPresentationOptions.domain;
200
200
 
201
201
  const {envelope} = parseResponseResult;
202
- if(envelope?.mediaType === 'application/mdoc-vp-token' ||
203
- // deprecated
204
- envelope?.mediaType === 'application/mdl-vp-token') {
205
- // generate `handover` for mdoc verification
206
- let handover;
207
-
208
- // common `handover` parameters:
209
- const origin = authorizationRequest?.expected_origins?.[0] ??
210
- new URL(authorizationRequest.response_uri).origin;
211
- const nonce = authorizationRequest.nonce;
212
-
213
- // `direct_post.jwt` => ISO18013-7 Annex B
214
- // FIXME: same response mode is also used for OID4VP 1.0 with
215
- // `OpenID4VPHandover` for non-Annex-B; this is not yet supported
216
- // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-invocation-via-redirects
217
- const {responseMode} = parseResponseResult;
218
- if(responseMode === 'direct_post.jwt') {
219
- handover = {
220
- type: 'AnnexBHandover',
221
- // per ISO 18013-7 B the `mdocGeneratedNonce` is base64url-encoded
222
- // and put into the `apu` protected header parameter, so parse that
223
- // here and convert it to a UTF-8 string instead
224
- mdocGeneratedNonce: Buffer
225
- .from(parseResponseResult.protectedHeader?.apu ?? '', 'base64url')
226
- .toString('utf8'),
227
- clientId: authorizationRequest.client_id,
228
- responseUri: authorizationRequest.response_uri,
229
- verifierGeneratedNonce: nonce
230
- };
231
- } else if(responseMode === 'dc_api') {
232
- // `dc_api` => ISO18013-7 Annex C
233
- handover = {
234
- type: 'dcapi',
235
- origin,
236
- nonce,
237
- recipientPublicJwk: parseResponseResult.recipientPublicJwk
238
- };
239
- } else if(responseMode === 'dc_api.jwt') {
240
- // `dc_api.jwt` => ISO18013-7 Annex D
241
- handover = {
242
- type: 'OpenID4VPDCAPIHandover',
243
- origin,
244
- nonce,
245
- jwkThumbprint: parseResponseResult.recipientPublicJwkThumbprint
246
- };
247
- }
202
+ if(parseResponseResult?.mdocOptions) {
203
+ // get expected `handover` for mdoc verification
204
+ const {expectedHandover: handover} = parseResponseResult.mdocOptions;
248
205
 
249
206
  // handle deprecated `mdl` option; `mdoc` is preferred
250
207
  const mdocKey = envelope.mediaType === 'application/mdoc-vp-token' ?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "7.18.1",
3
+ "version": "7.19.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.4.0",
43
- "@digitalbazaar/oid4-client": "^5.14.0",
43
+ "@digitalbazaar/oid4-client": "^5.15.0",
44
44
  "@digitalbazaar/vc": "^7.2.0",
45
45
  "@digitalbazaar/webkms-client": "^14.2.0",
46
46
  "assert-plus": "^1.0.0",
@@ -547,6 +547,22 @@ const oid4vpClientProfile = {
547
547
  required: ['x5c'],
548
548
  additionalProperties: false,
549
549
  properties: {
550
+ privateKeyJwk: {
551
+ type: 'object',
552
+ required: ['kid', 'kty', 'crv', 'x', 'y', 'd'],
553
+ properties: {
554
+ // only ES256 supported at this time
555
+ alg: {enum: ['ES256']},
556
+ kid: {type: 'string'},
557
+ kty: {enum: ['EC']},
558
+ crv: {enum: ['P-256']},
559
+ x: {type: 'string'},
560
+ y: {type: 'string'},
561
+ d: {type: 'string'},
562
+ use: {const: 'sig'},
563
+ key_ops: {const: ['sign']}
564
+ }
565
+ },
550
566
  x5c: {
551
567
  type: 'array',
552
568
  minItems: 1,
@@ -999,6 +1015,13 @@ export function openIdAuthorizationResponseBody() {
999
1015
  not: {
1000
1016
  required: ['presentation_submission', 'vp_token', 'state']
1001
1017
  }
1018
+ }, {
1019
+ // for response_mode == 'dc_api'
1020
+ required: ['Response'],
1021
+ // cannot also use any other params
1022
+ not: {
1023
+ required: ['presentation_submission', 'vp_token', 'state']
1024
+ }
1002
1025
  }],
1003
1026
  properties: {
1004
1027
  // is a JSON string in the x-www-form-urlencoded body
@@ -1027,6 +1050,10 @@ export function openIdAuthorizationResponseBody() {
1027
1050
  // must be an encrypted JWT
1028
1051
  type: 'string'
1029
1052
  },
1053
+ Response: {
1054
+ // must be an HPKE encrypted mdoc device response (ISO 18013-7 Annex C)
1055
+ type: 'string'
1056
+ },
1030
1057
  state: {
1031
1058
  type: 'string'
1032
1059
  }