@bedrock/vc-delivery 7.19.1 → 7.19.3
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/oid4/authorizationRequest.js +16 -59
- package/lib/oid4/authorizationResponse.js +5 -2
- package/lib/oid4/http.js +10 -4
- package/lib/oid4/mdoc.js +112 -0
- package/lib/oid4/oid4vp.js +14 -8
- package/lib/oid4/requestSigning.js +84 -0
- package/package.json +2 -2
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
* Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
|
-
import {
|
|
6
|
-
import {exportJWK, generateKeyPair, importJWK} from 'jose';
|
|
5
|
+
import {exportJWK, generateKeyPair} from 'jose';
|
|
7
6
|
import {oid4vp, signJWT} from '@digitalbazaar/oid4-client';
|
|
7
|
+
import {createDcApiRequest} from './mdoc.js';
|
|
8
8
|
import {getClientBaseUrl} from './clientProfiles.js';
|
|
9
|
-
import {
|
|
10
|
-
import {httpsAgent} from '@bedrock/https-agent';
|
|
9
|
+
import {getRequestSigningParameters} from './requestSigning.js';
|
|
11
10
|
import {randomUUID} from 'node:crypto';
|
|
12
11
|
|
|
13
12
|
const {util: {BedrockError}} = bedrock;
|
|
@@ -112,6 +111,13 @@ export async function create({
|
|
|
112
111
|
export async function encode({
|
|
113
112
|
workflow, clientProfile, authorizationRequest
|
|
114
113
|
} = {}) {
|
|
114
|
+
if(authorizationRequest.response_mode === 'dc_api') {
|
|
115
|
+
// create mdoc device request for DC API Annex C
|
|
116
|
+
return createDcApiRequest({
|
|
117
|
+
workflow, clientProfile, authorizationRequest
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
// if required, construct authz request as signed JWT
|
|
116
122
|
if(authorizationRequest.client_metadata.require_signed_request_object) {
|
|
117
123
|
return _createJwt({workflow, clientProfile, authorizationRequest});
|
|
@@ -234,64 +240,15 @@ async function _createClientMetaData({
|
|
|
234
240
|
|
|
235
241
|
async function _createJwt({workflow, clientProfile, authorizationRequest}) {
|
|
236
242
|
try {
|
|
237
|
-
//
|
|
238
|
-
const {
|
|
243
|
+
// get signing params
|
|
244
|
+
const {signer, kid, alg} = await getRequestSigningParameters({
|
|
245
|
+
workflow, clientProfile
|
|
246
|
+
});
|
|
239
247
|
|
|
240
|
-
// get any `x5c`
|
|
241
|
-
// authz request via the client profile
|
|
248
|
+
// get any `x5c` to include
|
|
242
249
|
const {
|
|
243
|
-
authorizationRequestSigningParameters: {
|
|
244
|
-
zcapReferenceIds: {signAuthorizationRequest: refId} = {}
|
|
250
|
+
authorizationRequestSigningParameters: {x5c} = {},
|
|
245
251
|
} = clientProfile;
|
|
246
|
-
if(privateKeyJwk === undefined && refId === undefined) {
|
|
247
|
-
throw new BedrockError(
|
|
248
|
-
'The OID4VP client profile does not specify the private key or ' +
|
|
249
|
-
'capability in the workflow configuration to use to sign ' +
|
|
250
|
-
'authorization requests.', {
|
|
251
|
-
name: 'DataError',
|
|
252
|
-
details: {httpStatusCode: 500, public: true}
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// code assumed ES256 (ECDSA P-256 + SHA-256) support only
|
|
257
|
-
const alg = 'ES256';
|
|
258
|
-
let kid;
|
|
259
|
-
let signer;
|
|
260
|
-
if(privateKeyJwk) {
|
|
261
|
-
kid = privateKeyJwk.kid;
|
|
262
|
-
|
|
263
|
-
// import `privateKeyJwk`
|
|
264
|
-
const privateKey = await importJWK(privateKeyJwk, alg);
|
|
265
|
-
|
|
266
|
-
// create signer API
|
|
267
|
-
const algorithm = {name: 'ECDSA', hash: {name: 'SHA-256'}};
|
|
268
|
-
signer = {
|
|
269
|
-
async sign({data}) {
|
|
270
|
-
return new Uint8Array(
|
|
271
|
-
await crypto.subtle.sign(algorithm, privateKey, data));
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
} else {
|
|
275
|
-
const capability = zcaps[refId];
|
|
276
|
-
if(capability === undefined) {
|
|
277
|
-
throw new BedrockError(
|
|
278
|
-
'The capability specified by the OID4VP client profile for signing ' +
|
|
279
|
-
'authorization requests was not found in the workflow ' +
|
|
280
|
-
'configuration.', {
|
|
281
|
-
name: 'DataError',
|
|
282
|
-
details: {httpStatusCode: 500, public: true}
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// create a WebKMS `signer` interface
|
|
287
|
-
const {invocationSigner} = zcapClient;
|
|
288
|
-
const kmsClient = new KmsClient({httpsAgent});
|
|
289
|
-
signer = await AsymmetricKey.fromCapability({
|
|
290
|
-
capability, invocationSigner, kmsClient
|
|
291
|
-
});
|
|
292
|
-
const keyDescription = await signer.getKeyDescription();
|
|
293
|
-
kid = keyDescription.id;
|
|
294
|
-
}
|
|
295
252
|
|
|
296
253
|
// create the JWT payload and header to be signed
|
|
297
254
|
const payload = {...authorizationRequest};
|
|
@@ -80,7 +80,7 @@ export async function parse({
|
|
|
80
80
|
function _getDecryptParameters({
|
|
81
81
|
exchange, clientProfileId, authorizationRequest, mdocOptions
|
|
82
82
|
}) {
|
|
83
|
-
// get private key agreement keys in
|
|
83
|
+
// get private key agreement keys in JWK format
|
|
84
84
|
const {keyAgreementKeyPairs} = exchange.secrets?.oid4vp?.clientProfiles
|
|
85
85
|
?.[clientProfileId ?? 'default'] ?? {};
|
|
86
86
|
const keys = keyAgreementKeyPairs.map(({privateKeyJwk}) => privateKeyJwk);
|
|
@@ -94,7 +94,10 @@ function _getDecryptParameters({
|
|
|
94
94
|
...mdocOptions?.expectedHandover,
|
|
95
95
|
recipientPublicJwk
|
|
96
96
|
};
|
|
97
|
-
|
|
97
|
+
// set encoded `SessionTranscript` as `info`
|
|
98
|
+
// note: this must be the raw value w/o CBOR tag 24, i.e., this is NOT
|
|
99
|
+
// `SessionTranscriptBytes`
|
|
100
|
+
return oid4vp.mdoc.encodeSessionTranscript({handover, raw: true});
|
|
98
101
|
};
|
|
99
102
|
}
|
|
100
103
|
|
package/lib/oid4/http.js
CHANGED
|
@@ -405,18 +405,24 @@ async function _handleOid4vpAuthzRequest(req, res) {
|
|
|
405
405
|
// returned in response; presently any wallet metadata is ignored
|
|
406
406
|
const {
|
|
407
407
|
authorizationRequest,
|
|
408
|
-
clientProfile
|
|
408
|
+
clientProfile,
|
|
409
|
+
exchange
|
|
409
410
|
} = await oid4vp.getAuthorizationRequest({req, clientProfileId});
|
|
410
411
|
const {config: workflow} = req.serviceObject;
|
|
411
412
|
result = await oid4vp.encodeAuthorizationRequest({
|
|
412
|
-
workflow,
|
|
413
|
+
workflow, exchange,
|
|
414
|
+
clientProfileId, clientProfile, authorizationRequest,
|
|
413
415
|
requestMethod: req.method.toLowerCase()
|
|
414
416
|
});
|
|
415
|
-
|
|
417
|
+
if(authorizationRequest.response_mode === 'dc_api') {
|
|
418
|
+
res.json(result);
|
|
419
|
+
} else {
|
|
420
|
+
res.set('content-type', 'application/oauth-authz-req+jwt');
|
|
421
|
+
res.send(result);
|
|
422
|
+
}
|
|
416
423
|
} catch(error) {
|
|
417
424
|
return _sendOID4Error({res, error});
|
|
418
425
|
}
|
|
419
|
-
res.send(result);
|
|
420
426
|
}
|
|
421
427
|
|
|
422
428
|
async function _handleOid4vpAuthzResponse(req, res) {
|
package/lib/oid4/mdoc.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Digital Bazaar, Inc.
|
|
3
|
+
*/
|
|
4
|
+
import {CoseKey, IsoMdocDcApi} from '@owf/mdoc';
|
|
5
|
+
import {getRequestSigningParameters} from './requestSigning.js';
|
|
6
|
+
import {selectJwk} from '@digitalbazaar/oid4-client';
|
|
7
|
+
|
|
8
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
9
|
+
|
|
10
|
+
export async function createDcApiRequest({
|
|
11
|
+
workflow, clientProfile, authorizationRequest
|
|
12
|
+
} = {}) {
|
|
13
|
+
return _createAnnexCDcApiRequest({
|
|
14
|
+
workflow, clientProfile, authorizationRequest
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function _createAnnexCDcApiRequest({
|
|
19
|
+
workflow, clientProfile, authorizationRequest
|
|
20
|
+
}) {
|
|
21
|
+
// get recipient public key from authz request
|
|
22
|
+
const keys = authorizationRequest.client_metadata.jwks?.keys ?? [];
|
|
23
|
+
// these parameters are only ones supported Annex C for encryption key pair
|
|
24
|
+
const recipientPublicJwk = selectJwk({
|
|
25
|
+
keys, alg: 'ECDH-ES', kty: 'EC', crv: 'P-256'
|
|
26
|
+
});
|
|
27
|
+
const recipientPublicKey = CoseKey.fromJwk(recipientPublicJwk);
|
|
28
|
+
|
|
29
|
+
// get signing params
|
|
30
|
+
const {signer} = await getRequestSigningParameters({
|
|
31
|
+
workflow, clientProfile
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// get `x5c` to include in certificate chain
|
|
35
|
+
const {
|
|
36
|
+
authorizationRequestSigningParameters: {x5c} = {},
|
|
37
|
+
} = clientProfile;
|
|
38
|
+
const certificateChain = x5c.map(b64 => Buffer.from(b64, 'base64'));
|
|
39
|
+
|
|
40
|
+
// create Annex C DC API mdoc device request
|
|
41
|
+
const {dcql_query: dcqlQuery} = authorizationRequest;
|
|
42
|
+
const {request} = await IsoMdocDcApi.createRequest({
|
|
43
|
+
version: '1.1',
|
|
44
|
+
nonce: TEXT_ENCODER.encode(authorizationRequest.nonce),
|
|
45
|
+
docRequests: _createDocRequests({dcqlQuery}),
|
|
46
|
+
recipientPublicKey,
|
|
47
|
+
readerAuth: {
|
|
48
|
+
signingKey: CoseKey.fromJwk({
|
|
49
|
+
kid: signer.id,
|
|
50
|
+
alg: 'ES256',
|
|
51
|
+
kty: 'EC',
|
|
52
|
+
crv: 'P-256'
|
|
53
|
+
}),
|
|
54
|
+
certificateChain,
|
|
55
|
+
origin: authorizationRequest.expected_origins[0]
|
|
56
|
+
}
|
|
57
|
+
}, _createMdocContext({signer}));
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
meta: {authorizationRequest},
|
|
61
|
+
request: {
|
|
62
|
+
protocol: 'org-iso-mdoc',
|
|
63
|
+
data: request
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _createDocRequests({dcqlQuery} = {}) {
|
|
69
|
+
// process all `mso_mdoc` queries as a separate doc request
|
|
70
|
+
const credentials = dcqlQuery?.credentials
|
|
71
|
+
?.filter(c => c?.format === 'mso_mdoc') ?? [];
|
|
72
|
+
return credentials.map(c => {
|
|
73
|
+
const docType = c.meta?.doctype_value;
|
|
74
|
+
const namespaces = new Map();
|
|
75
|
+
const claims = c.claims ?? [];
|
|
76
|
+
for(const claim of claims) {
|
|
77
|
+
const path = claim.path;
|
|
78
|
+
if(!(Array.isArray(path) && path.length >= 2)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const [namespace, field] = path;
|
|
82
|
+
let fields = namespaces.get(namespace);
|
|
83
|
+
if(!fields) {
|
|
84
|
+
namespaces.set(namespace, fields = new Map());
|
|
85
|
+
}
|
|
86
|
+
fields.set(field, true);
|
|
87
|
+
}
|
|
88
|
+
return {docType, namespaces};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _createMdocContext({signer}) {
|
|
93
|
+
const crypto = globalThis.crypto;
|
|
94
|
+
return {
|
|
95
|
+
crypto: {
|
|
96
|
+
async digest({digestAlgorithm, bytes}) {
|
|
97
|
+
const digest = await crypto.subtle.digest(digestAlgorithm, bytes);
|
|
98
|
+
return new Uint8Array(digest);
|
|
99
|
+
},
|
|
100
|
+
random(length) {
|
|
101
|
+
return crypto.getRandomValues(new Uint8Array(length));
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
cose: {
|
|
105
|
+
sign1: {
|
|
106
|
+
async sign({toBeSigned}) {
|
|
107
|
+
return signer.sign({data: toBeSigned});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
package/lib/oid4/oid4vp.js
CHANGED
|
@@ -107,15 +107,21 @@ export async function getOID4VPProtocols({workflow, exchange, step}) {
|
|
|
107
107
|
} = await _getOrCreateStepAuthorizationRequest({
|
|
108
108
|
workflow, exchange, clientProfileId, clientProfile, step
|
|
109
109
|
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
searchParams
|
|
110
|
+
if(authorizationRequest.response_mode === 'dc_api') {
|
|
111
|
+
// use `request_uri` only
|
|
112
|
+
protocols[name] = `${clientBaseUrl}/authorization/request`;
|
|
113
|
+
} else {
|
|
114
|
+
// build OID4VP-style URL
|
|
115
|
+
const {client_id, request_uri_method} = authorizationRequest;
|
|
116
|
+
const searchParams = new URLSearchParams({
|
|
117
|
+
client_id,
|
|
118
|
+
request_uri: `${clientBaseUrl}/authorization/request`
|
|
119
|
+
});
|
|
120
|
+
if(request_uri_method && version !== 'OID4VP-draft18') {
|
|
121
|
+
searchParams.set('request_uri_method', request_uri_method);
|
|
122
|
+
}
|
|
123
|
+
protocols[name] = `${scheme}://?${searchParams}`;
|
|
117
124
|
}
|
|
118
|
-
protocols[name] = `${scheme}://?${searchParams}`;
|
|
119
125
|
}
|
|
120
126
|
return protocols;
|
|
121
127
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import {AsymmetricKey, KmsClient} from '@digitalbazaar/webkms-client';
|
|
6
|
+
import {getZcapClient} from '../helpers.js';
|
|
7
|
+
import {httpsAgent} from '@bedrock/https-agent';
|
|
8
|
+
import {importJWK} from 'jose';
|
|
9
|
+
|
|
10
|
+
const {util: {BedrockError}} = bedrock;
|
|
11
|
+
|
|
12
|
+
export async function getRequestSigningParameters({
|
|
13
|
+
workflow, clientProfile
|
|
14
|
+
} = {}) {
|
|
15
|
+
try {
|
|
16
|
+
// create zcap client
|
|
17
|
+
const {zcapClient, zcaps} = await getZcapClient({workflow});
|
|
18
|
+
|
|
19
|
+
// get any `x5c` and either `privateKeyJwk` or the zcap to use to sign the
|
|
20
|
+
// authz request via the client profile
|
|
21
|
+
const {
|
|
22
|
+
authorizationRequestSigningParameters: {privateKeyJwk} = {},
|
|
23
|
+
zcapReferenceIds: {signAuthorizationRequest: refId} = {}
|
|
24
|
+
} = clientProfile;
|
|
25
|
+
if(privateKeyJwk === undefined && refId === undefined) {
|
|
26
|
+
throw new BedrockError(
|
|
27
|
+
'The OID4VP client profile does not specify the private key or ' +
|
|
28
|
+
'capability in the workflow configuration to use to sign ' +
|
|
29
|
+
'authorization requests.', {
|
|
30
|
+
name: 'DataError',
|
|
31
|
+
details: {httpStatusCode: 500, public: true}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// code assumed ES256 (ECDSA P-256 + SHA-256) support only
|
|
36
|
+
const alg = 'ES256';
|
|
37
|
+
let kid;
|
|
38
|
+
let signer;
|
|
39
|
+
if(privateKeyJwk) {
|
|
40
|
+
kid = privateKeyJwk.kid;
|
|
41
|
+
|
|
42
|
+
// import `privateKeyJwk`
|
|
43
|
+
const privateKey = await importJWK(privateKeyJwk, alg);
|
|
44
|
+
|
|
45
|
+
// create signer API
|
|
46
|
+
const algorithm = {name: 'ECDSA', hash: {name: 'SHA-256'}};
|
|
47
|
+
signer = {
|
|
48
|
+
async sign({data}) {
|
|
49
|
+
return new Uint8Array(
|
|
50
|
+
await crypto.subtle.sign(algorithm, privateKey, data));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
} else {
|
|
54
|
+
const capability = zcaps[refId];
|
|
55
|
+
if(capability === undefined) {
|
|
56
|
+
throw new BedrockError(
|
|
57
|
+
'The capability specified by the OID4VP client profile for signing ' +
|
|
58
|
+
'authorization requests was not found in the workflow ' +
|
|
59
|
+
'configuration.', {
|
|
60
|
+
name: 'DataError',
|
|
61
|
+
details: {httpStatusCode: 500, public: true}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// create a WebKMS `signer` interface
|
|
66
|
+
const {invocationSigner} = zcapClient;
|
|
67
|
+
const kmsClient = new KmsClient({httpsAgent});
|
|
68
|
+
signer = await AsymmetricKey.fromCapability({
|
|
69
|
+
capability, invocationSigner, kmsClient
|
|
70
|
+
});
|
|
71
|
+
const keyDescription = await signer.getKeyDescription();
|
|
72
|
+
kid = keyDescription.id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {signer, privateKeyJwk, kid, alg};
|
|
76
|
+
} catch(cause) {
|
|
77
|
+
throw new BedrockError(
|
|
78
|
+
`Could not get request signing parameters: ${cause.message}`, {
|
|
79
|
+
name: cause instanceof BedrockError ? cause.name : 'OperationError',
|
|
80
|
+
cause,
|
|
81
|
+
details: {httpStatusCode: 500, public: true}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock/vc-delivery",
|
|
3
|
-
"version": "7.19.
|
|
3
|
+
"version": "7.19.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bedrock Verifiable Credential Delivery",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@digitalbazaar/ed25519-signature-2020": "^5.4.0",
|
|
41
41
|
"@digitalbazaar/ezcap": "^4.1.0",
|
|
42
42
|
"@digitalbazaar/http-client": "^4.4.0",
|
|
43
|
-
"@digitalbazaar/oid4-client": "^5.15.
|
|
43
|
+
"@digitalbazaar/oid4-client": "^5.15.2",
|
|
44
44
|
"@digitalbazaar/vc": "^7.2.0",
|
|
45
45
|
"@digitalbazaar/webkms-client": "^14.2.0",
|
|
46
46
|
"assert-plus": "^1.0.0",
|