@digitalbazaar/oid4-client 5.7.2 → 5.8.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/index.js +1 -1
- package/lib/oid4vp/authorizationRequest.js +18 -2
- package/lib/oid4vp/authorizationResponse.js +122 -50
- package/lib/oid4vp/hpke.js +116 -0
- package/lib/oid4vp/index.js +2 -1
- package/lib/oid4vp/jwt.js +66 -0
- package/lib/oid4vp/mdl.js +163 -0
- package/lib/oid4vp/verifier.js +87 -64
- package/lib/query/presentationExchange.js +10 -0
- package/lib/util.js +52 -3
- package/package.json +3 -1
package/lib/index.js
CHANGED
|
@@ -85,6 +85,17 @@ export function getClientIdScheme({authorizationRequest} = {}) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export function requestsFormat({authorizationRequest, format} = {}) {
|
|
88
|
+
/* e.g. DCQL requesting an mdoc:
|
|
89
|
+
{
|
|
90
|
+
credentials: [{
|
|
91
|
+
id: 'mdl-id',
|
|
92
|
+
format: 'mso_mdoc',
|
|
93
|
+
meta: {
|
|
94
|
+
doctype_value: 'org.iso.18013.5.1.mDL'
|
|
95
|
+
}
|
|
96
|
+
}]
|
|
97
|
+
}
|
|
98
|
+
*/
|
|
88
99
|
/* e.g. presentation definition requesting an mdoc:
|
|
89
100
|
{
|
|
90
101
|
id: 'mdl-test-age-over-21',
|
|
@@ -98,8 +109,13 @@ export function requestsFormat({authorizationRequest, format} = {}) {
|
|
|
98
109
|
}]
|
|
99
110
|
}
|
|
100
111
|
*/
|
|
101
|
-
return
|
|
102
|
-
|
|
112
|
+
return (
|
|
113
|
+
// DCQL
|
|
114
|
+
authorizationRequest.dcql_query?.credentials?.some(
|
|
115
|
+
e => e?.format === format) ||
|
|
116
|
+
// PE
|
|
117
|
+
authorizationRequest.presentation_definition?.input_descriptors?.some(
|
|
118
|
+
e => e?.format?.[format]));
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
export function usesClientIdScheme({authorizationRequest, scheme} = {}) {
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2023-
|
|
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 {
|
|
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`
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
49
|
-
'
|
|
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
|
|
79
|
+
// include vp token and presentation submission directly in body
|
|
61
80
|
body.vp_token = vpToken;
|
|
62
|
-
|
|
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
|
|
208
|
-
const
|
|
209
|
-
|
|
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:
|
|
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
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
}
|
package/lib/oid4vp/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2023-
|
|
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
|
+
}
|
package/lib/oid4vp/verifier.js
CHANGED
|
@@ -1,57 +1,115 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2023-
|
|
2
|
+
* Copyright (c) 2023-2026 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
63
|
+
if(payload.presentation_submission) {
|
|
64
|
+
parsed.presentationSubmission = parseJSON(
|
|
65
|
+
payload.presentation_submission, 'presentation_submission');
|
|
66
|
+
}
|
|
36
67
|
}
|
|
37
68
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
vp_token
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
@@ -60,6 +60,48 @@ export function fetchJSON({url, agent} = {}) {
|
|
|
60
60
|
return httpClient.get(url, fetchOptions);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// only supports keys used w/mDL at this time
|
|
64
|
+
export function jwkToCoseKey({jwk} = {}) {
|
|
65
|
+
if(!(jwk.kty === 'EC' && (jwk.crv === 'P-256' || jwk.crv === 'P-384'))) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'Unknown supported JWK for mDL encryption; ' +
|
|
68
|
+
'only EC P-256 or P-384 are accepted.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// https://datatracker.ietf.org/doc/html/rfc9053
|
|
72
|
+
return {
|
|
73
|
+
// `kty`; only types supported for mDL:
|
|
74
|
+
// `EC2` 2
|
|
75
|
+
1: 2,
|
|
76
|
+
// `crv`; only curves supported for mDL:
|
|
77
|
+
// `P-256` 1
|
|
78
|
+
// `P-384` 2
|
|
79
|
+
'-1': jwk.crv === 'P-256' ? 1 : 2,
|
|
80
|
+
// `x`
|
|
81
|
+
'-2': base64url.decode(jwk.x),
|
|
82
|
+
// `y`
|
|
83
|
+
'-3': base64url.decode(jwk.y)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function coseKeyToJwk({coseKey} = {}) {
|
|
88
|
+
const {1: kty, '-1': crv, '-2': x, '-3': y} = coseKey;
|
|
89
|
+
|
|
90
|
+
if(!(kty === 'EC' && (crv === 'P-256' || crv === 'P-384'))) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'Unknown supported COSE key for mDL decryption; ' +
|
|
93
|
+
'only EC P-256 or P-384 are accepted.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// https://datatracker.ietf.org/doc/html/rfc9053
|
|
97
|
+
return {
|
|
98
|
+
kty,
|
|
99
|
+
crv,
|
|
100
|
+
x: base64url.encode(x),
|
|
101
|
+
y: base64url.encode(y)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
63
105
|
export function parseJSON(x, name) {
|
|
64
106
|
try {
|
|
65
107
|
return JSON.parse(x);
|
|
@@ -73,7 +115,9 @@ export function parseJSON(x, name) {
|
|
|
73
115
|
}
|
|
74
116
|
}
|
|
75
117
|
|
|
76
|
-
export function selectJwk({
|
|
118
|
+
export function selectJwk({
|
|
119
|
+
keys, kid, alg, kty, crv, use, x, y
|
|
120
|
+
} = {}) {
|
|
77
121
|
/* Example JWKs "keys":
|
|
78
122
|
"jwks": {
|
|
79
123
|
"keys": [
|
|
@@ -103,16 +147,21 @@ export function selectJwk({keys, kid, alg, kty, crv, use} = {}) {
|
|
|
103
147
|
const kty1 = kty ?? jwk.kty;
|
|
104
148
|
const crv1 = crv ?? jwk.crv;
|
|
105
149
|
const use1 = use ?? jwk.use;
|
|
150
|
+
const x1 = x ?? jwk.x;
|
|
151
|
+
const y1 = y ?? jwk.y;
|
|
106
152
|
const {
|
|
107
153
|
// default missing `alg` value in `jwk` to search value
|
|
108
154
|
alg: alg2 = alg1,
|
|
109
155
|
kty: kty2,
|
|
110
156
|
crv: crv2,
|
|
111
157
|
// default missing `use` value in `jwk` to search value
|
|
112
|
-
use: use2 = use1
|
|
158
|
+
use: use2 = use1,
|
|
159
|
+
x: x2,
|
|
160
|
+
y: y2
|
|
113
161
|
} = jwk;
|
|
114
162
|
// return if `jwk` matches computed values
|
|
115
|
-
return alg1 === alg2 && kty1 === kty2 && crv1 === crv2 && use1 === use2
|
|
163
|
+
return alg1 === alg2 && kty1 === kty2 && crv1 === crv2 && use1 === use2 &&
|
|
164
|
+
x1 == x2 && y1 === y2;
|
|
116
165
|
});
|
|
117
166
|
}
|
|
118
167
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalbazaar/oid4-client",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.8.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",
|