@bedrock/vc-delivery 7.18.1 → 7.19.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/helpers.js +21 -0
- package/lib/oid4/authorizationRequest.js +48 -25
- package/lib/oid4/authorizationResponse.js +29 -25
- package/lib/oid4/oid4vci.js +4 -23
- package/lib/oid4/oid4vp.js +4 -47
- package/package.json +2 -2
- package/schemas/bedrock-vc-workflow.js +27 -0
package/lib/helpers.js
CHANGED
|
@@ -175,6 +175,27 @@ export async function generateRandom() {
|
|
|
175
175
|
});
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
export function getAlgFromJwk({jwk} = {}) {
|
|
179
|
+
if(jwk.alg) {
|
|
180
|
+
return jwk.alg;
|
|
181
|
+
}
|
|
182
|
+
if(jwk.kty === 'EC' && jwk.crv) {
|
|
183
|
+
if(jwk.crv.startsWith('P-')) {
|
|
184
|
+
return `ES${jwk.crv.slice(2)}`;
|
|
185
|
+
}
|
|
186
|
+
if(jwk.crv === 'secp256k1') {
|
|
187
|
+
return 'ES256K';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if(jwk.kty === 'OKP' && jwk.crv?.startsWith('Ed')) {
|
|
191
|
+
return 'EdDSA';
|
|
192
|
+
}
|
|
193
|
+
if(jwk.kty === 'RSA') {
|
|
194
|
+
return 'PS256';
|
|
195
|
+
}
|
|
196
|
+
return 'invalid';
|
|
197
|
+
}
|
|
198
|
+
|
|
178
199
|
export function getWorkflowIssuerInstances({workflow} = {}) {
|
|
179
200
|
let {issuerInstances} = workflow;
|
|
180
201
|
if(!issuerInstances && workflow.zcaps.issue) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import {AsymmetricKey, KmsClient} from '@digitalbazaar/webkms-client';
|
|
6
|
-
import {exportJWK, generateKeyPair} from 'jose';
|
|
6
|
+
import {exportJWK, generateKeyPair, importJWK} from 'jose';
|
|
7
7
|
import {oid4vp, signJWT} from '@digitalbazaar/oid4-client';
|
|
8
8
|
import {getClientBaseUrl} from './clientProfiles.js';
|
|
9
9
|
import {getZcapClient} from '../helpers.js';
|
|
@@ -237,42 +237,65 @@ async function _createJwt({workflow, clientProfile, authorizationRequest}) {
|
|
|
237
237
|
// create zcap client
|
|
238
238
|
const {zcapClient, zcaps} = await getZcapClient({workflow});
|
|
239
239
|
|
|
240
|
-
// get any `x5c` and the zcap to use to sign the
|
|
241
|
-
// the client profile
|
|
240
|
+
// get any `x5c` and either `privateKeyJwk` or the zcap to use to sign the
|
|
241
|
+
// authz request via the client profile
|
|
242
242
|
const {
|
|
243
|
-
authorizationRequestSigningParameters: {x5c} = {},
|
|
243
|
+
authorizationRequestSigningParameters: {privateKeyJwk, x5c} = {},
|
|
244
244
|
zcapReferenceIds: {signAuthorizationRequest: refId} = {}
|
|
245
245
|
} = clientProfile;
|
|
246
|
-
if(refId === undefined) {
|
|
246
|
+
if(privateKeyJwk === undefined && refId === undefined) {
|
|
247
247
|
throw new BedrockError(
|
|
248
|
-
'The OID4VP client profile does not specify
|
|
249
|
-
'workflow configuration to use to sign
|
|
250
|
-
|
|
251
|
-
details: {httpStatusCode: 500, public: true}
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
const capability = zcaps[refId];
|
|
255
|
-
if(capability === undefined) {
|
|
256
|
-
throw new BedrockError(
|
|
257
|
-
'The capability specified by the OID4VP client profile for signing ' +
|
|
258
|
-
'authorization requests was not found in the workflow configuration.', {
|
|
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.', {
|
|
259
251
|
name: 'DataError',
|
|
260
252
|
details: {httpStatusCode: 500, public: true}
|
|
261
253
|
});
|
|
262
254
|
}
|
|
263
255
|
|
|
264
|
-
//
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
}
|
|
272
295
|
|
|
273
296
|
// create the JWT payload and header to be signed
|
|
274
297
|
const payload = {...authorizationRequest};
|
|
275
|
-
const protectedHeader = {typ: OID4VP_JWT_TYP, alg
|
|
298
|
+
const protectedHeader = {typ: OID4VP_JWT_TYP, alg, kid, x5c};
|
|
276
299
|
|
|
277
300
|
// create the JWT
|
|
278
301
|
return signJWT({payload, protectedHeader, signer});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022-2026 Digital Bazaar, Inc.
|
|
2
|
+
* Copyright (c) 2022-2026 Digital Bazaar, Inc.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import {
|
|
@@ -16,8 +16,6 @@ const VALIDATORS = {
|
|
|
16
16
|
presentationSubmission: null
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
|
|
20
|
-
|
|
21
19
|
bedrock.events.on('bedrock.init', () => {
|
|
22
20
|
VALIDATORS.presentation = compile({schema: verifiablePresentationSchema()});
|
|
23
21
|
VALIDATORS.presentationSubmission = compile({
|
|
@@ -33,11 +31,14 @@ export async function parse({
|
|
|
33
31
|
const {
|
|
34
32
|
responseMode, parsed, protectedHeader,
|
|
35
33
|
recipientPublicJwk, recipientPublicJwkThumbprint,
|
|
36
|
-
vpTokenMediaType
|
|
34
|
+
vpTokenMediaType,
|
|
35
|
+
envelope, presentation, mdocOptions
|
|
37
36
|
} = await oid4vp.verifier.parseAuthorizationResponse({
|
|
38
37
|
body,
|
|
39
|
-
getDecryptParameters() {
|
|
40
|
-
return _getDecryptParameters({
|
|
38
|
+
getDecryptParameters({mdocOptions} = {}) {
|
|
39
|
+
return _getDecryptParameters({
|
|
40
|
+
exchange, clientProfileId, authorizationRequest, mdocOptions
|
|
41
|
+
});
|
|
41
42
|
},
|
|
42
43
|
authorizationRequest
|
|
43
44
|
});
|
|
@@ -48,22 +49,8 @@ export async function parse({
|
|
|
48
49
|
_validate(VALIDATORS.presentationSubmission, presentationSubmission);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
let presentation;
|
|
54
|
-
let envelope;
|
|
55
|
-
|
|
56
|
-
if(vpTokenMediaType !== 'application/vp') {
|
|
57
|
-
// `vp_token` contains some enveloped format
|
|
58
|
-
presentation = {
|
|
59
|
-
'@context': VC_CONTEXT_2,
|
|
60
|
-
id: `data:${vpTokenMediaType},${vpToken}`,
|
|
61
|
-
type: 'EnvelopedVerifiablePresentation'
|
|
62
|
-
};
|
|
63
|
-
envelope = {mediaType: vpTokenMediaType};
|
|
64
|
-
} else {
|
|
65
|
-
// simplest case: `vpToken` is a VP; validate it against basic schema
|
|
66
|
-
presentation = vpToken;
|
|
52
|
+
// validate `presentation` against basic schema
|
|
53
|
+
if(vpTokenMediaType === 'application/vp') {
|
|
67
54
|
_validate(VALIDATORS.presentation, presentation);
|
|
68
55
|
}
|
|
69
56
|
|
|
@@ -74,7 +61,8 @@ export async function parse({
|
|
|
74
61
|
envelope,
|
|
75
62
|
protectedHeader,
|
|
76
63
|
recipientPublicJwk,
|
|
77
|
-
recipientPublicJwkThumbprint
|
|
64
|
+
recipientPublicJwkThumbprint,
|
|
65
|
+
mdocOptions
|
|
78
66
|
};
|
|
79
67
|
} catch(cause) {
|
|
80
68
|
throw new BedrockError(
|
|
@@ -89,12 +77,28 @@ export async function parse({
|
|
|
89
77
|
}
|
|
90
78
|
}
|
|
91
79
|
|
|
92
|
-
function _getDecryptParameters({
|
|
80
|
+
function _getDecryptParameters({
|
|
81
|
+
exchange, clientProfileId, authorizationRequest, mdocOptions
|
|
82
|
+
}) {
|
|
93
83
|
// get private key agreement keys in JWT format
|
|
94
84
|
const {keyAgreementKeyPairs} = exchange.secrets?.oid4vp?.clientProfiles
|
|
95
85
|
?.[clientProfileId ?? 'default'] ?? {};
|
|
96
86
|
const keys = keyAgreementKeyPairs.map(({privateKeyJwk}) => privateKeyJwk);
|
|
97
|
-
|
|
87
|
+
|
|
88
|
+
// include decryption AAD info if `response_mode` calls for it
|
|
89
|
+
let getInfo;
|
|
90
|
+
if(authorizationRequest.response_mode === 'dc_api') {
|
|
91
|
+
// include `recipientPublicJwk` in expected mdoc handover
|
|
92
|
+
getInfo = ({recipientPublicJwk}) => {
|
|
93
|
+
const handover = {
|
|
94
|
+
...mdocOptions?.expectedHandover,
|
|
95
|
+
recipientPublicJwk
|
|
96
|
+
};
|
|
97
|
+
return oid4vp.mdl.encodeSessionTranscript({handover});
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {keys, getInfo};
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
function _validate(validator, data) {
|
package/lib/oid4/oid4vci.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import * as draft13 from './oid4vciDraft13.js';
|
|
6
6
|
import {issue as defaultIssue, getIssueRequestsParams} from '../issue.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getAlgFromJwk, getWorkflowIssuerInstances, setVariable
|
|
9
|
+
} from '../helpers.js';
|
|
8
10
|
import {importJWK, SignJWT} from 'jose';
|
|
9
11
|
import {timingSafeEqual, randomUUID as uuid} from 'node:crypto';
|
|
10
12
|
import {
|
|
@@ -648,7 +650,7 @@ function _createSupportedCredentialRequests({
|
|
|
648
650
|
async function _createOAuth2AccessToken({
|
|
649
651
|
privateKeyJwk, audience, action, target, exp, iss, nbf, typ = 'at+jwt'
|
|
650
652
|
}) {
|
|
651
|
-
const alg =
|
|
653
|
+
const alg = getAlgFromJwk({jwk: privateKeyJwk});
|
|
652
654
|
const scope = `${action}:${target}`;
|
|
653
655
|
const builder = new SignJWT({scope})
|
|
654
656
|
.setProtectedHeader({alg, typ})
|
|
@@ -671,27 +673,6 @@ async function _createOAuth2AccessToken({
|
|
|
671
673
|
return {accessToken, ttl};
|
|
672
674
|
}
|
|
673
675
|
|
|
674
|
-
function _getAlgFromPrivateKey({privateKeyJwk}) {
|
|
675
|
-
if(privateKeyJwk.alg) {
|
|
676
|
-
return privateKeyJwk.alg;
|
|
677
|
-
}
|
|
678
|
-
if(privateKeyJwk.kty === 'EC' && privateKeyJwk.crv) {
|
|
679
|
-
if(privateKeyJwk.crv.startsWith('P-')) {
|
|
680
|
-
return `ES${privateKeyJwk.crv.slice(2)}`;
|
|
681
|
-
}
|
|
682
|
-
if(privateKeyJwk.crv === 'secp256k1') {
|
|
683
|
-
return 'ES256K';
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
if(privateKeyJwk.kty === 'OKP' && privateKeyJwk.crv?.startsWith('Ed')) {
|
|
687
|
-
return 'EdDSA';
|
|
688
|
-
}
|
|
689
|
-
if(privateKeyJwk.kty === 'RSA') {
|
|
690
|
-
return 'PS256';
|
|
691
|
-
}
|
|
692
|
-
return 'invalid';
|
|
693
|
-
}
|
|
694
|
-
|
|
695
676
|
function _getSupportedCredentialConfigurations({workflow, exchange, step}) {
|
|
696
677
|
// get all OID4VCI credential configuration IDs from issue requests in step
|
|
697
678
|
const issueRequestsParams = getIssueRequestsParams({
|
package/lib/oid4/oid4vp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022-2026 Digital Bazaar, Inc.
|
|
2
|
+
* Copyright (c) 2022-2026 Digital Bazaar, Inc.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import {
|
|
@@ -199,52 +199,9 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
199
199
|
expectedDomain = verifyPresentationOptions.domain;
|
|
200
200
|
|
|
201
201
|
const {envelope} = parseResponseResult;
|
|
202
|
-
if(
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
// generate `handover` for mdoc verification
|
|
206
|
-
let handover;
|
|
207
|
-
|
|
208
|
-
// common `handover` parameters:
|
|
209
|
-
const origin = authorizationRequest?.expected_origins?.[0] ??
|
|
210
|
-
new URL(authorizationRequest.response_uri).origin;
|
|
211
|
-
const nonce = authorizationRequest.nonce;
|
|
212
|
-
|
|
213
|
-
// `direct_post.jwt` => ISO18013-7 Annex B
|
|
214
|
-
// FIXME: same response mode is also used for OID4VP 1.0 with
|
|
215
|
-
// `OpenID4VPHandover` for non-Annex-B; this is not yet supported
|
|
216
|
-
// https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-invocation-via-redirects
|
|
217
|
-
const {responseMode} = parseResponseResult;
|
|
218
|
-
if(responseMode === 'direct_post.jwt') {
|
|
219
|
-
handover = {
|
|
220
|
-
type: 'AnnexBHandover',
|
|
221
|
-
// per ISO 18013-7 B the `mdocGeneratedNonce` is base64url-encoded
|
|
222
|
-
// and put into the `apu` protected header parameter, so parse that
|
|
223
|
-
// here and convert it to a UTF-8 string instead
|
|
224
|
-
mdocGeneratedNonce: Buffer
|
|
225
|
-
.from(parseResponseResult.protectedHeader?.apu ?? '', 'base64url')
|
|
226
|
-
.toString('utf8'),
|
|
227
|
-
clientId: authorizationRequest.client_id,
|
|
228
|
-
responseUri: authorizationRequest.response_uri,
|
|
229
|
-
verifierGeneratedNonce: nonce
|
|
230
|
-
};
|
|
231
|
-
} else if(responseMode === 'dc_api') {
|
|
232
|
-
// `dc_api` => ISO18013-7 Annex C
|
|
233
|
-
handover = {
|
|
234
|
-
type: 'dcapi',
|
|
235
|
-
origin,
|
|
236
|
-
nonce,
|
|
237
|
-
recipientPublicJwk: parseResponseResult.recipientPublicJwk
|
|
238
|
-
};
|
|
239
|
-
} else if(responseMode === 'dc_api.jwt') {
|
|
240
|
-
// `dc_api.jwt` => ISO18013-7 Annex D
|
|
241
|
-
handover = {
|
|
242
|
-
type: 'OpenID4VPDCAPIHandover',
|
|
243
|
-
origin,
|
|
244
|
-
nonce,
|
|
245
|
-
jwkThumbprint: parseResponseResult.recipientPublicJwkThumbprint
|
|
246
|
-
};
|
|
247
|
-
}
|
|
202
|
+
if(parseResponseResult?.mdocOptions) {
|
|
203
|
+
// get expected `handover` for mdoc verification
|
|
204
|
+
const {expectedHandover: handover} = parseResponseResult.mdocOptions;
|
|
248
205
|
|
|
249
206
|
// handle deprecated `mdl` option; `mdoc` is preferred
|
|
250
207
|
const mdocKey = envelope.mediaType === 'application/mdoc-vp-token' ?
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock/vc-delivery",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.19.0",
|
|
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.
|
|
43
|
+
"@digitalbazaar/oid4-client": "^5.15.0",
|
|
44
44
|
"@digitalbazaar/vc": "^7.2.0",
|
|
45
45
|
"@digitalbazaar/webkms-client": "^14.2.0",
|
|
46
46
|
"assert-plus": "^1.0.0",
|
|
@@ -547,6 +547,22 @@ const oid4vpClientProfile = {
|
|
|
547
547
|
required: ['x5c'],
|
|
548
548
|
additionalProperties: false,
|
|
549
549
|
properties: {
|
|
550
|
+
privateKeyJwk: {
|
|
551
|
+
type: 'object',
|
|
552
|
+
required: ['kid', 'kty', 'crv', 'x', 'y', 'd'],
|
|
553
|
+
properties: {
|
|
554
|
+
// only ES256 supported at this time
|
|
555
|
+
alg: {enum: ['ES256']},
|
|
556
|
+
kid: {type: 'string'},
|
|
557
|
+
kty: {enum: ['EC']},
|
|
558
|
+
crv: {enum: ['P-256']},
|
|
559
|
+
x: {type: 'string'},
|
|
560
|
+
y: {type: 'string'},
|
|
561
|
+
d: {type: 'string'},
|
|
562
|
+
use: {const: 'sig'},
|
|
563
|
+
key_ops: {const: ['sign']}
|
|
564
|
+
}
|
|
565
|
+
},
|
|
550
566
|
x5c: {
|
|
551
567
|
type: 'array',
|
|
552
568
|
minItems: 1,
|
|
@@ -999,6 +1015,13 @@ export function openIdAuthorizationResponseBody() {
|
|
|
999
1015
|
not: {
|
|
1000
1016
|
required: ['presentation_submission', 'vp_token', 'state']
|
|
1001
1017
|
}
|
|
1018
|
+
}, {
|
|
1019
|
+
// for response_mode == 'dc_api'
|
|
1020
|
+
required: ['Response'],
|
|
1021
|
+
// cannot also use any other params
|
|
1022
|
+
not: {
|
|
1023
|
+
required: ['presentation_submission', 'vp_token', 'state']
|
|
1024
|
+
}
|
|
1002
1025
|
}],
|
|
1003
1026
|
properties: {
|
|
1004
1027
|
// is a JSON string in the x-www-form-urlencoded body
|
|
@@ -1027,6 +1050,10 @@ export function openIdAuthorizationResponseBody() {
|
|
|
1027
1050
|
// must be an encrypted JWT
|
|
1028
1051
|
type: 'string'
|
|
1029
1052
|
},
|
|
1053
|
+
Response: {
|
|
1054
|
+
// must be an HPKE encrypted mdoc device response (ISO 18013-7 Annex C)
|
|
1055
|
+
type: 'string'
|
|
1056
|
+
},
|
|
1030
1057
|
state: {
|
|
1031
1058
|
type: 'string'
|
|
1032
1059
|
}
|