@digitalbazaar/oid4-client 4.4.0 → 5.0.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.
@@ -2,12 +2,25 @@
2
2
  * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import {
5
- assert, assertOptional, createNamedError, fetchJSON, selectJwk
5
+ assert, assertOptional, base64Encode, createNamedError, fetchJSON, selectJwk
6
6
  } from './util.js';
7
- import {decodeJwt, jwtVerify} from 'jose';
7
+ import {decodeJwt, importX509, jwtVerify} from 'jose';
8
+ import {
9
+ hasDomainSubjectAltName, parseCertificateChain, verifyCertificateChain
10
+ } from './x509.js';
11
+
12
+ const REQUIRED_SIGNED_AUTHZ_REQUEST_CLIENT_ID_SCHEMES = new Set([
13
+ 'x509_san_dns', 'x509_hash', 'did', 'decentralized_identifier'
14
+ ]);
15
+ const SUPPORTED_CLIENT_ID_SCHEMES = new Set([
16
+ 'redirect_uri',
17
+ 'x509_san_dns', 'x509_hash', 'did', 'decentralized_identifier'
18
+ ]);
8
19
 
9
20
  // get an authorization request from a verifier
10
- export async function get({url, getVerificationKey, agent} = {}) {
21
+ export async function get({
22
+ url, getTrustedCertificates, getVerificationKey, agent
23
+ } = {}) {
11
24
  try {
12
25
  assert(url, 'url', 'string');
13
26
 
@@ -28,6 +41,7 @@ export async function get({url, getVerificationKey, agent} = {}) {
28
41
  if(authorizationRequest.request) {
29
42
  authorizationRequest = await _parseJwt({
30
43
  jwt: authorizationRequest.request,
44
+ getTrustedCertificates,
31
45
  getVerificationKey,
32
46
  signatureRequired: true
33
47
  });
@@ -42,15 +56,14 @@ export async function get({url, getVerificationKey, agent} = {}) {
42
56
  fetched = true;
43
57
  ({
44
58
  payload: authorizationRequest, response, jwt
45
- } = await _fetch({requestUrl, getVerificationKey, agent}));
59
+ } = await _fetch({
60
+ requestUrl, getTrustedCertificates, getVerificationKey, agent
61
+ }));
46
62
  }
47
63
 
48
64
  // ensure authorization request is valid
49
65
  validate({authorizationRequest, expectedClientId});
50
66
 
51
- // resolve and validate any additional parameters in the request
52
- authorizationRequest = await resolveParams({authorizationRequest, agent});
53
-
54
67
  return {authorizationRequest, fetched, requestUrl, response, jwt};
55
68
  } catch(cause) {
56
69
  const message = cause.data?.error_description ?? cause.message;
@@ -62,6 +75,11 @@ export async function get({url, getVerificationKey, agent} = {}) {
62
75
  }
63
76
  }
64
77
 
78
+ export function getClientIdScheme({authorizationRequest} = {}) {
79
+ return authorizationRequest.client_id_scheme ??
80
+ authorizationRequest.client_id?.split(':')[0];
81
+ }
82
+
65
83
  export function requestsFormat({authorizationRequest, format} = {}) {
66
84
  /* e.g. presentation definition requesting an mdoc:
67
85
  {
@@ -80,84 +98,11 @@ export function requestsFormat({authorizationRequest, format} = {}) {
80
98
  e => e?.format?.[format]);
81
99
  }
82
100
 
83
- // FIXME: in a major release, remove support for params requiring resolution
84
- export async function resolveParams({authorizationRequest, agent}) {
85
- const {
86
- client_metadata_uri,
87
- presentation_definition_uri,
88
- ...resolved
89
- } = {...authorizationRequest};
90
-
91
- // get client meta data from URL if specified
92
- if(client_metadata_uri) {
93
- const response = await fetchJSON({url: client_metadata_uri, agent});
94
- if(!response.data) {
95
- throw createNamedError({
96
- message: 'Client meta data format is not JSON.',
97
- name: 'DataError'
98
- });
99
- }
100
- resolved.client_metadata = response.data;
101
- }
102
-
103
- // get presentation definition from URL if not embedded
104
- if(presentation_definition_uri) {
105
- const response = await fetchJSON(
106
- {url: presentation_definition_uri, agent});
107
- if(!response.data) {
108
- throw createNamedError({
109
- message: 'Presentation definition format is not JSON.',
110
- name: 'DataError'
111
- });
112
- }
113
- resolved.presentation_definition = response.data;
114
- }
115
-
116
- assert(resolved.presentation_definition, 'presentation_definition', 'object');
117
- assert(
118
- resolved.presentation_definition?.id,
119
- 'presentation_definition.id', 'string');
120
-
121
- // FIXME: further validate `authorizationRequest.presentation_definition`
122
- // FIXME: further validate `authorizationRequest.client_metadata`
123
-
124
- // `direct_post.jwt` response mode requires encryption; ensure the client
125
- // meta data has the necessary parameters
126
- if(resolved.response_mode === 'direct_post.jwt') {
127
- const {
128
- authorization_encrypted_response_alg = 'ECDH-ES',
129
- authorization_encrypted_response_enc = 'A256GCM',
130
- jwks
131
- } = resolved.client_metadata;
132
- if(authorization_encrypted_response_alg !== 'ECDH-ES') {
133
- throw createNamedError({
134
- message: `"${authorization_encrypted_response_alg}" is not ` +
135
- 'supported; only "ECDH-ES" is supported.',
136
- name: 'NotSupportedError'
137
- });
138
- }
139
- if(authorization_encrypted_response_enc !== 'A256GCM') {
140
- throw createNamedError({
141
- message: `"${authorization_encrypted_response_enc}" is not ` +
142
- 'supported; only "A256GCM" is supported.',
143
- name: 'NotSupportedError'
144
- });
145
- }
146
- if(!selectJwk({
147
- keys: jwks?.keys, alg: 'ECDH-ES', kty: 'EC', crv: 'P-256', use: 'enc'
148
- })) {
149
- throw createNamedError({
150
- message: 'No matching key found for "ECDH-ES" in client meta data ' +
151
- 'JWK key set.',
152
- name: 'NotFoundError'
153
- });
154
- }
155
- }
156
-
157
- return resolved;
158
- }
159
-
160
101
  export function usesClientIdScheme({authorizationRequest, scheme} = {}) {
102
+ if(Array.isArray(scheme)) {
103
+ return scheme.some(
104
+ scheme => usesClientIdScheme({authorizationRequest, scheme}));
105
+ }
161
106
  return authorizationRequest?.client_id_scheme === scheme ||
162
107
  authorizationRequest?.client_id?.startsWith(`${scheme}:`);
163
108
  }
@@ -168,10 +113,8 @@ export async function validate({authorizationRequest, expectedClientId}) {
168
113
  client_id,
169
114
  client_id_scheme,
170
115
  client_metadata,
171
- client_metadata_uri,
172
116
  nonce,
173
117
  presentation_definition,
174
- presentation_definition_uri,
175
118
  response_mode,
176
119
  scope
177
120
  } = authorizationRequest;
@@ -187,31 +130,26 @@ export async function validate({authorizationRequest, expectedClientId}) {
187
130
  assert(nonce, 'nonce', 'string');
188
131
  assertOptional(client_id_scheme, 'client_id_scheme', 'string');
189
132
  assertOptional(client_metadata, 'client_metadata', 'object');
190
- // FIXME: remove `client_metadata_uri` in a future revision, it is not
191
- // supported in the latest OID4VP, bad practice, and rarely used
192
- assertOptional(client_metadata_uri, 'client_metadata_uri', 'string');
193
133
  assertOptional(
194
134
  presentation_definition, 'presentation_definition', 'object');
195
- // FIXME: remove `presentation_definition_uri` in a future revision, it is
196
- // not supported in the latest OID4VP, bad practice, and rarely used
197
- assertOptional(
198
- presentation_definition_uri, 'presentation_definition_uri', 'string');
199
135
  assertOptional(response_mode, 'response_mode', 'string');
200
136
  assertOptional(scope, 'scope', 'string');
201
- if(client_metadata && client_metadata_uri) {
202
- throw createNamedError({
203
- message: 'Only one of "client_metadata" and ' +
204
- '"client_metadata_uri" must be present.',
205
- name: 'DataError'
206
- });
207
- }
208
- if(presentation_definition && presentation_definition_uri) {
137
+ // FIXME: further validate `presentation_definition`
138
+ // FIXME: further validate `client_metadata`
139
+
140
+ // Note: This implementation requires client ID scheme to be one of:
141
+ // `redirect_uri`, `x509_san_dns`, `x509_hash`, `did`, or
142
+ // `decentralized_identifier`
143
+ const scheme = getClientIdScheme({authorizationRequest});
144
+ if(!SUPPORTED_CLIENT_ID_SCHEMES.has(scheme)) {
145
+ const schemes = [...SUPPORTED_CLIENT_ID_SCHEMES].join(', ');
209
146
  throw createNamedError({
210
- message: 'Only one of "presentation_definition" and ' +
211
- '"presentation_definition_uri" must be present.',
212
- name: 'DataError'
147
+ message: `Unsupported client ID scheme "${scheme}"; ` +
148
+ `supported schemes are: ${schemes}.'`,
149
+ name: 'NotSupportedError'
213
150
  });
214
151
  }
152
+
215
153
  // Note: This implementation requires `response_mode` to be `direct_post`
216
154
  // or `direct_post.jwt`; no other modes are supported.
217
155
  if(!(response_mode === 'direct_post' ||
@@ -222,9 +160,151 @@ export async function validate({authorizationRequest, expectedClientId}) {
222
160
  name: 'NotSupportedError'
223
161
  });
224
162
  }
163
+
164
+ // `direct_post.jwt` response mode requires encryption; ensure the client
165
+ // meta data has the necessary parameters
166
+ if(response_mode === 'direct_post.jwt') {
167
+ const {
168
+ authorization_encrypted_response_alg = 'ECDH-ES',
169
+ authorization_encrypted_response_enc = 'A256GCM',
170
+ jwks
171
+ } = client_metadata;
172
+ if(authorization_encrypted_response_alg !== 'ECDH-ES') {
173
+ throw createNamedError({
174
+ message: `"${authorization_encrypted_response_alg}" is not ` +
175
+ 'supported; only "ECDH-ES" is supported.',
176
+ name: 'NotSupportedError'
177
+ });
178
+ }
179
+ if(authorization_encrypted_response_enc !== 'A256GCM') {
180
+ throw createNamedError({
181
+ message: `"${authorization_encrypted_response_enc}" is not ` +
182
+ 'supported; only "A256GCM" is supported.',
183
+ name: 'NotSupportedError'
184
+ });
185
+ }
186
+ if(!selectJwk({
187
+ keys: jwks?.keys, alg: 'ECDH-ES', kty: 'EC', crv: 'P-256', use: 'enc'
188
+ })) {
189
+ throw createNamedError({
190
+ message: 'No matching key found for "ECDH-ES" in client meta data ' +
191
+ 'JWK key set.',
192
+ name: 'NotFoundError'
193
+ });
194
+ }
195
+ }
196
+ }
197
+
198
+ async function _checkClientIdSchemeRequirements({
199
+ clientIdScheme, authorizationRequest, protectedHeader,
200
+ certificatePublicKey, getTrustedCertificates
201
+ }) {
202
+ // if `x509_san_dns` or `x509_hash`...
203
+ if(clientIdScheme.startsWith('x509_')) {
204
+ // `x5c` MUST be present where the public key is in the leaf cert (which is
205
+ // the first in the chain per RFC 7515:
206
+ // https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6)
207
+ if(!certificatePublicKey) {
208
+ throw createNamedError({
209
+ message:
210
+ 'No "x5c" header with an acceptable public key found; client ID ' +
211
+ 'schemes starting with "x509_" must use the "x5c" header ' +
212
+ 'to provide an X.509 certificate with the public key for verifying ' +
213
+ 'the request.',
214
+ name: 'DataError'
215
+ });
216
+ }
217
+
218
+ // ensure trusted certs can be retrieved
219
+ if(typeof getTrustedCertificates !== 'function') {
220
+ throw createNamedError({
221
+ message:
222
+ 'No "getTrustedCertificates" function provided; client ID schemes ' +
223
+ 'starting with "x509_" require such a function to be provided ' +
224
+ 'that will return the certificates that are to be trusted ' +
225
+ 'when verifying X.509 certificate chains.',
226
+ name: 'DataError'
227
+ });
228
+ }
229
+
230
+ // get trusted certificates for `x5c` and verify chain
231
+ const {x5c} = protectedHeader;
232
+ const chain = parseCertificateChain({x5c});
233
+ const trustedCertificates = await getTrustedCertificates({
234
+ x5c, chain, authorizationRequest
235
+ });
236
+ const verifyResult = await verifyCertificateChain({
237
+ chain, trustedCertificates
238
+ });
239
+ if(!verifyResult.result) {
240
+ throw createNamedError({
241
+ message:
242
+ 'Signed authorization request "x5c" certificate chain is invalid: ' +
243
+ verifyResult.resultMessage,
244
+ name: 'DataError'
245
+ });
246
+ }
247
+
248
+ let {client_id: clientId} = authorizationRequest;
249
+ clientId = clientId.startsWith(`${clientIdScheme}:`) ?
250
+ clientId.slice(clientIdScheme.length + 2) : clientId;
251
+
252
+ if(clientIdScheme === 'x509_san_dns') {
253
+ // `x509_san_dns` requires leaf cert to have a dNSName ("domain" type) in
254
+ // a subject alternative name field that matches the client_id
255
+ if(!hasDomainSubjectAltName({certificate: chain[0], name: clientId})) {
256
+ throw createNamedError({
257
+ message:
258
+ `Signed authorization request header "x5c" parameter's leaf ` +
259
+ 'certificate does not have a DNS subject alternative name that ' +
260
+ 'matches the client ID as required by the used "x509_san_dns" ' +
261
+ 'client ID scheme.',
262
+ name: 'DataError'
263
+ });
264
+ }
265
+ /* Note: The current implementation does not support `redirect_uri` as
266
+ a `response_mode`. If a future revision adds support for this, then
267
+ when `x509_san_dns` is used, the `redirect_uri` value must match the
268
+ FQDN of the `client_id` unless an allow list for overrides is passed
269
+ and it includes the client ID. */
270
+ } else if(clientIdScheme === 'x509_hash') {
271
+ // `x509_hash:<base64url sha256-hash of DER leaf cert>`
272
+ const hash = base64Encode(await _sha256(chain[0].toBER()));
273
+ if(clientId !== hash) {
274
+ throw createNamedError({
275
+ message:
276
+ `The signed authorization request header "x5c" parameter's leaf ` +
277
+ `certificate's SHA-256 hash digest not match the client ID as ` +
278
+ 'required by the used "x509_hash" client ID scheme.',
279
+ name: 'DataError'
280
+ });
281
+ }
282
+ }
283
+ } else if(
284
+ clientIdScheme === 'did' || clientIdScheme === 'decentralized_identifier') {
285
+ // "kid" header must reference a verification method controlled by the
286
+ // DID expressed in client ID; this is checked by default when a proper
287
+ // DID resolver is used in `getVerificationKey` but this check provides
288
+ // a partial additional sanity check
289
+ let {client_id: clientId} = authorizationRequest;
290
+ clientId = clientId.startsWith('decentralized_identifier:did:') ?
291
+ clientId.slice('decentralized_identifier:'.length + 1) : clientId;
292
+ if(!protectedHeader?.kid?.startsWith(clientId + '#')) {
293
+ throw createNamedError({
294
+ message:
295
+ `The signed authorization request header "kid" parameter's value ` +
296
+ 'does not reference a verification method controlled by the DID ' +
297
+ 'identified in the client ID as required by the used ' +
298
+ '"did"/"decentralized_identifier" client ID scheme.',
299
+ name: 'DataError'
300
+ });
301
+ }
302
+ }
225
303
  }
226
304
 
227
- async function _fetch({requestUrl, getVerificationKey, agent}) {
305
+ async function _fetch({
306
+ requestUrl, getTrustedCertificates, getVerificationKey, agent
307
+ }) {
228
308
  // FIXME: every `fetchJSON` call needs to use a block list or other
229
309
  // protections to prevent a confused deputy attack where the `requestUrl`
230
310
  // accesses a location it should not, e.g., a URL `localhost` is used when
@@ -246,7 +326,9 @@ async function _fetch({requestUrl, getVerificationKey, agent}) {
246
326
  }
247
327
 
248
328
  // return parsed payload and original response
249
- const payload = await _parseJwt({jwt, getVerificationKey});
329
+ const payload = await _parseJwt({
330
+ jwt, getTrustedCertificates, getVerificationKey
331
+ });
250
332
  return {payload, response, jwt};
251
333
  }
252
334
 
@@ -255,15 +337,27 @@ function _get(sp, name) {
255
337
  return value === null ? undefined : value;
256
338
  }
257
339
 
258
- async function _parseJwt({jwt, getVerificationKey, signatureRequired}) {
340
+ function _importPublicKeyFromX5c({x5c}) {
341
+ if(x5c?.[0]) {
342
+ const pem =
343
+ `-----BEGIN CERTIFICATE-----\n${x5c[0]}\n-----END CERTIFICATE-----`;
344
+ return importX509(pem, 'ES256');
345
+ }
346
+ }
347
+
348
+ async function _parseJwt({
349
+ jwt, getTrustedCertificates, getVerificationKey, signatureRequired
350
+ }) {
351
+ // parse unprotected payload and scheme from it
259
352
  const payload = decodeJwt(jwt);
353
+ const clientIdScheme = getClientIdScheme({authorizationRequest: payload});
260
354
 
261
355
  // check if a signature on the JWT is required:
262
356
  // - `client_metadata.require_signed_request_object` is `true`, OR
263
357
  // - `client_id_scheme` requires the JWT to be signed
264
358
  signatureRequired = signatureRequired ||
265
359
  payload.client_metadata?.require_signed_request_object === true ||
266
- usesClientIdScheme({authorizationRequest: payload, scheme: 'x509_san_dns'});
360
+ REQUIRED_SIGNED_AUTHZ_REQUEST_CLIENT_ID_SCHEMES.has(clientIdScheme);
267
361
  if(!signatureRequired) {
268
362
  // no signature required, just use the decoded payload
269
363
  return payload;
@@ -271,16 +365,34 @@ async function _parseJwt({jwt, getVerificationKey, signatureRequired}) {
271
365
 
272
366
  // create callback function to handle key lookup; `getVerificationKey` may
273
367
  // return a promise
274
- const getKey = protectedHeader => {
275
- // FIXME: add parser for `x5c`?
368
+ let certificatePublicKey;
369
+ const getKey = async protectedHeader => {
370
+ // parse any `x5c` to get certificate public key and include that in
371
+ // `getVerificationKey` params
372
+ const {x5c} = protectedHeader;
373
+ certificatePublicKey = await _importPublicKeyFromX5c({x5c});
276
374
  if(getVerificationKey) {
277
- return getVerificationKey({protectedHeader});
375
+ return getVerificationKey({
376
+ protectedHeader, certificatePublicKey,
377
+ clientIdScheme, authorizationRequest: payload
378
+ });
379
+ }
380
+ if(x5c) {
381
+ return certificatePublicKey;
278
382
  }
279
383
  _throwKeyNotFound(protectedHeader);
280
384
  };
281
385
 
282
386
  // verify the JWT
283
387
  const verifyResult = await jwtVerify(jwt, getKey, {alg: 'ES256'});
388
+ const {payload: authorizationRequest, protectedHeader} = verifyResult;
389
+
390
+ // ensure all client ID scheme requirements are met
391
+ await _checkClientIdSchemeRequirements({
392
+ clientIdScheme, authorizationRequest, protectedHeader,
393
+ certificatePublicKey, getTrustedCertificates
394
+ });
395
+
284
396
  return verifyResult.payload;
285
397
  }
286
398
 
@@ -292,8 +404,6 @@ function _parseOID4VPUrl({url}) {
292
404
  const response_mode = _get(searchParams, 'response_mode');
293
405
  const presentation_definition = _get(
294
406
  searchParams, 'presentation_definition');
295
- const presentation_definition_uri = _get(
296
- searchParams, 'presentation_definition_uri');
297
407
  const client_id = _get(searchParams, 'client_id');
298
408
  const client_id_scheme = _get(searchParams, 'client_id_scheme');
299
409
  const client_metadata = _get(searchParams, 'client_metadata');
@@ -324,7 +434,6 @@ function _parseOID4VPUrl({url}) {
324
434
  response_mode,
325
435
  presentation_definition: presentation_definition &&
326
436
  JSON.parse(presentation_definition),
327
- presentation_definition_uri,
328
437
  client_id,
329
438
  client_id_scheme,
330
439
  client_metadata: client_metadata && JSON.parse(client_metadata),
@@ -335,6 +444,14 @@ function _parseOID4VPUrl({url}) {
335
444
  return {authorizationRequest};
336
445
  }
337
446
 
447
+ async function _sha256(data) {
448
+ if(typeof data === 'string') {
449
+ data = new TextEncoder().encode(data);
450
+ }
451
+ const algorithm = {name: 'SHA-256'};
452
+ return new Uint8Array(await crypto.subtle.digest(algorithm, data));
453
+ }
454
+
338
455
  function _throwKeyNotFound(protectedHeader) {
339
456
  const error = new Error(
340
457
  'Could not verify signed authorization request; ' +
@@ -42,6 +42,15 @@ export async function send({
42
42
 
43
43
  // if `authorizationRequest.response_mode` is `direct.jwt` generate a JWT
44
44
  if(authorizationRequest.response_mode === 'direct_post.jwt') {
45
+ if(submitsFormat({presentationSubmission, format: 'mso_mdoc'}) &&
46
+ !encryptionOptions?.mdl?.sessionTranscript) {
47
+ throw createNamedError({
48
+ message: '"encryptionOptions.mdl.sessionTranscript" is required ' +
49
+ 'when submitting an mDL presentation.',
50
+ name: 'DataError'
51
+ });
52
+ }
53
+
45
54
  const jwt = await _encrypt({
46
55
  vpToken, presentationSubmission, authorizationRequest,
47
56
  encryptionOptions
@@ -72,7 +81,7 @@ export async function send({
72
81
  } catch(cause) {
73
82
  const message = cause.data?.error_description ?? cause.message;
74
83
  const error = new Error(
75
- `Could not send OID4VP authorization response: ${message}.`,
84
+ `Could not send OID4VP authorization response: ${message}`,
76
85
  {cause});
77
86
  error.name = 'OperationError';
78
87
  throw error;
@@ -135,6 +144,22 @@ export function createPresentationSubmission({
135
144
  return {presentationSubmission};
136
145
  }
137
146
 
147
+ export function submitsFormat({presentationSubmission, format} = {}) {
148
+ /* e.g. presentation submission submitting an mdoc:
149
+ {
150
+ "definition_id": "mDL-sample-req",
151
+ "id": "mDL-sample-res",
152
+ "descriptor_map": [{
153
+ "id": "org.iso.18013.5.1.mDL",
154
+ "format": "mso_mdoc",
155
+ "path": "$"
156
+ }]
157
+ }
158
+ */
159
+ return presentationSubmission?.descriptor_map?.some(
160
+ e => e?.format === format);
161
+ }
162
+
138
163
  async function _encrypt({
139
164
  vpToken, presentationSubmission, authorizationRequest, encryptionOptions
140
165
  }) {
@@ -153,13 +178,13 @@ async function _encrypt({
153
178
 
154
179
  // configure `keyManagementParameters` for `EncryptJWT` API
155
180
  const keyManagementParameters = {};
156
- if(encryptionOptions?.mdoc?.sessionTranscript) {
181
+ if(encryptionOptions?.mdl?.sessionTranscript) {
157
182
  // ISO 18013-7: include specific session transcript params as apu + apv
158
183
  const {
159
184
  mdocGeneratedNonce,
160
185
  // default to using `authorizationRequest.nonce` for verifier nonce
161
186
  verifierGeneratedNonce = authorizationRequest.nonce
162
- } = encryptionOptions.mdoc.sessionTranscript;
187
+ } = encryptionOptions.mdl.sessionTranscript;
163
188
  // note: `EncryptJWT` API requires `apu/apv` (`partyInfoU`/`partyInfoV`)
164
189
  // to be passed as Uint8Arrays; they will be encoded using `base64url` by
165
190
  // that API
package/lib/convert.js CHANGED
@@ -1,12 +1,11 @@
1
1
  /*!
2
2
  * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
+ import {JSONPath} from 'jsonpath-plus';
5
+ import jsonpointer from 'jsonpointer';
4
6
  import {
5
- resolveParams as resolveAuthorizationRequestParams,
6
7
  validate as validateAuthorizationRequest
7
8
  } from './authorizationRequest.js';
8
- import {JSONPath} from 'jsonpath-plus';
9
- import jsonpointer from 'jsonpointer';
10
9
 
11
10
  // converts a VPR to partial "authorization request"
12
11
  export function fromVpr({
@@ -100,26 +99,17 @@ export function pathsToVerifiableCredentialPointers({paths} = {}) {
100
99
 
101
100
  // converts an OID4VP authorization request (including its
102
101
  // "presentation definition") to a VPR
103
- export async function toVpr({
104
- authorizationRequest, strict = false, agent
105
- } = {}) {
102
+ export async function toVpr({authorizationRequest, strict = false} = {}) {
106
103
  try {
107
104
  // ensure authorization request is valid
108
105
  validateAuthorizationRequest({authorizationRequest});
109
106
 
110
- // FIXME: in a major release, remove support for params requiring
111
- // resolution
112
-
113
- // resolve and validate any additional parameters in the request
114
- authorizationRequest = await resolveAuthorizationRequestParams({
115
- authorizationRequest, agent
116
- });
117
-
118
107
  const {
119
108
  client_id,
120
109
  client_metadata,
121
110
  nonce,
122
- presentation_definition
111
+ presentation_definition,
112
+ response_uri
123
113
  } = authorizationRequest;
124
114
 
125
115
  // disallow unsupported `submission_requirements` in strict mode
@@ -147,9 +137,9 @@ export async function toVpr({
147
137
  }
148
138
  }
149
139
 
150
- // map `client_id` to `domain`
151
- if(client_id !== undefined) {
152
- verifiablePresentationRequest.domain = client_id;
140
+ // map `response_uri` or `client_id` to `domain`
141
+ if(response_uri !== undefined || client_id !== undefined) {
142
+ verifiablePresentationRequest.domain = response_uri ?? client_id;
153
143
  }
154
144
 
155
145
  // map `nonce` to `challenge`
package/lib/util.js CHANGED
@@ -21,6 +21,22 @@ export function assertOptional(x, name, type) {
21
21
  return assert(x, name, type, true);
22
22
  }
23
23
 
24
+ export function base64Decode(str) {
25
+ if(Uint8Array.fromBase64) {
26
+ return Uint8Array.fromBase64(str);
27
+ }
28
+ return base64url.decode(
29
+ str.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'));
30
+ }
31
+
32
+ export function base64Encode(data) {
33
+ if(data.toBase64) {
34
+ return data.toBase64();
35
+ }
36
+ // note: this is base64-no-pad; will only work with specific data lengths
37
+ base64url.encode(data).replace(/-/g, '+').replace(/_/g, '/');
38
+ }
39
+
24
40
  export function createNamedError({message, name, cause} = {}) {
25
41
  const error = new Error(message, {cause});
26
42
  error.name = name;
package/lib/x509.js ADDED
@@ -0,0 +1,61 @@
1
+ /*!
2
+ * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {
5
+ Certificate,
6
+ CertificateChainValidationEngine,
7
+ id_SubjectAltName
8
+ } from 'pkijs';
9
+ import {base64Decode} from './util.js';
10
+
11
+ export function fromPemOrBase64(str) {
12
+ const tag = 'CERTIFICATE';
13
+ const pattern = new RegExp(
14
+ `-{5}BEGIN ${tag}-{5}([a-zA-Z0-9=+\\/\\n\\r]+)-{5}END ${tag}-{5}`, 'g');
15
+ const matches = pattern.exec(str);
16
+ if(!matches) {
17
+ throw new Error('No PEM or Base64-formatted certificate found.');
18
+ }
19
+ const b64 = matches[1].replace(/\r/g, '').replace(/\n/g, '');
20
+ return _fromBase64(b64);
21
+ }
22
+
23
+ export function hasDomainSubjectAltName({certificate, name} = {}) {
24
+ const subjectAltNames = new Set();
25
+ for(const extension of certificate.extensions) {
26
+ if(extension.extnID === id_SubjectAltName) {
27
+ for(const altName of extension.parsedValue.altNames) {
28
+ // `domain` type
29
+ if(altName.type === 2) {
30
+ subjectAltNames.add(altName.value);
31
+ }
32
+ }
33
+ }
34
+ }
35
+ return subjectAltNames.has(name);
36
+ }
37
+
38
+ export function parseCertificateChain({x5c} = {}) {
39
+ return x5c.map(c => Certificate.fromBER(base64Decode(c)));
40
+ }
41
+
42
+ export async function verifyCertificateChain({
43
+ chain, trustedCertificates
44
+ } = {}) {
45
+ if(!(chain?.length > 0)) {
46
+ throw new Error('No matching certificate.');
47
+ }
48
+
49
+ const chainEngine = new CertificateChainValidationEngine({
50
+ certs: chain.map(c => typeof c === 'string' ? fromPemOrBase64(c) : c),
51
+ trustedCerts: trustedCertificates.map(
52
+ c => typeof c === 'string' ? fromPemOrBase64(c) : c)
53
+ });
54
+
55
+ const verifyResult = await chainEngine.verify();
56
+ return verifyResult;
57
+ }
58
+
59
+ function _fromBase64(str) {
60
+ return Certificate.fromBER(base64Decode(str));
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalbazaar/oid4-client",
3
- "version": "4.4.0",
3
+ "version": "5.0.0",
4
4
  "description": "An OID4 (VC + VP) client",
5
5
  "homepage": "https://github.com/digitalbazaar/oid4-client",
6
6
  "author": {
@@ -27,7 +27,8 @@
27
27
  "base64url-universal": "^2.0.0",
28
28
  "jose": "^6.0.13",
29
29
  "jsonpath-plus": "^10.3.0",
30
- "jsonpointer": "^5.0.1"
30
+ "jsonpointer": "^5.0.1",
31
+ "pkijs": "^3.2.5"
31
32
  },
32
33
  "devDependencies": {
33
34
  "c8": "^10.1.3",