@bedrock/vc-delivery 2.0.0 → 3.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/http.js +35 -6
- package/lib/index.js +1 -1
- package/lib/{oidc4vci.js → openId.js} +212 -66
- package/lib/verify.js +2 -2
- package/package.json +5 -5
package/lib/http.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2018-
|
|
2
|
+
* Copyright (c) 2018-2023 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
+
import * as _openId from './openId.js';
|
|
4
5
|
import * as bedrock from '@bedrock/core';
|
|
5
6
|
import * as exchanges from './exchanges.js';
|
|
6
|
-
import * as oidc4vci from './oidc4vci.js';
|
|
7
7
|
import {createChallenge as _createChallenge, verify} from './verify.js';
|
|
8
8
|
import {
|
|
9
9
|
createExchangeBody, useExchangeBody
|
|
10
10
|
} from '../schemas/bedrock-vc-exchanger.js';
|
|
11
|
+
import {exportJWK, generateKeyPair, importJWK} from 'jose';
|
|
11
12
|
import {metering, middleware} from '@bedrock/service-core';
|
|
12
13
|
import {asyncHandler} from '@bedrock/express';
|
|
13
14
|
import bodyParser from 'body-parser';
|
|
@@ -70,7 +71,7 @@ export async function addRoutes({app, service} = {}) {
|
|
|
70
71
|
try {
|
|
71
72
|
const {config} = req.serviceObject;
|
|
72
73
|
const {
|
|
73
|
-
ttl,
|
|
74
|
+
ttl, openId, variables = {},
|
|
74
75
|
// allow steps to be skipped by creator as needed
|
|
75
76
|
step = config.initialStep
|
|
76
77
|
} = req.body;
|
|
@@ -83,13 +84,41 @@ export async function addRoutes({app, service} = {}) {
|
|
|
83
84
|
});
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// perform key generation if requested
|
|
88
|
+
if(openId?.oauth2?.generateKeyPair) {
|
|
89
|
+
const {oauth2} = openId;
|
|
90
|
+
const {algorithm} = oauth2.generateKeyPair;
|
|
91
|
+
const kp = await generateKeyPair(algorithm, {extractable: true});
|
|
92
|
+
const [privateKeyJwk, publicKeyJwk] = await Promise.all([
|
|
93
|
+
exportJWK(kp.privateKey),
|
|
94
|
+
exportJWK(kp.publicKey),
|
|
95
|
+
]);
|
|
96
|
+
oauth2.keyPair = {privateKeyJwk, publicKeyJwk};
|
|
97
|
+
delete oauth2.generateKeyPair;
|
|
98
|
+
} else if(openId) {
|
|
99
|
+
// ensure key pair can be imported
|
|
100
|
+
try {
|
|
101
|
+
const {oauth2: {keyPair}} = openId;
|
|
102
|
+
await Promise.all([
|
|
103
|
+
importJWK(keyPair.privateKeyJwk),
|
|
104
|
+
importJWK(keyPair.publicKeyJwk)
|
|
105
|
+
]);
|
|
106
|
+
} catch(e) {
|
|
107
|
+
throw new BedrockError('Could not import OpenID OAuth2 key pair.', {
|
|
108
|
+
name: 'DataError',
|
|
109
|
+
details: {httpStatusCode: 400, public: true},
|
|
110
|
+
cause: e
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
// insert exchange
|
|
87
116
|
const {id: exchangerId} = config;
|
|
88
117
|
const exchange = {
|
|
89
118
|
id: await generateRandom(),
|
|
90
119
|
ttl,
|
|
91
120
|
variables,
|
|
92
|
-
|
|
121
|
+
openId,
|
|
93
122
|
step
|
|
94
123
|
};
|
|
95
124
|
await exchanges.insert({exchangerId, exchange});
|
|
@@ -189,7 +218,7 @@ export async function addRoutes({app, service} = {}) {
|
|
|
189
218
|
res.json({verifiablePresentation});
|
|
190
219
|
}));
|
|
191
220
|
|
|
192
|
-
// create
|
|
193
|
-
await
|
|
221
|
+
// create OID4VCI routes to be used with each individual exchange
|
|
222
|
+
await _openId.createRoutes(
|
|
194
223
|
{app, exchangeRoute: routes.exchange, getConfigMiddleware, getExchange});
|
|
195
224
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved.
|
|
2
|
+
* Copyright (c) 2022-2023 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as exchanges from './exchanges.js';
|
|
5
5
|
import {importJWK, SignJWT} from 'jose';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
openIdBatchCredentialBody, openIdCredentialBody, openIdTokenBody
|
|
8
8
|
} from '../schemas/bedrock-vc-exchanger.js';
|
|
9
9
|
import {asyncHandler} from '@bedrock/express';
|
|
10
10
|
import bodyParser from 'body-parser';
|
|
@@ -15,10 +15,10 @@ import {timingSafeEqual} from 'node:crypto';
|
|
|
15
15
|
import {createValidateMiddleware as validate} from '@bedrock/validation';
|
|
16
16
|
import {verifyDidProofJwt} from './verify.js';
|
|
17
17
|
|
|
18
|
-
/* NOTE: Parts of the
|
|
18
|
+
/* NOTE: Parts of the OID4VCI design imply tight integration between the
|
|
19
19
|
authorization server and the credential issuance / delivery server. This
|
|
20
20
|
file provides the routes for both and treats them as integrated; supporting
|
|
21
|
-
the
|
|
21
|
+
the OID4VCI pre-authz code flow only as a result. However, we also try to
|
|
22
22
|
avoid tight-coupling where possible to enable the non-pre-authz code flow
|
|
23
23
|
that would use, somehow, a separate authorization server.
|
|
24
24
|
|
|
@@ -30,7 +30,7 @@ authentic without breaking some abstraction around how the Authorization
|
|
|
30
30
|
Server is implemented behind its API. Here we do not implement this option,
|
|
31
31
|
instead, if a challenge is required, the credential delivery server will send
|
|
32
32
|
an error with the challenge nonce if one was not provided in the payload to the
|
|
33
|
-
credential endpoint. This error follows the
|
|
33
|
+
credential endpoint. This error follows the OID4VCI spec and avoids this
|
|
34
34
|
particular tight coupling.
|
|
35
35
|
|
|
36
36
|
Other tight couplings cannot be avoided at this time -- such as the fact that
|
|
@@ -43,17 +43,18 @@ instantiating a new authorization server instance per VC exchange. */
|
|
|
43
43
|
const PRE_AUTH_GRANT_TYPE =
|
|
44
44
|
'urn:ietf:params:oauth:grant-type:pre-authorized_code';
|
|
45
45
|
|
|
46
|
-
// creates
|
|
46
|
+
// creates OID4VCI Authorization Server + Credential Delivery Server
|
|
47
47
|
// endpoints for each individual exchange
|
|
48
48
|
export async function createRoutes({
|
|
49
49
|
app, exchangeRoute, getConfigMiddleware, getExchange
|
|
50
50
|
} = {}) {
|
|
51
|
-
const
|
|
51
|
+
const openIdRoute = `${exchangeRoute}/openid`;
|
|
52
52
|
const routes = {
|
|
53
53
|
asMetadata: `/.well-known/oauth-authorization-server${exchangeRoute}`,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
batchCredential: `${openIdRoute}/batch_credential`,
|
|
55
|
+
credential: `${openIdRoute}/credential`,
|
|
56
|
+
token: `${openIdRoute}/token`,
|
|
57
|
+
jwks: `${openIdRoute}/jwks`
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
// urlencoded body parser (extended=true for rich JSON-like representation)
|
|
@@ -72,9 +73,11 @@ export async function createRoutes({
|
|
|
72
73
|
const exchangeId = `${exchanger.id}/exchanges/${req.params.exchangeId}`;
|
|
73
74
|
const oauth2Config = {
|
|
74
75
|
issuer: exchangeId,
|
|
75
|
-
jwks_uri: `${exchangeId}/
|
|
76
|
-
token_endpoint: `${exchangeId}/
|
|
77
|
-
credential_endpoint: `${exchangeId}/
|
|
76
|
+
jwks_uri: `${exchangeId}/openid/jwks`,
|
|
77
|
+
token_endpoint: `${exchangeId}/openid/token`,
|
|
78
|
+
credential_endpoint: `${exchangeId}/openid/credential`,
|
|
79
|
+
batch_credential_endpoint: `${exchangeId}/openid/batch_credential`
|
|
80
|
+
// FIXME: add `credentials_supported`
|
|
78
81
|
};
|
|
79
82
|
res.json(oauth2Config);
|
|
80
83
|
}));
|
|
@@ -88,13 +91,13 @@ export async function createRoutes({
|
|
|
88
91
|
getExchange,
|
|
89
92
|
asyncHandler(async (req, res) => {
|
|
90
93
|
const {exchange} = await req.exchange;
|
|
91
|
-
if(!exchange.
|
|
94
|
+
if(!exchange.openId) {
|
|
92
95
|
// FIXME: improve error
|
|
93
96
|
// unsupported protocol for the exchange
|
|
94
97
|
throw new Error('unsupported protocol');
|
|
95
98
|
}
|
|
96
99
|
// serve exchange's public key
|
|
97
|
-
res.json({keys: [exchange.
|
|
100
|
+
res.json({keys: [exchange.openId.oauth2.keyPair.publicKeyJwk]});
|
|
98
101
|
}));
|
|
99
102
|
|
|
100
103
|
// an authorization server endpoint
|
|
@@ -105,13 +108,13 @@ export async function createRoutes({
|
|
|
105
108
|
routes.token,
|
|
106
109
|
cors(),
|
|
107
110
|
urlencoded,
|
|
108
|
-
validate({bodySchema:
|
|
111
|
+
validate({bodySchema: openIdTokenBody}),
|
|
109
112
|
getConfigMiddleware,
|
|
110
113
|
getExchange,
|
|
111
114
|
asyncHandler(async (req, res) => {
|
|
112
115
|
const exchangeRecord = await req.exchange;
|
|
113
116
|
const {exchange} = exchangeRecord;
|
|
114
|
-
if(!exchange.
|
|
117
|
+
if(!exchange.openId) {
|
|
115
118
|
// FIXME: improve error
|
|
116
119
|
// unsupported protocol for the exchange
|
|
117
120
|
throw new Error('unsupported protocol');
|
|
@@ -142,7 +145,7 @@ export async function createRoutes({
|
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
// validate grant type
|
|
145
|
-
const {
|
|
148
|
+
const {openId: {preAuthorizedCode: expectedCode}} = exchange;
|
|
146
149
|
if(expectedCode) {
|
|
147
150
|
// ensure expected pre-authz code matches
|
|
148
151
|
if(!timingSafeEqual(
|
|
@@ -173,7 +176,7 @@ export async function createRoutes({
|
|
|
173
176
|
app.post(
|
|
174
177
|
routes.credential,
|
|
175
178
|
cors(),
|
|
176
|
-
validate({bodySchema:
|
|
179
|
+
validate({bodySchema: openIdCredentialBody}),
|
|
177
180
|
getConfigMiddleware,
|
|
178
181
|
getExchange,
|
|
179
182
|
asyncHandler(async (req, res) => {
|
|
@@ -184,8 +187,17 @@ export async function createRoutes({
|
|
|
184
187
|
Authorization: BEARER czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
|
185
188
|
|
|
186
189
|
{
|
|
187
|
-
"type": "https://did.example.org/healthCard"
|
|
188
190
|
"format": "ldp_vc",
|
|
191
|
+
"credential_description": {
|
|
192
|
+
"@context": [
|
|
193
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
194
|
+
"https://www.w3.org/2018/credentials/examples/v1"
|
|
195
|
+
],
|
|
196
|
+
"type": [
|
|
197
|
+
"VerifiableCredential",
|
|
198
|
+
"UniversityDegreeCredential"
|
|
199
|
+
]
|
|
200
|
+
},
|
|
189
201
|
"did": "did:example:ebfeb1f712ebc6f1c276e12ec21",
|
|
190
202
|
"proof": {
|
|
191
203
|
"proof_type": "jwt",
|
|
@@ -193,54 +205,72 @@ export async function createRoutes({
|
|
|
193
205
|
}
|
|
194
206
|
}
|
|
195
207
|
*/
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// unsupported protocol for the exchange
|
|
202
|
-
throw new Error('unsupported protocol');
|
|
208
|
+
const result = await _processCredentialRequests(
|
|
209
|
+
{req, res, isBatchRequest: false});
|
|
210
|
+
if(!result) {
|
|
211
|
+
// DID proof request response sent
|
|
212
|
+
return;
|
|
203
213
|
}
|
|
204
214
|
|
|
205
|
-
//
|
|
206
|
-
|
|
215
|
+
// send VC
|
|
216
|
+
res.json({
|
|
217
|
+
format: 'ldp_vc',
|
|
218
|
+
/* Note: The `/credential` route only supports sending a single VC;
|
|
219
|
+
assume here that this exchanger is configured for a single VC and an
|
|
220
|
+
error code would have been sent to the client to use the batch
|
|
221
|
+
endpoint if there was more than one VC to deliver. */
|
|
222
|
+
credential: result.verifiablePresentation.verifiableCredential[0]
|
|
223
|
+
});
|
|
224
|
+
}));
|
|
207
225
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
226
|
+
// a batch credential delivery server endpoint
|
|
227
|
+
// receives N credential requests and returns N VCs
|
|
228
|
+
app.options(routes.batchCredential, cors());
|
|
229
|
+
app.post(
|
|
230
|
+
routes.batchCredential,
|
|
231
|
+
cors(),
|
|
232
|
+
validate({bodySchema: openIdBatchCredentialBody}),
|
|
233
|
+
getConfigMiddleware,
|
|
234
|
+
getExchange,
|
|
235
|
+
asyncHandler(async (req, res) => {
|
|
236
|
+
/* Clients must POST, e.g.:
|
|
237
|
+
POST /batch_credential HTTP/1.1
|
|
238
|
+
Host: server.example.com
|
|
239
|
+
Content-Type: application/json
|
|
240
|
+
Authorization: BEARER czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
|
211
241
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
242
|
+
{
|
|
243
|
+
credential_requests: [{
|
|
244
|
+
"format": "ldp_vc",
|
|
245
|
+
"credential_description": {
|
|
246
|
+
"@context": [
|
|
247
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
248
|
+
"https://www.w3.org/2018/credentials/examples/v1"
|
|
249
|
+
],
|
|
250
|
+
"type": [
|
|
251
|
+
"VerifiableCredential",
|
|
252
|
+
"UniversityDegreeCredential"
|
|
253
|
+
]
|
|
254
|
+
},
|
|
255
|
+
"did": "did:example:ebfeb1f712ebc6f1c276e12ec21",
|
|
256
|
+
"proof": {
|
|
257
|
+
"proof_type": "jwt",
|
|
258
|
+
"jwt": "eyJra...nOzM"
|
|
218
259
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
260
|
+
}]
|
|
261
|
+
}
|
|
262
|
+
*/
|
|
263
|
+
const result = await _processCredentialRequests(
|
|
264
|
+
{req, res, isBatchRequest: true});
|
|
265
|
+
if(!result) {
|
|
266
|
+
// DID proof request response sent
|
|
267
|
+
return;
|
|
225
268
|
}
|
|
226
269
|
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// replay attack detected) after exchange has been marked complete
|
|
232
|
-
|
|
233
|
-
// issue VCs
|
|
234
|
-
const {verifiablePresentation} = await issue({exchanger, exchange});
|
|
235
|
-
|
|
236
|
-
// send VC
|
|
237
|
-
res.json({
|
|
238
|
-
format: 'ldp_vc',
|
|
239
|
-
/* Note: OIDC4VCI only supports sending a single VC; assume here that
|
|
240
|
-
the exchanger is configured to only allow OIDC4VCI when a single
|
|
241
|
-
VC is being issued. */
|
|
242
|
-
credential: verifiablePresentation.verifiableCredential[0]
|
|
243
|
-
});
|
|
270
|
+
// send VCs
|
|
271
|
+
const responses = result.verifiablePresentation.verifiableCredential.map(
|
|
272
|
+
vc => ({format: 'ldp_vc', credential: vc}));
|
|
273
|
+
res.json({credential_responses: responses});
|
|
244
274
|
}));
|
|
245
275
|
}
|
|
246
276
|
|
|
@@ -253,7 +283,7 @@ async function _createExchangeAccessToken({exchanger, exchangeRecord}) {
|
|
|
253
283
|
// FIXME: allow per-service-object-instance agent via `signer` and custom
|
|
254
284
|
// JWT signer code instead?
|
|
255
285
|
const {exchange} = exchangeRecord;
|
|
256
|
-
const {
|
|
286
|
+
const {openId: {oauth2: {keyPair: {privateKeyJwk}}}} = exchange;
|
|
257
287
|
const exchangeId = `${exchanger.id}/exchanges/${exchange.id}`;
|
|
258
288
|
const {accessToken, ttl} = await _createOAuth2AccessToken({
|
|
259
289
|
privateKeyJwk, audience: exchangeId, action: 'write', target: exchangeId,
|
|
@@ -265,9 +295,10 @@ async function _createExchangeAccessToken({exchanger, exchangeRecord}) {
|
|
|
265
295
|
async function _createOAuth2AccessToken({
|
|
266
296
|
privateKeyJwk, audience, action, target, exp, iss, nbf, typ = 'at+jwt'
|
|
267
297
|
}) {
|
|
298
|
+
const alg = _getAlgFromPrivateKey({privateKeyJwk});
|
|
268
299
|
const scope = `${action}:${target}`;
|
|
269
300
|
const builder = new SignJWT({scope})
|
|
270
|
-
.setProtectedHeader({alg
|
|
301
|
+
.setProtectedHeader({alg, typ})
|
|
271
302
|
.setIssuer(iss)
|
|
272
303
|
.setAudience(audience);
|
|
273
304
|
let ttl;
|
|
@@ -282,14 +313,14 @@ async function _createOAuth2AccessToken({
|
|
|
282
313
|
if(nbf !== undefined) {
|
|
283
314
|
builder.setNotBefore(nbf);
|
|
284
315
|
}
|
|
285
|
-
const key = await importJWK({...privateKeyJwk, alg
|
|
316
|
+
const key = await importJWK({...privateKeyJwk, alg});
|
|
286
317
|
const accessToken = await builder.sign(key);
|
|
287
318
|
return {accessToken, ttl};
|
|
288
319
|
}
|
|
289
320
|
|
|
290
321
|
async function _checkAuthz({req, exchanger, exchange}) {
|
|
291
322
|
// optional oauth2 options
|
|
292
|
-
const {oauth2} = exchange.
|
|
323
|
+
const {oauth2} = exchange.openId;
|
|
293
324
|
const {maxClockSkew} = oauth2;
|
|
294
325
|
|
|
295
326
|
// audience is always the `exchangeId` and cannot be configured; this
|
|
@@ -311,6 +342,97 @@ async function _checkAuthz({req, exchanger, exchange}) {
|
|
|
311
342
|
await checkAccessToken({req, issuerConfigUrl, maxClockSkew, audience});
|
|
312
343
|
}
|
|
313
344
|
|
|
345
|
+
function _getAlgFromPrivateKey({privateKeyJwk}) {
|
|
346
|
+
if(privateKeyJwk.alg) {
|
|
347
|
+
return privateKeyJwk.alg;
|
|
348
|
+
}
|
|
349
|
+
if(privateKeyJwk.kty === 'EC' && privateKeyJwk.crv) {
|
|
350
|
+
if(privateKeyJwk.crv.startsWith('P-')) {
|
|
351
|
+
return `ES${privateKeyJwk.crv.slice(2)}`;
|
|
352
|
+
}
|
|
353
|
+
if(privateKeyJwk.crv === 'secp256k1') {
|
|
354
|
+
return 'ES256K';
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if(privateKeyJwk.kty === 'OKP' && privateKeyJwk.crv?.startsWith('Ed')) {
|
|
358
|
+
return 'EdDSA';
|
|
359
|
+
}
|
|
360
|
+
if(privateKeyJwk.kty === 'RSA') {
|
|
361
|
+
return 'PS256';
|
|
362
|
+
}
|
|
363
|
+
return 'invalid';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function _processCredentialRequests({req, res, isBatchRequest}) {
|
|
367
|
+
const {config: exchanger} = req.serviceObject;
|
|
368
|
+
const exchangeRecord = await req.exchange;
|
|
369
|
+
const {exchange} = exchangeRecord;
|
|
370
|
+
if(!exchange.openId) {
|
|
371
|
+
// FIXME: improve error
|
|
372
|
+
// unsupported protocol for the exchange
|
|
373
|
+
throw new Error('unsupported protocol');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ensure oauth2 access token is valid
|
|
377
|
+
await _checkAuthz({req, exchanger, exchange});
|
|
378
|
+
|
|
379
|
+
// validate body against expected credential requests
|
|
380
|
+
const {openId: {expectedCredentialRequests}} = exchange;
|
|
381
|
+
let credentialRequests;
|
|
382
|
+
if(isBatchRequest) {
|
|
383
|
+
({credential_requests: credentialRequests} = req.body);
|
|
384
|
+
} else {
|
|
385
|
+
if(expectedCredentialRequests.length > 1) {
|
|
386
|
+
// clients interacting with exchanges with more than one VC to be
|
|
387
|
+
// delivered must use the "batch credential" endpoint
|
|
388
|
+
// FIXME: improve error
|
|
389
|
+
throw new Error('batch_credential_endpoint must be used');
|
|
390
|
+
}
|
|
391
|
+
credentialRequests = [req.body];
|
|
392
|
+
}
|
|
393
|
+
_assertCredentialRequests(
|
|
394
|
+
{credentialRequests, expectedCredentialRequests});
|
|
395
|
+
|
|
396
|
+
// process exchange step if present
|
|
397
|
+
if(exchange.step) {
|
|
398
|
+
const step = exchanger.steps[exchange.step];
|
|
399
|
+
|
|
400
|
+
// handle JWT DID Proof request; if step requires it, then `proof` must
|
|
401
|
+
// be in every credential request
|
|
402
|
+
if(step.jwtDidProofRequest) {
|
|
403
|
+
// if no proof is one of the requests...
|
|
404
|
+
if(credentialRequests.some(cr => !cr.proof?.jwt)) {
|
|
405
|
+
return _requestDidProof({res, exchangeRecord});
|
|
406
|
+
}
|
|
407
|
+
// verify every DID proof and get resulting DIDs
|
|
408
|
+
const results = await Promise.all(
|
|
409
|
+
credentialRequests.map(async cr => {
|
|
410
|
+
const {proof: {jwt}} = cr;
|
|
411
|
+
const {did} = await verifyDidProofJwt({exchanger, exchange, jwt});
|
|
412
|
+
return did;
|
|
413
|
+
}));
|
|
414
|
+
// require `did` to be the same for every proof
|
|
415
|
+
// FIXME: determine if this needs to be more flexible
|
|
416
|
+
const did = results[0];
|
|
417
|
+
if(results.some(d => did !== d)) {
|
|
418
|
+
// FIXME: improve error
|
|
419
|
+
throw new Error('every DID must be the same');
|
|
420
|
+
}
|
|
421
|
+
// add `did` to exchange variables
|
|
422
|
+
exchange.variables[exchange.step] = {did};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// mark exchange complete
|
|
427
|
+
await exchanges.complete({exchangerId: exchanger.id, id: exchange.id});
|
|
428
|
+
|
|
429
|
+
// FIXME: decide what the best recovery path is if delivery fails (but no
|
|
430
|
+
// replay attack detected) after exchange has been marked complete
|
|
431
|
+
|
|
432
|
+
// issue VCs
|
|
433
|
+
return issue({exchanger, exchange});
|
|
434
|
+
}
|
|
435
|
+
|
|
314
436
|
async function _requestDidProof({res, exchangeRecord}) {
|
|
315
437
|
/* `9.4 Credential Issuer-provided nonce` allows the credential
|
|
316
438
|
issuer infrastructure to provide the nonce via an error:
|
|
@@ -327,7 +449,7 @@ async function _requestDidProof({res, exchangeRecord}) {
|
|
|
327
449
|
"c_nonce_expires_in": 86400
|
|
328
450
|
}*/
|
|
329
451
|
|
|
330
|
-
/*
|
|
452
|
+
/* OID4VCI exchanges themselves are not replayable and single-step, so the
|
|
331
453
|
challenge to be signed is just the exchange ID itself. An exchange cannot
|
|
332
454
|
be reused and neither can a challenge. */
|
|
333
455
|
const {exchange, meta: {expires}} = exchangeRecord;
|
|
@@ -343,3 +465,27 @@ async function _requestDidProof({res, exchangeRecord}) {
|
|
|
343
465
|
c_nonce_expires_in: ttl
|
|
344
466
|
});
|
|
345
467
|
}
|
|
468
|
+
|
|
469
|
+
function _assertCredentialRequests({
|
|
470
|
+
credentialRequests, expectedCredentialRequests
|
|
471
|
+
}) {
|
|
472
|
+
// ensure every credential request matches against an expected one and none
|
|
473
|
+
// are missing
|
|
474
|
+
if(!(credentialRequests.length === expectedCredentialRequests.length &&
|
|
475
|
+
credentialRequests.every(cr => expectedCredentialRequests.some(
|
|
476
|
+
ecr => _matchCredentialRequest(ecr, cr))))) {
|
|
477
|
+
// FIXME: improve error
|
|
478
|
+
throw new Error('unexpected credential request');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function _matchCredentialRequest(a, b) {
|
|
483
|
+
if(a.format !== b.format) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
const {credential_definition: {'@context': c1, type: t1}} = a;
|
|
487
|
+
const {credential_definition: {'@context': c2, type: t2}} = b;
|
|
488
|
+
// contexts must match exact order but types can have different order
|
|
489
|
+
return (c1.length === c2.length && t1.length === t2.length &&
|
|
490
|
+
c1.every((c, i) => c === c2[i]) && t1.every(t => t2.some(x => t === x)));
|
|
491
|
+
}
|
package/lib/verify.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved.
|
|
2
|
+
* Copyright (c) 2022-2023 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import {importJWK, jwtVerify} from 'jose';
|
|
@@ -60,7 +60,7 @@ export async function verify({
|
|
|
60
60
|
|
|
61
61
|
export async function verifyDidProofJwt({exchanger, exchange, jwt} = {}) {
|
|
62
62
|
// optional oauth2 options
|
|
63
|
-
const {oauth2} = exchange.
|
|
63
|
+
const {oauth2} = exchange.openId;
|
|
64
64
|
const {maxClockSkew} = oauth2;
|
|
65
65
|
|
|
66
66
|
// audience is always the `exchangeId` and cannot be configured; this
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock/vc-delivery",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bedrock Verifiable Credential Delivery",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
"lib": "./lib"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"eslint": "^8.
|
|
57
|
-
"eslint-config-digitalbazaar": "^
|
|
58
|
-
"eslint-plugin-jsdoc": "^
|
|
59
|
-
"eslint-plugin-unicorn": "^
|
|
56
|
+
"eslint": "^8.41.0",
|
|
57
|
+
"eslint-config-digitalbazaar": "^5.0.1",
|
|
58
|
+
"eslint-plugin-jsdoc": "^45.0.0",
|
|
59
|
+
"eslint-plugin-unicorn": "^47.0.0",
|
|
60
60
|
"jsdoc": "^4.0.2",
|
|
61
61
|
"jsdoc-to-markdown": "^8.0.0"
|
|
62
62
|
},
|