@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.
- package/lib/authorizationRequest.js +231 -114
- package/lib/authorizationResponse.js +28 -3
- package/lib/convert.js +8 -18
- package/lib/util.js +16 -0
- package/lib/x509.js +61 -0
- package/package.json +3 -2
|
@@ -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({
|
|
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({
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
if(
|
|
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:
|
|
211
|
-
|
|
212
|
-
name: '
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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({
|
|
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?.
|
|
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.
|
|
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": "
|
|
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",
|