@digitalbazaar/oid4-client 5.7.2 → 5.9.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.
@@ -139,8 +139,10 @@ export function toVpr({authorizationRequest, strict = false} = {}) {
139
139
  client_id,
140
140
  client_metadata,
141
141
  dcql_query,
142
+ expected_origins,
142
143
  nonce,
143
144
  presentation_definition,
145
+ response_mode,
144
146
  response_uri
145
147
  } = authorizationRequest;
146
148
 
@@ -186,9 +188,14 @@ export function toVpr({authorizationRequest, strict = false} = {}) {
186
188
  verifiablePresentationRequest.query = [didAuthnQuery];
187
189
  }
188
190
 
189
- // map `response_uri` or `client_id` to `domain`
191
+ // map `expected_origins` or `response_uri` to `domain`
190
192
  if(response_uri !== undefined || client_id !== undefined) {
191
- verifiablePresentationRequest.domain = response_uri ?? client_id;
193
+ if(response_mode?.startsWith('dc_api') && !response_uri &&
194
+ expected_origins?.length > 0) {
195
+ verifiablePresentationRequest.domain = expected_origins[0];
196
+ } else {
197
+ verifiablePresentationRequest.domain = response_uri;
198
+ }
192
199
  }
193
200
 
194
201
  // map `nonce` to `challenge`
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  export * as oid4vp from './oid4vp/index.js';
5
5
  export * as query from './query/index.js';
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2023-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import {
5
5
  assert, assertOptional, base64Encode,
@@ -23,13 +23,15 @@ const SUPPORTED_CLIENT_ID_SCHEMES = new Set([
23
23
 
24
24
  // get an authorization request from a verifier
25
25
  export async function get({
26
- url, getTrustedCertificates, getVerificationKey, agent
26
+ url, getTrustedCertificates, getVerificationKey, getPostBody, agent,
27
+ overrideMethod
27
28
  } = {}) {
28
29
  try {
29
30
  assert(url, 'url', 'string');
30
31
 
31
32
  let authorizationRequest;
32
33
  let requestUrl;
34
+ let requestUrlMethod = 'get';
33
35
  let expectedClientId;
34
36
  if(url.startsWith('https://')) {
35
37
  // the request must be retrieved via HTTP
@@ -40,6 +42,10 @@ export async function get({
40
42
  expectedClientId = authorizationRequest.client_id;
41
43
  if(authorizationRequest.request_uri) {
42
44
  requestUrl = authorizationRequest.request_uri;
45
+ if(authorizationRequest.request_uri_method === 'post' ||
46
+ authorizationRequest.request_uri_method === 'get') {
47
+ requestUrlMethod = authorizationRequest.request_uri_method;
48
+ }
43
49
  }
44
50
  // if whole request is passed by reference, then it MUST be a signed JWT
45
51
  if(authorizationRequest.request) {
@@ -52,6 +58,11 @@ export async function get({
52
58
  }
53
59
  }
54
60
 
61
+ // allow method override
62
+ if(overrideMethod) {
63
+ requestUrlMethod = overrideMethod;
64
+ }
65
+
55
66
  // fetch request if necessary...
56
67
  let fetched = false;
57
68
  let response;
@@ -60,8 +71,9 @@ export async function get({
60
71
  fetched = true;
61
72
  ({
62
73
  payload: authorizationRequest, response, jwt
63
- } = await _fetch({
64
- requestUrl, getTrustedCertificates, getVerificationKey, agent
74
+ } = await _fetchAuthzRequest({
75
+ method: requestUrlMethod, requestUrl, getPostBody,
76
+ getTrustedCertificates, getVerificationKey, agent
65
77
  }));
66
78
  }
67
79
 
@@ -85,6 +97,17 @@ export function getClientIdScheme({authorizationRequest} = {}) {
85
97
  }
86
98
 
87
99
  export function requestsFormat({authorizationRequest, format} = {}) {
100
+ /* e.g. DCQL requesting an mdoc:
101
+ {
102
+ credentials: [{
103
+ id: 'mdl-id',
104
+ format: 'mso_mdoc',
105
+ meta: {
106
+ doctype_value: 'org.iso.18013.5.1.mDL'
107
+ }
108
+ }]
109
+ }
110
+ */
88
111
  /* e.g. presentation definition requesting an mdoc:
89
112
  {
90
113
  id: 'mdl-test-age-over-21',
@@ -98,8 +121,13 @@ export function requestsFormat({authorizationRequest, format} = {}) {
98
121
  }]
99
122
  }
100
123
  */
101
- return authorizationRequest.presentation_definition?.input_descriptors?.some(
102
- e => e?.format?.[format]);
124
+ return (
125
+ // DCQL
126
+ authorizationRequest.dcql_query?.credentials?.some(
127
+ e => e?.format === format) ||
128
+ // PE
129
+ authorizationRequest.presentation_definition?.input_descriptors?.some(
130
+ e => e?.format?.[format]));
103
131
  }
104
132
 
105
133
  export function usesClientIdScheme({authorizationRequest, scheme} = {}) {
@@ -252,8 +280,8 @@ async function _checkClientIdSchemeRequirements({
252
280
  }
253
281
 
254
282
  let {client_id: clientId} = authorizationRequest;
255
- clientId = clientId.startsWith(`${clientIdScheme}:`) ?
256
- clientId.slice(clientIdScheme.length + 2) : clientId;
283
+ clientId = clientId?.startsWith(`${clientIdScheme}:`) ?
284
+ clientId.slice(clientIdScheme.length + 1) : clientId;
257
285
 
258
286
  if(clientIdScheme === 'x509_san_dns') {
259
287
  // `x509_san_dns` requires leaf cert to have a dNSName ("domain" type) in
@@ -308,14 +336,21 @@ async function _checkClientIdSchemeRequirements({
308
336
  }
309
337
  }
310
338
 
311
- async function _fetch({
312
- requestUrl, getTrustedCertificates, getVerificationKey, agent
339
+ async function _fetchAuthzRequest({
340
+ method = 'get', requestUrl, getPostBody,
341
+ getTrustedCertificates, getVerificationKey, agent
313
342
  }) {
314
343
  // FIXME: every `fetchJSON` call needs to use a block list or other
315
344
  // protections to prevent a confused deputy attack where the `requestUrl`
316
345
  // accesses a location it should not, e.g., a URL `localhost` is used when
317
346
  // it shouldn't be
318
- const response = await fetchJSON({url: requestUrl, agent});
347
+ const options = {url: requestUrl, agent};
348
+ if(method === 'post') {
349
+ const {body} = await getPostBody?.() ?? new URLSearchParams();
350
+ options.method = 'post';
351
+ options.body = body ?? new URLSearchParams();
352
+ }
353
+ const response = await fetchJSON(options);
319
354
 
320
355
  // parse payload from response data...
321
356
  const contentType = response.headers.get('content-type');
@@ -406,6 +441,7 @@ function _parseOID4VPUrl({url}) {
406
441
  const {searchParams} = new URL(url);
407
442
  const request = _get(searchParams, 'request');
408
443
  const request_uri = _get(searchParams, 'request_uri');
444
+ const request_uri_method = _get(searchParams, 'request_uri_method');
409
445
  const response_type = _get(searchParams, 'response_type');
410
446
  const response_mode = _get(searchParams, 'response_mode');
411
447
  const presentation_definition = _get(
@@ -436,6 +472,7 @@ function _parseOID4VPUrl({url}) {
436
472
  const authorizationRequest = {
437
473
  request,
438
474
  request_uri,
475
+ request_uri_method,
439
476
  response_type,
440
477
  response_mode,
441
478
  presentation_definition: presentation_definition &&
@@ -1,9 +1,15 @@
1
1
  /*!
2
- * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2023-2026 Digital Bazaar, Inc.
3
+ *
4
+ * SPDX-License-Identifier: BSD-3-Clause
3
5
  */
6
+ import * as base64url from 'base64url-universal';
4
7
  import {createNamedError, selectJwk} from '../util.js';
5
- import {EncryptJWT} from 'jose';
8
+ import {encode as cborEncode} from 'cborg';
9
+ import {encodeSessionTranscript} from './mdl.js';
10
+ import {encrypt as hpkeEncrypt} from './hpke.js';
6
11
  import {httpClient} from '@digitalbazaar/http-client';
12
+ import {encrypt as jwtEncrypt} from './jwt.js';
7
13
  import {pathsToVerifiableCredentialPointers} from '../convert/index.js';
8
14
  import {resolvePointer} from '../query/util.js';
9
15
 
@@ -15,7 +21,7 @@ export async function create({
15
21
  verifiablePresentation,
16
22
  presentationSubmission,
17
23
  authorizationRequest,
18
- vpToken,
24
+ vpToken, vpTokenMediaType,
19
25
  encryptionOptions = {}
20
26
  } = {}) {
21
27
  if(!(verifiablePresentation || vpToken)) {
@@ -27,9 +33,10 @@ export async function create({
27
33
  // if no `vpToken` given, use VP
28
34
  vpToken = vpToken ?? JSON.stringify(verifiablePresentation);
29
35
 
30
- // if no `presentationSubmission` provided, auto-generate one
36
+ // if no `presentationSubmission` provided, auto-generate one if no
37
+ // `vpTokenMediaType` is given
31
38
  let generatedPresentationSubmission = false;
32
- if(!presentationSubmission) {
39
+ if(!presentationSubmission && vpTokenMediaType === undefined) {
33
40
  ({presentationSubmission} = createPresentationSubmission({
34
41
  presentationDefinition: authorizationRequest.presentation_definition,
35
42
  verifiablePresentation
@@ -37,29 +44,43 @@ export async function create({
37
44
  generatedPresentationSubmission = true;
38
45
  }
39
46
 
47
+ encryptionOptions = _normalizeEncryptionOptions({
48
+ authorizationRequest, encryptionOptions
49
+ });
50
+
40
51
  // prepare response body
41
52
  const body = {};
42
53
 
43
- // if `authorizationRequest.response_mode` is `direct.jwt` generate a JWT
44
- if(authorizationRequest.response_mode === 'direct_post.jwt') {
45
- if(submitsFormat({presentationSubmission, format: 'mso_mdoc'}) &&
46
- !encryptionOptions?.mdl?.sessionTranscript) {
54
+ // if `authorizationRequest.response_mode` is `direct.jwt` or
55
+ // `dc_api.jwt`, then generate a JWT
56
+ if(authorizationRequest.response_mode === 'direct_post.jwt' ||
57
+ authorizationRequest.response_mode === 'dc_api.jwt') {
58
+ // `presentationSubmission` is not used in OID4VP 1.0+
59
+ if((vpTokenMediaType === 'application/mdl-vp-token' ||
60
+ submitsFormat({presentationSubmission, format: 'mso_mdoc'})) &&
61
+ !encryptionOptions?.mdl?.handover) {
47
62
  throw createNamedError({
48
- message: '"encryptionOptions.mdl.sessionTranscript" is required ' +
49
- 'when submitting an mDL presentation.',
63
+ message: '"encryptionOptions.mdl.handover" is required ' +
64
+ 'to submit an mDL presentation.',
50
65
  name: 'DataError'
51
66
  });
52
67
  }
53
68
 
54
69
  const jwt = await _encrypt({
55
- vpToken, presentationSubmission, authorizationRequest,
56
- encryptionOptions
70
+ vpToken, presentationSubmission, authorizationRequest, encryptionOptions
57
71
  });
58
72
  body.response = jwt;
73
+ } else if(encryptionOptions?.mdl?.handover?.type === 'dcapi') {
74
+ // ISO 18013-7 Annex C (HPKE encrypted payload)
75
+ body.Response = await _encrypt({
76
+ vpToken, presentationSubmission, authorizationRequest, encryptionOptions
77
+ });
59
78
  } else {
60
- // include vp token and presentation submittion directly in body
79
+ // include vp token and presentation submission directly in body
61
80
  body.vp_token = vpToken;
62
- body.presentation_submission = JSON.stringify(presentationSubmission);
81
+ if(presentationSubmission) {
82
+ body.presentation_submission = JSON.stringify(presentationSubmission);
83
+ }
63
84
  }
64
85
 
65
86
  const authorizationResponse = body;
@@ -70,11 +91,19 @@ export async function create({
70
91
  return {authorizationResponse};
71
92
  }
72
93
 
94
+ export function selectRecipientPublicJwk({authorizationRequest} = {}) {
95
+ // get recipient public JWK from client_metadata JWK key set
96
+ const jwks = authorizationRequest?.client_metadata?.jwks;
97
+ return selectJwk({
98
+ keys: jwks?.keys, alg: 'ECDH-ES', kty: 'EC', crv: 'P-256', use: 'enc'
99
+ });
100
+ }
101
+
73
102
  export async function send({
74
103
  verifiablePresentation,
75
104
  presentationSubmission,
76
105
  authorizationRequest,
77
- vpToken,
106
+ vpToken, vpTokenMediaType,
78
107
  encryptionOptions = {},
79
108
  authorizationResponse,
80
109
  agent
@@ -90,7 +119,7 @@ export async function send({
90
119
  verifiablePresentation,
91
120
  presentationSubmission,
92
121
  authorizationRequest,
93
- vpToken,
122
+ vpToken, vpTokenMediaType,
94
123
  encryptionOptions
95
124
  }));
96
125
  } else if(verifiablePresentation || presentationSubmission || vpToken ||
@@ -204,48 +233,41 @@ export function submitsFormat({presentationSubmission, format} = {}) {
204
233
  async function _encrypt({
205
234
  vpToken, presentationSubmission, authorizationRequest, encryptionOptions
206
235
  }) {
207
- // get recipient public JWK from client_metadata JWK key set
208
- const jwks = authorizationRequest?.client_metadata?.jwks;
209
- const recipientPublicJwk = selectJwk({
210
- keys: jwks?.keys, alg: 'ECDH-ES', kty: 'EC', crv: 'P-256', use: 'enc'
211
- });
236
+ // get recipient public JWK
237
+ const recipientPublicJwk = encryptionOptions?.recipientPublicJwk ??
238
+ selectRecipientPublicJwk({authorizationRequest});
212
239
  if(!recipientPublicJwk) {
213
240
  throw createNamedError({
214
- message: 'No matching key found for "ECDH-ES" in client meta data ' +
215
- 'JWK key set.',
241
+ message:
242
+ 'No matching key found for "ECDH-ES" in client meta data JWK key set.',
216
243
  name: 'NotFoundError'
217
244
  });
218
245
  }
246
+ encryptionOptions = {...encryptionOptions, recipientPublicJwk};
219
247
 
220
- // configure `keyManagementParameters` for `EncryptJWT` API
221
- const keyManagementParameters = {};
222
- if(encryptionOptions?.mdl?.sessionTranscript) {
223
- // ISO 18013-7: include specific session transcript params as apu + apv
248
+ // determine if encrypting to a JWT or using HPKE...
249
+
250
+ // only mDL Annex C uses HPKE at this time; handover type 'dcapi' === Annex C
251
+ if(encryptionOptions?.mdl?.handover?.type === 'dcapi') {
252
+ const pt = typeof vpToken === 'string' ?
253
+ base64url.decode(vpToken) : vpToken;
254
+ // set encoded session transcript as `info`
255
+ const info = await encodeSessionTranscript({
256
+ handover: encryptionOptions.mdl.handover
257
+ });
224
258
  const {
225
- mdocGeneratedNonce,
226
- // default to using `authorizationRequest.nonce` for verifier nonce
227
- verifierGeneratedNonce = authorizationRequest.nonce
228
- } = encryptionOptions.mdl.sessionTranscript;
229
- // note: `EncryptJWT` API requires `apu/apv` (`partyInfoU`/`partyInfoV`)
230
- // to be passed as Uint8Arrays; they will be encoded using `base64url` by
231
- // that API
232
- keyManagementParameters.apu = TEXT_ENCODER.encode(mdocGeneratedNonce);
233
- keyManagementParameters.apv = TEXT_ENCODER.encode(verifierGeneratedNonce);
259
+ enc, ct: cipherText
260
+ } = await hpkeEncrypt({pt, info, encryptionOptions});
261
+ const EncryptedResponse = ['dcapi', {enc, cipherText}];
262
+ return base64url.encode(cborEncode(EncryptedResponse));
234
263
  }
235
264
 
236
- const claimSet = {
237
- vp_token: vpToken,
238
- presentation_submission: presentationSubmission
239
- };
240
- const jwt = await new EncryptJWT(claimSet)
241
- .setProtectedHeader({
242
- alg: 'ECDH-ES',
243
- enc: encryptionOptions?.enc ?? 'A256GCM',
244
- kid: recipientPublicJwk.kid
245
- })
246
- .setKeyManagementParameters(keyManagementParameters)
247
- .encrypt(recipientPublicJwk);
248
- return jwt;
265
+ // all other cases presently use JWT
266
+ const payload = {vp_token: vpToken};
267
+ if(presentationSubmission) {
268
+ payload.presentation_submission = presentationSubmission;
269
+ }
270
+ return jwtEncrypt({payload, encryptionOptions});
249
271
  }
250
272
 
251
273
  function _filterToValue({filter, strict = false}) {
@@ -377,3 +399,53 @@ function _matchesInputDescriptor({
377
399
 
378
400
  return true;
379
401
  }
402
+
403
+ function _normalizeEncryptionOptions({
404
+ authorizationRequest, encryptionOptions
405
+ }) {
406
+ if(encryptionOptions?.mdl?.sessionTranscript &&
407
+ !encryptionOptions?.mdl.handover) {
408
+ // deprecated Annex B style handover info
409
+ encryptionOptions = {
410
+ ...encryptionOptions,
411
+ mdl: {
412
+ ...encryptionOptions.mdl,
413
+ handover: {
414
+ type: 'AnnexBHandover',
415
+ ...encryptionOptions.mdl.sessionTranscript
416
+ }
417
+ }
418
+ };
419
+ }
420
+
421
+ // configure `keyManagementParameters` for `EncryptJWT` API
422
+ if(!encryptionOptions?.keyManagementParameters) {
423
+ const keyManagementParameters = {};
424
+ if(encryptionOptions?.mdl?.handover) {
425
+ // ISO 18013-7 Annex B has specific handover params for apu + apv; for
426
+ // Annex D generate `apu` and use `nonce` for `apv` but this isn't a
427
+ // requirement; Annex C uses HPKE not a JWT so not relevant here
428
+ const {
429
+ mdocGeneratedNonce,
430
+ nonce,
431
+ verifierGeneratedNonce
432
+ } = encryptionOptions.mdl.handover;
433
+
434
+ // generate 128-bit random `apu` if no `mdocGeneratedNonce` provided
435
+ const apu = mdocGeneratedNonce ??
436
+ globalThis.crypto.getRandomValues(new Uint8Array(16));
437
+ // default to using `authorizationRequest.nonce` for verifier nonce
438
+ const apv = verifierGeneratedNonce ?? nonce ?? authorizationRequest.nonce;
439
+ // note: `EncryptJWT` API requires `apu/apv` (`partyInfoU`/`partyInfoV`)
440
+ // to be passed as Uint8Arrays; they will be encoded using `base64url` by
441
+ // that API
442
+ keyManagementParameters.apu = typeof apu === 'string' ?
443
+ TEXT_ENCODER.encode(apu) : apu;
444
+ keyManagementParameters.apv = typeof apv === 'string' ?
445
+ TEXT_ENCODER.encode(apv) : apv;
446
+ }
447
+ encryptionOptions = {...encryptionOptions, keyManagementParameters};
448
+ }
449
+
450
+ return encryptionOptions;
451
+ }
@@ -0,0 +1,116 @@
1
+ /*!
2
+ * Copyright (c) 2023-2026 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {
5
+ Aes128Gcm,
6
+ CipherSuite,
7
+ DhkemP256HkdfSha256,
8
+ HkdfSha256
9
+ } from '@hpke/core';
10
+ import {createNamedError, selectJwk} from '../util.js';
11
+ import {importJWK} from 'jose';
12
+
13
+ // `enc`: encapsulated sender public key
14
+ // `ct`: cipher text
15
+ export async function decrypt({enc, ct, getDecryptParameters}) {
16
+ if(typeof getDecryptParameters !== 'function') {
17
+ throw new TypeError(
18
+ '"getDecryptParameters" is required for "direct_post.jwt" ' +
19
+ 'response mode.');
20
+ }
21
+
22
+ // load decryption parameters
23
+ const params = await getDecryptParameters({enc});
24
+ const {keys} = params;
25
+ let {getKey} = params;
26
+ let recipientPublicJwk;
27
+ if(!getKey) {
28
+ // process `enc` to find key
29
+ getKey = async ({enc}) => {
30
+ // import sender key and export as JWK to find a matching recipient key
31
+ // for decryption
32
+ const jwk = await _rawPublicKeyToJwk({rawPublicKey: enc});
33
+ const match = selectJwk({keys, kty: jwk.kty, crv: jwk.crv});
34
+ if(!match) {
35
+ throw createNamedError({
36
+ message: 'No matching recipient cryptographic key found.',
37
+ name: 'NotSupportedError',
38
+ details: {httpStatusCode: 400, public: true}
39
+ });
40
+ }
41
+ const {d, ...rest} = match;
42
+ const recipientSecretJwk = {...rest, d};
43
+ recipientPublicJwk = {alg: 'ECDH-ES', use: 'enc', ...rest};
44
+ return importJWK(recipientSecretJwk, 'ECDH-ES', {extractable: true});
45
+ };
46
+ }
47
+ const recipientKey = await getKey({enc});
48
+ const info = await params?.getInfo?.({enc, recipientPublicJwk});
49
+ const aad = await params?.getAad?.({enc, info, recipientPublicJwk});
50
+
51
+ try {
52
+ // open: ciphertext + encapsulated key => plaintext
53
+ const suite = _createCiphersuite();
54
+ const recipient = await suite.createRecipientContext({
55
+ recipientKey, enc, info
56
+ });
57
+ const pt = new Uint8Array(await recipient.open(ct, aad));
58
+ return {pt, recipientPublicJwk};
59
+ } catch(cause) {
60
+ throw createNamedError({
61
+ message: `Decryption failed.`,
62
+ name: 'DataError',
63
+ details: {httpStatusCode: 400, public: true},
64
+ cause
65
+ });
66
+ }
67
+ }
68
+
69
+ export async function encrypt({pt, info, aad, encryptionOptions}) {
70
+ if(encryptionOptions?.enc && !(encryptionOptions.enc !== 'A128GCM')) {
71
+ throw createNamedError({
72
+ message:
73
+ `Unsupported encryption algorithm "${encryptionOptions.enc}"; ` +
74
+ 'only "A128GCM" is supported.',
75
+ name: 'DataError'
76
+ });
77
+ }
78
+
79
+ // import recipient public key
80
+ const {recipientPublicJwk} = encryptionOptions;
81
+ const recipientPublicKey = await importJWK(recipientPublicJwk, 'ECDH-ES');
82
+
83
+ // seal: plaintext => ciphertext + encapsulated key
84
+ const suite = _createCiphersuite();
85
+ const sender = await suite.createSenderContext({
86
+ recipientPublicKey, info
87
+ });
88
+ const ct = await sender.seal(pt, aad);
89
+ return {enc: sender.enc, ct};
90
+ }
91
+
92
+ // only supported cipher suite
93
+ function _createCiphersuite() {
94
+ return new CipherSuite({
95
+ kem: new DhkemP256HkdfSha256(),
96
+ kdf: new HkdfSha256(),
97
+ aead: new Aes128Gcm()
98
+ });
99
+ }
100
+
101
+ async function _rawPublicKeyToJwk({rawPublicKey}) {
102
+ try {
103
+ const publicKey = await globalThis.crypto.subtle.importKey(
104
+ 'raw', rawPublicKey, {name: 'ECDH', namedCurve: 'P-256'},
105
+ true, []);
106
+ const jwk = await globalThis.crypto.subtle.exportKey('jwk', publicKey);
107
+ return jwk;
108
+ } catch(e) {
109
+ throw createNamedError({
110
+ message:
111
+ 'Unsupported public key; it must be "ECDH-ES" with curve "P-256".',
112
+ name: 'NotSupportedError',
113
+ details: {httpStatusCode: 400, public: true}
114
+ });
115
+ }
116
+ }
@@ -1,9 +1,10 @@
1
1
  /*!
2
- * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2023-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  export * as authzRequest from './authorizationRequest.js';
5
5
  export * as authzResponse from './authorizationResponse.js';
6
6
  export * as convert from '../convert/index.js';
7
+ export * as mdl from './mdl.js';
7
8
  export * as verifier from './verifier.js';
8
9
 
9
10
  // backwards compatibility APIs
@@ -0,0 +1,66 @@
1
+ /*!
2
+ * Copyright (c) 2023-2026 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {createNamedError, selectJwk} from '../util.js';
5
+ import {EncryptJWT, importJWK, jwtDecrypt} from 'jose';
6
+
7
+ export async function decrypt({jwt, getDecryptParameters}) {
8
+ if(typeof getDecryptParameters !== 'function') {
9
+ throw new TypeError(
10
+ '"getDecryptParameters" is required for "direct_post.jwt" ' +
11
+ 'response mode.');
12
+ }
13
+
14
+ const params = await getDecryptParameters({jwt});
15
+ const {keys} = params;
16
+ let {getKey} = params;
17
+ let recipientPublicJwk;
18
+ if(!getKey) {
19
+ // note: `jose` lib's JWK key set feature cannot be used and passed to
20
+ // `jwtDecrypt()` as the second parameter because the expected `alg`
21
+ // "ECDH-ES" is not a unsupported algorithm for selecting a key from a set
22
+ getKey = protectedHeader => {
23
+ if(protectedHeader.alg !== 'ECDH-ES') {
24
+ throw createNamedError({
25
+ message: `Unsupported algorithm "${protectedHeader.alg}"; ` +
26
+ 'algorithm must be "ECDH-ES".',
27
+ name: 'NotSupportedError',
28
+ details: {httpStatusCode: 400, public: true}
29
+ });
30
+ }
31
+ const {d, ...rest} = selectJwk({keys, kid: protectedHeader.kid});
32
+ const recipientSecretJwk = {...rest, d};
33
+ recipientPublicJwk = {alg: 'ECDH-ES', use: 'enc', ...rest};
34
+ return importJWK(recipientSecretJwk, 'ECDH-ES');
35
+ };
36
+ }
37
+
38
+ try {
39
+ const {payload, protectedHeader} = await jwtDecrypt(jwt, getKey, {
40
+ // only supported algorithms at this time:
41
+ contentEncryptionAlgorithms: ['A256GCM', 'A128GCM'],
42
+ keyManagementAlgorithms: ['ECDH-ES']
43
+ });
44
+ return {payload, protectedHeader, recipientPublicJwk};
45
+ } catch(cause) {
46
+ throw createNamedError({
47
+ message: `Decryption failed.`,
48
+ name: 'DataError',
49
+ details: {httpStatusCode: 400, public: true},
50
+ cause
51
+ });
52
+ }
53
+ }
54
+
55
+ export async function encrypt({payload, encryptionOptions}) {
56
+ const {keyManagementParameters, recipientPublicJwk} = encryptionOptions;
57
+ const jwt = await new EncryptJWT(payload)
58
+ .setProtectedHeader({
59
+ alg: 'ECDH-ES',
60
+ enc: encryptionOptions?.enc ?? 'A256GCM',
61
+ kid: recipientPublicJwk.kid
62
+ })
63
+ .setKeyManagementParameters(keyManagementParameters)
64
+ .encrypt(recipientPublicJwk);
65
+ return jwt;
66
+ }
@@ -0,0 +1,163 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as base64url from 'base64url-universal';
5
+ import {decode as cborDecode, encode as cborEncode, Token, Type} from 'cborg';
6
+ import {createNamedError, jwkToCoseKey, sha256} from '../util.js';
7
+ import {calculateJwkThumbprint} from 'jose';
8
+ import {decrypt as hpkeDecrypt} from './hpke.js';
9
+
10
+ const TEXT_ENCODER = new TextEncoder();
11
+
12
+ /**
13
+ * Encodes a `SessionTranscript` for use with mDL (ISO 18013-7 variants).
14
+ *
15
+ * The `handover` parameter's properties, other than `type`, depend on the
16
+ * value of `type`:
17
+ *
18
+ * For 'AnnexBHandover':
19
+ * mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce
20
+ * For 'OpenID4VPDCAPIHandover':
21
+ * origin, nonce, jwkThumbprint
22
+ * For 'dcapi':
23
+ * nonce, recipientPublicKey.
24
+ *
25
+ * @param {object} options - The options.
26
+ * @param {object} options.handover - The handover options to use in the
27
+ * session transcript, including the `type` and any type-specific properties;
28
+ * the `type` can be any of the following:
29
+ * 'AnnexBHandover' for (ISO 18013-7 Annex B),
30
+ * 'OpenID4VPDCAPIHandover' for ISO 18013-7 Annex D "Google DC API",
31
+ * 'dcapi' for ISO 18013-7 Annex C "Apple DC API".
32
+ *
33
+ * @returns {Promise<Uint8Array>} The cbor-encoded session transcript.
34
+ */
35
+ export async function encodeSessionTranscript({handover} = {}) {
36
+ // produce `Handover` component of mDL session transcript
37
+ let Handover;
38
+ if(handover.type === 'AnnexBHandover') {
39
+ Handover = await _encodeAnnexBHandover({handover});
40
+ } else if(handover.type === 'dcapi') {
41
+ Handover = await _encodeAnnexCHandover({handover});
42
+ } else if(handover.type === 'OpenID4VPDCAPIHandover') {
43
+ Handover = await _encodeAnnexDHandover({handover});
44
+ } else {
45
+ throw new Error(`Unknown handover type "${handover.type}".`);
46
+ }
47
+
48
+ // create session transcript which is always:
49
+ // `[DeviceEngagementBytes, EReaderKeyBytes, Handover]`
50
+ // where `DeviceEngagementBytes` and `EReaderKeyBytes` are `null`
51
+ const sessionTranscript = [null, null, Handover];
52
+
53
+ // session transcript bytes are encoded as a CBOR data item within a byte
54
+ // string (CBOR Tag 24):
55
+ const dataItem = cborEncode(sessionTranscript);
56
+ return cborEncode(dataItem, {
57
+ typeEncoders: {Uint8Array: createTag24Encoder(dataItem)}
58
+ });
59
+ }
60
+
61
+ export async function decryptAnnexCResponse({
62
+ base64urlEncryptedResponse, getDecryptParameters
63
+ } = {}) {
64
+ // ISO 18013-7 Annex C, with hpke-encrypted payload
65
+ const EncryptedResponse = cborDecode(
66
+ base64url.decode(base64urlEncryptedResponse));
67
+ const [protocol] = EncryptedResponse;
68
+ if(protocol !== 'dcapi') {
69
+ throw createNamedError({
70
+ message: `Unsupported encryption protocol "${protocol}".`,
71
+ name: 'NotSupportedError'
72
+ });
73
+ }
74
+ const [, {enc, cipherText: ct}] = EncryptedResponse;
75
+ return hpkeDecrypt({enc, ct, getDecryptParameters});
76
+ }
77
+
78
+ function createTag24Encoder(value) {
79
+ return function tag24Encoder(obj) {
80
+ if(obj !== value) {
81
+ return null;
82
+ }
83
+ return [
84
+ new Token(Type.tag, 24),
85
+ new Token(Type.bytes, obj)
86
+ ];
87
+ };
88
+ }
89
+
90
+ // encode `handover` as ISO 18013-7 Annex B Handover
91
+ async function _encodeAnnexBHandover({handover}) {
92
+ const {
93
+ mdocGeneratedNonce,
94
+ clientId,
95
+ responseUri,
96
+ verifierGeneratedNonce
97
+ } = handover;
98
+ return [mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce];
99
+ }
100
+
101
+ // encode `handover` as ISO 18013-7 Annex C Handover
102
+ async function _encodeAnnexCHandover({handover}) {
103
+ /* Details:
104
+
105
+ Handover = `['dcapi', dcapiInfoHash]`
106
+ dcapiInfo = [Base64EncryptionInfo, SerializedOrigin]
107
+
108
+ SerializedOrigin = tstr
109
+ dcapiInfoHash = bstr
110
+
111
+ `Base64EncryptionInfo` is the base64url-no-pad encoding of the cbor-encoded
112
+ `EncryptionInfo`.
113
+
114
+ EncryptionInfo = [
115
+ // encryption protocol identifier
116
+ 'dcapi',
117
+ EncryptionParameters
118
+ ]
119
+
120
+ EncryptionParameters = {
121
+ // binary string
122
+ nonce,
123
+ // COSE key
124
+ recipientPublicKey
125
+ }
126
+ */
127
+ const {origin, nonce} = handover;
128
+ // if `recipientPublicKey` is not present, convert it from
129
+ // `recipientPublicKeyJwk`
130
+ const recipientPublicKey = handover.recipientPublicKey ??
131
+ jwkToCoseKey({jwk: handover.recipientPublicJwk});
132
+ const nonceBytes = typeof nonce === 'string' ?
133
+ TEXT_ENCODER.encode(nonce) : nonce;
134
+ const EncryptionParameters = [nonceBytes, recipientPublicKey];
135
+ const EncryptionInfo = ['dcapi', EncryptionParameters];
136
+ const Base64EncryptionInfo = base64url.encode(cborEncode(EncryptionInfo));
137
+ const dcapiInfo = [Base64EncryptionInfo, origin];
138
+ const dcapiInfoHash = await sha256(cborEncode(dcapiInfo));
139
+ return ['dcapi', dcapiInfoHash];
140
+ }
141
+
142
+ // encode `handover` as ISO 18013-7 Annex D Handover
143
+ async function _encodeAnnexDHandover({handover}) {
144
+ // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#appendix-B.2.6.2
145
+ // for `"response_mode": "dc_api"`, `jwkThumbprint` MUST be `null`
146
+ // for `"response_mode": "dc_api.jwt"`, `jwkThumbprint` MUST be the JWK
147
+ // SHA-256 Thumbprint of the verifier's public key used to encrypt
148
+ // the response (as a Uint8Array)
149
+ const {origin, nonce} = handover;
150
+
151
+ // calculate thumbprint if only `recipientPublicJwk` given
152
+ if(!handover.jwkThumbprint && handover.recipientPublicJwk) {
153
+ handover = {
154
+ ...handover,
155
+ jwkThumbprint: await calculateJwkThumbprint(handover.recipientPublicJwk)
156
+ };
157
+ }
158
+ const jwkThumbprint = typeof handover.jwkThumbprint === 'string' ?
159
+ base64url.decode(handover.jwkThumbprint) : handover.jwkThumbprint;
160
+ const handoverInfo = [origin, nonce, jwkThumbprint];
161
+ const handoverInfoHash = await sha256(cborEncode(handoverInfo));
162
+ return ['OpenID4VPDCAPIHandover', handoverInfoHash];
163
+ }
@@ -1,57 +1,115 @@
1
1
  /*!
2
- * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2023-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
- import {createNamedError, parseJSON, selectJwk} from '../util.js';
5
- import {importJWK, jwtDecrypt} from 'jose';
4
+ import * as base64url from 'base64url-universal';
5
+ import {createNamedError, parseJSON} from '../util.js';
6
+ import {calculateJwkThumbprint} from 'jose';
7
+ import {decryptAnnexCResponse} from './mdl.js';
8
+ import {decrypt as jwtDecrypt} from './jwt.js';
9
+
10
+ // start of JSON object, array, or string
11
+ const VP_TOKEN_JSON_PREFIXES = new Set(['{', '[', '"']);
6
12
 
7
13
  // parses (and decrypts) an authz response from a response body object
8
14
  export async function parseAuthorizationResponse({
9
15
  body = {},
10
- supportedResponseModes = ['direct_post.jwt', 'direct_post'],
11
- getDecryptParameters
16
+ getDecryptParameters,
17
+ authorizationRequest,
18
+ // only used if `authorizationRequest.response_mode` is not set, otherwise
19
+ // the response must match the authz request's response mode
20
+ supportedResponseModes = [
21
+ 'direct_post.jwt', 'direct_post', 'dc_api.jwt', 'dc_api'
22
+ ]
12
23
  }) {
13
24
  let responseMode;
14
25
  const parsed = {};
26
+ let vpTokenMediaType = 'application/octet-stream';
15
27
  let payload;
16
28
  let protectedHeader;
29
+ let recipientPublicJwk;
17
30
 
18
- supportedResponseModes = new Set(supportedResponseModes);
31
+ supportedResponseModes = new Set(authorizationRequest?.response_mode ?
32
+ [authorizationRequest.response_mode] : supportedResponseModes);
19
33
 
20
34
  if(body.response) {
21
- // `body.response` is present which must contain an encrypted JWT
22
- responseMode = 'direct_post.jwt';
35
+ // `body.response` is present which must contain an encrypted JWT;
36
+ // response mode can also be `dc_api.jwt` here, but distinction can only
37
+ // be made if `authorizationRequest` was passed
38
+ responseMode = authorizationRequest?.response_mode === 'dc_api.jwt' ?
39
+ 'dc_api.jwt' : 'direct_post.jwt';
23
40
  _assertSupportedResponseMode({responseMode, supportedResponseModes});
24
41
  const jwt = body.response;
25
42
  ({
26
43
  payload,
27
- protectedHeader
28
- } = await _decrypt({jwt, getDecryptParameters}));
44
+ protectedHeader,
45
+ recipientPublicJwk
46
+ } = await jwtDecrypt({jwt, getDecryptParameters}));
29
47
  parsed.presentationSubmission = payload.presentation_submission;
48
+ } else if(body.Response) {
49
+ // ISO 18013-7 Annex C, with hpke-encrypted payload
50
+ responseMode = 'dc_api';
51
+ _assertSupportedResponseMode({responseMode, supportedResponseModes});
52
+ const base64urlEncryptedResponse = body.Response;
53
+ ({pt: payload, recipientPublicJwk} = await decryptAnnexCResponse({
54
+ base64urlEncryptedResponse, getDecryptParameters
55
+ }));
56
+ // normalize payload to base64url-encoded mDL device response
57
+ parsed.vpToken = base64url.encode(payload);
58
+ vpTokenMediaType = 'application/mdl-vp-token';
30
59
  } else {
31
60
  responseMode = 'direct_post';
32
61
  _assertSupportedResponseMode({responseMode, supportedResponseModes});
33
62
  payload = body;
34
- parsed.presentationSubmission = parseJSON(
35
- payload.presentation_submission, 'presentation_submission');
63
+ if(payload.presentation_submission) {
64
+ parsed.presentationSubmission = parseJSON(
65
+ payload.presentation_submission, 'presentation_submission');
66
+ }
36
67
  }
37
68
 
38
- // `vp_token` is either:
39
- // 1. a JSON object (a VP)
40
- // 2. a JSON array (of something)
41
- // 3. a JSON string (a quoted JWT: "<JWT>")
42
- // 4. a JWT
43
- // 5. a base64url-encoded mDL device response
44
- // 6. unknown
45
- const {vp_token} = payload;
46
- if(typeof vp_token === 'string' &&
47
- (vp_token.startsWith('{') || vp_token.startsWith('[') ||
48
- vp_token.startsWith('"'))) {
49
- parsed.vpToken = parseJSON(vp_token, 'vp_token');
50
- } else {
51
- parsed.vpToken = vp_token;
69
+ // if payload is set but not a Uint8Array (ISO 18013-7 Annex C case)...
70
+ if(payload && !(payload instanceof Uint8Array)) {
71
+ // `vp_token` is either:
72
+ // 1. a JSON object (a VP)
73
+ // 2. a JSON array (of something; unknown media type)
74
+ // 3. a JSON string (a quoted JWT: "<JWT>")
75
+ // 4. a JWT (starts with 'ey'...)
76
+ // 5. a base64url-encoded mDL device response
77
+ // 6. unknown
78
+ const {vp_token} = payload;
79
+ if(typeof vp_token === 'string') {
80
+ if(VP_TOKEN_JSON_PREFIXES.has(vp_token[0])) {
81
+ // cases: 1-3 - JSON
82
+ parsed.vpToken = parseJSON(vp_token, 'vp_token');
83
+ if(typeof parsed.vpToken === 'string') {
84
+ vpTokenMediaType = 'application/jwt';
85
+ } else if(!Array.isArray(parsed.vpToken)) {
86
+ vpTokenMediaType = 'application/vp';
87
+ }
88
+ } else {
89
+ // cases 4-5: JWT or mDL device response
90
+ parsed.vpToken = vp_token;
91
+ // if does not look like a JWT, assume mDL device response
92
+ vpTokenMediaType = vp_token.startsWith('ey') ?
93
+ 'application/jwt' : 'application/mdl-vp-token';
94
+ }
95
+ } else {
96
+ // unknown case
97
+ parsed.vpToken = vp_token;
98
+ }
52
99
  }
53
100
 
54
- return {responseMode, parsed, payload, protectedHeader};
101
+ // calculate JWK thumbprint for recipient public key, if any
102
+ let recipientPublicJwkThumbprint;
103
+ if(recipientPublicJwk) {
104
+ recipientPublicJwkThumbprint = await calculateJwkThumbprint(
105
+ recipientPublicJwk);
106
+ }
107
+
108
+ return {
109
+ responseMode, parsed, payload, protectedHeader,
110
+ recipientPublicJwk, recipientPublicJwkThumbprint,
111
+ vpTokenMediaType
112
+ };
55
113
  }
56
114
 
57
115
  function _assertSupportedResponseMode({
@@ -60,43 +118,8 @@ function _assertSupportedResponseMode({
60
118
  if(!supportedResponseModes.has(responseMode)) {
61
119
  throw createNamedError({
62
120
  message: `Unsupported response mode "${responseMode}".`,
63
- name: 'NotSupportedError'
121
+ name: 'NotSupportedError',
122
+ details: {httpStatusCode: 400, public: true}
64
123
  });
65
124
  }
66
125
  }
67
-
68
- async function _decrypt({jwt, getDecryptParameters}) {
69
- if(typeof getDecryptParameters !== 'function') {
70
- throw new TypeError(
71
- '"getDecryptParameters" is required for "direct_post.jwt" ' +
72
- 'response mode.');
73
- }
74
-
75
- const params = await getDecryptParameters({jwt});
76
- const {keys} = params;
77
- let {getKey} = params;
78
- if(!getKey) {
79
- // note: `jose` lib's JWK key set feature cannot be used and passed to
80
- // `jwtDecrypt()` as the second parameter because the expected `alg`
81
- // "ECDH-ES" is not a unsupported algorithm for selecting a key from a set
82
- getKey = protectedHeader => {
83
- if(protectedHeader.alg !== 'ECDH-ES') {
84
- const error = createNamedError({
85
- message: `Unsupported algorithm "${protectedHeader.alg}"; ` +
86
- 'algorithm must be "ECDH-ES".',
87
- name: 'NotSupportedError',
88
- details: {httpStatusCode: 400, public: true}
89
- });
90
- throw error;
91
- }
92
- const jwk = selectJwk({keys, kid: protectedHeader.kid});
93
- return importJWK(jwk, 'ECDH-ES');
94
- };
95
- }
96
-
97
- return jwtDecrypt(jwt, getKey, {
98
- // only supported algorithms at this time:
99
- contentEncryptionAlgorithms: ['A256GCM', 'A128GCM'],
100
- keyManagementAlgorithms: ['ECDH-ES']
101
- });
102
- }
@@ -6,6 +6,7 @@ import {exampleToJsonPointerMap} from './queryByExample.js';
6
6
  import {JSONPath} from 'jsonpath-plus';
7
7
  import jsonpointer from 'json-pointer';
8
8
 
9
+ const MDOC_MDL = 'org.iso.18013.5.1.mDL';
9
10
  const VALUE_TYPES = new Set(['string', 'number', 'boolean']);
10
11
  const SUPPORTED_JWT_ALGS = ['EdDSA', 'Ed25519', 'ES256', 'ES384'];
11
12
 
@@ -98,6 +99,15 @@ export function vprGroupsToPresentationDefinition({
98
99
  };
99
100
  acceptsVcJwt = true;
100
101
  }
102
+ if(envelopes?.includes('application/mdl') ||
103
+ envelopes?.includes('application/mdoc')) {
104
+ inputDescriptor.format.mso_mdoc = {};
105
+ // `inputDescriptor` MUST be `org.iso.18013.5.1.mDL` for an mDL
106
+ // query for ecosystem compatibility
107
+ if(envelopes?.includes('application/mdl')) {
108
+ inputDescriptor.id = MDOC_MDL;
109
+ }
110
+ }
101
111
  if(cryptosuites?.length > 0) {
102
112
  inputDescriptor.format.ldp_vc = {
103
113
  proof_type: cryptosuites
package/lib/util.js CHANGED
@@ -47,7 +47,7 @@ export function createNamedError({message, name, details, cause} = {}) {
47
47
  return error;
48
48
  }
49
49
 
50
- export function fetchJSON({url, agent} = {}) {
50
+ export function fetchJSON({method = 'get', url, body, json, agent} = {}) {
51
51
  // allow these params to be passed / configured
52
52
  const fetchOptions = {
53
53
  // max size for issuer config related responses (in bytes, ~4 KiB)
@@ -57,9 +57,59 @@ export function fetchJSON({url, agent} = {}) {
57
57
  agent
58
58
  };
59
59
 
60
+ if(method === 'post') {
61
+ if(body !== undefined) {
62
+ fetchOptions.body = body;
63
+ } else if(json !== undefined) {
64
+ fetchOptions.json = json;
65
+ }
66
+ return httpClient.post(url, fetchOptions);
67
+ }
60
68
  return httpClient.get(url, fetchOptions);
61
69
  }
62
70
 
71
+ // only supports keys used w/mDL at this time
72
+ export function jwkToCoseKey({jwk} = {}) {
73
+ if(!(jwk.kty === 'EC' && (jwk.crv === 'P-256' || jwk.crv === 'P-384'))) {
74
+ throw new Error(
75
+ 'Unknown supported JWK for mDL encryption; ' +
76
+ 'only EC P-256 or P-384 are accepted.');
77
+ }
78
+
79
+ // https://datatracker.ietf.org/doc/html/rfc9053
80
+ return {
81
+ // `kty`; only types supported for mDL:
82
+ // `EC2` 2
83
+ 1: 2,
84
+ // `crv`; only curves supported for mDL:
85
+ // `P-256` 1
86
+ // `P-384` 2
87
+ '-1': jwk.crv === 'P-256' ? 1 : 2,
88
+ // `x`
89
+ '-2': base64url.decode(jwk.x),
90
+ // `y`
91
+ '-3': base64url.decode(jwk.y)
92
+ };
93
+ }
94
+
95
+ export function coseKeyToJwk({coseKey} = {}) {
96
+ const {1: kty, '-1': crv, '-2': x, '-3': y} = coseKey;
97
+
98
+ if(!(kty === 'EC' && (crv === 'P-256' || crv === 'P-384'))) {
99
+ throw new Error(
100
+ 'Unknown supported COSE key for mDL decryption; ' +
101
+ 'only EC P-256 or P-384 are accepted.');
102
+ }
103
+
104
+ // https://datatracker.ietf.org/doc/html/rfc9053
105
+ return {
106
+ kty,
107
+ crv,
108
+ x: base64url.encode(x),
109
+ y: base64url.encode(y)
110
+ };
111
+ }
112
+
63
113
  export function parseJSON(x, name) {
64
114
  try {
65
115
  return JSON.parse(x);
@@ -73,7 +123,9 @@ export function parseJSON(x, name) {
73
123
  }
74
124
  }
75
125
 
76
- export function selectJwk({keys, kid, alg, kty, crv, use} = {}) {
126
+ export function selectJwk({
127
+ keys, kid, alg, kty, crv, use, x, y
128
+ } = {}) {
77
129
  /* Example JWKs "keys":
78
130
  "jwks": {
79
131
  "keys": [
@@ -103,16 +155,21 @@ export function selectJwk({keys, kid, alg, kty, crv, use} = {}) {
103
155
  const kty1 = kty ?? jwk.kty;
104
156
  const crv1 = crv ?? jwk.crv;
105
157
  const use1 = use ?? jwk.use;
158
+ const x1 = x ?? jwk.x;
159
+ const y1 = y ?? jwk.y;
106
160
  const {
107
161
  // default missing `alg` value in `jwk` to search value
108
162
  alg: alg2 = alg1,
109
163
  kty: kty2,
110
164
  crv: crv2,
111
165
  // default missing `use` value in `jwk` to search value
112
- use: use2 = use1
166
+ use: use2 = use1,
167
+ x: x2,
168
+ y: y2
113
169
  } = jwk;
114
170
  // return if `jwk` matches computed values
115
- return alg1 === alg2 && kty1 === kty2 && crv1 === crv2 && use1 === use2;
171
+ return alg1 === alg2 && kty1 === kty2 && crv1 === crv2 && use1 === use2 &&
172
+ x1 == x2 && y1 === y2;
116
173
  });
117
174
  }
118
175
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalbazaar/oid4-client",
3
- "version": "5.7.2",
3
+ "version": "5.9.0",
4
4
  "description": "An OID4 (VC + VP) client",
5
5
  "homepage": "https://github.com/digitalbazaar/oid4-client",
6
6
  "author": {
@@ -24,7 +24,9 @@
24
24
  ],
25
25
  "dependencies": {
26
26
  "@digitalbazaar/http-client": "^4.0.0",
27
+ "@hpke/core": "^1.7.5",
27
28
  "base64url-universal": "^2.0.0",
29
+ "cborg": "^4.5.8",
28
30
  "jose": "^6.1.0",
29
31
  "json-pointer": "^0.6.2",
30
32
  "jsonpath-plus": "^10.3.0",