@bedrock/vc-delivery 3.4.1 → 3.5.1
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 +10 -1
- package/lib/http.js +50 -9
- package/lib/issue.js +12 -12
- package/lib/verify.js +8 -3
- package/package.json +1 -1
- package/schemas/bedrock-vc-exchanger.js +25 -7
package/lib/helpers.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
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 {decodeId, generateId} from 'bnid';
|
|
6
6
|
import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
|
|
7
7
|
import {httpsAgent} from '@bedrock/https-agent';
|
|
8
|
+
import jsonata from 'jsonata';
|
|
8
9
|
import {serviceAgents} from '@bedrock/service-agent';
|
|
9
10
|
import {ZcapClient} from '@digitalbazaar/ezcap';
|
|
10
11
|
|
|
11
12
|
const {config} = bedrock;
|
|
12
13
|
|
|
14
|
+
export async function evaluateTemplate({exchange, typedTemplate} = {}) {
|
|
15
|
+
// run jsonata compiler; only `jsonata` template type is supported and this
|
|
16
|
+
// assumes only this template type will be passed in
|
|
17
|
+
const {template} = typedTemplate;
|
|
18
|
+
const {variables = {}} = exchange;
|
|
19
|
+
return jsonata(template).evaluate(variables, variables);
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
export function getExchangerId({routePrefix, localId} = {}) {
|
|
14
23
|
return `${config.server.baseUri}${routePrefix}/${localId}`;
|
|
15
24
|
}
|
package/lib/http.js
CHANGED
|
@@ -8,12 +8,12 @@ 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 {evaluateTemplate, generateRandom} from './helpers.js';
|
|
11
12
|
import {exportJWK, generateKeyPair, importJWK} from 'jose';
|
|
12
13
|
import {metering, middleware} from '@bedrock/service-core';
|
|
13
14
|
import {asyncHandler} from '@bedrock/express';
|
|
14
15
|
import bodyParser from 'body-parser';
|
|
15
16
|
import cors from 'cors';
|
|
16
|
-
import {generateRandom} from './helpers.js';
|
|
17
17
|
import {issue} from './issue.js';
|
|
18
18
|
import {klona} from 'klona';
|
|
19
19
|
import {logger} from './logger.js';
|
|
@@ -31,6 +31,8 @@ bedrock.events.on('bedrock-express.configure.bodyParser', app => {
|
|
|
31
31
|
}));
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
const MAXIMUM_STEPS = 100;
|
|
35
|
+
|
|
34
36
|
export async function addRoutes({app, service} = {}) {
|
|
35
37
|
const {routePrefix} = service;
|
|
36
38
|
|
|
@@ -170,15 +172,43 @@ export async function addRoutes({app, service} = {}) {
|
|
|
170
172
|
let receivedPresentation = req?.body?.verifiablePresentation;
|
|
171
173
|
|
|
172
174
|
// process exchange step(s)
|
|
175
|
+
let i = 0;
|
|
173
176
|
let currentStep = exchange.step;
|
|
174
177
|
while(true) {
|
|
178
|
+
if(i++ > MAXIMUM_STEPS) {
|
|
179
|
+
throw new BedrockError('Maximum steps exceeded.', {
|
|
180
|
+
name: 'DataError',
|
|
181
|
+
details: {httpStatusCode: 500, public: true}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
175
185
|
// no step present, break out to complete exchange
|
|
176
186
|
if(!currentStep) {
|
|
177
187
|
break;
|
|
178
188
|
}
|
|
179
189
|
|
|
180
190
|
// get current step details
|
|
181
|
-
|
|
191
|
+
let step = exchanger.steps[currentStep];
|
|
192
|
+
if(step.stepTemplate) {
|
|
193
|
+
// generate step from the template; assume the template type is
|
|
194
|
+
// `jsonata` per the JSON schema
|
|
195
|
+
step = await evaluateTemplate(
|
|
196
|
+
{exchange, typedTemplate: step.stepTemplate});
|
|
197
|
+
if(Object.keys(step).length === 0) {
|
|
198
|
+
throw new BedrockError('Empty step detected.', {
|
|
199
|
+
name: 'DataError',
|
|
200
|
+
details: {httpStatusCode: 500, public: true}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// if next step is the same as the current step, throw an error
|
|
206
|
+
if(step.nextStep === currentStep) {
|
|
207
|
+
throw new BedrockError('Cyclical step detected.', {
|
|
208
|
+
name: 'DataError',
|
|
209
|
+
details: {httpStatusCode: 500, public: true}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
182
212
|
|
|
183
213
|
// handle VPR: if step requires it, then `verifiablePresentation` must
|
|
184
214
|
// be in the request
|
|
@@ -213,11 +243,11 @@ export async function addRoutes({app, service} = {}) {
|
|
|
213
243
|
// verify the received VP
|
|
214
244
|
const expectedChallenge = isInitialStep ? exchange.id : undefined;
|
|
215
245
|
const {verificationMethod} = await verify({
|
|
216
|
-
exchanger,
|
|
246
|
+
exchanger,
|
|
247
|
+
verifiablePresentationRequest: step.verifiablePresentationRequest,
|
|
248
|
+
presentation: receivedPresentation, expectedChallenge
|
|
217
249
|
});
|
|
218
250
|
|
|
219
|
-
// FIXME: ensure VP satisfies step VPR
|
|
220
|
-
|
|
221
251
|
// store VP results in variables associated with current step
|
|
222
252
|
if(!exchange.variables.results) {
|
|
223
253
|
exchange.variables.results = {};
|
|
@@ -246,6 +276,16 @@ export async function addRoutes({app, service} = {}) {
|
|
|
246
276
|
exchangerId: exchanger.id, exchange, expectedStep: currentStep
|
|
247
277
|
});
|
|
248
278
|
currentStep = exchange.step;
|
|
279
|
+
|
|
280
|
+
// FIXME: there may be VCs to issue during this step, do so before
|
|
281
|
+
// sending the VPR above
|
|
282
|
+
} else if(step.nextStep) {
|
|
283
|
+
// next steps without VPRs are prohibited
|
|
284
|
+
throw new BedrockError(
|
|
285
|
+
'Invalid step detected; continuing exchanges must include VPRs.', {
|
|
286
|
+
name: 'DataError',
|
|
287
|
+
details: {httpStatusCode: 500, public: true}
|
|
288
|
+
});
|
|
249
289
|
}
|
|
250
290
|
}
|
|
251
291
|
|
|
@@ -257,11 +297,12 @@ export async function addRoutes({app, service} = {}) {
|
|
|
257
297
|
// FIXME: decide what the best recovery path is if delivery fails (but no
|
|
258
298
|
// replay attack detected) after exchange has been marked complete
|
|
259
299
|
|
|
260
|
-
// issue VCs
|
|
261
|
-
|
|
300
|
+
// issue any VCs; may return an empty result if the step defines no
|
|
301
|
+
// VCs to issue
|
|
302
|
+
const result = await issue({exchanger, exchange});
|
|
262
303
|
|
|
263
|
-
// send
|
|
264
|
-
res.json(
|
|
304
|
+
// send result
|
|
305
|
+
res.json(result);
|
|
265
306
|
}));
|
|
266
307
|
|
|
267
308
|
// create OID4VCI routes to be used with each individual exchange
|
package/lib/issue.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
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
|
+
import {evaluateTemplate, getZcapClient} from './helpers.js';
|
|
4
5
|
import {createPresentation} from '@digitalbazaar/vc';
|
|
5
|
-
import {getZcapClient} from './helpers.js';
|
|
6
|
-
import jsonata from 'jsonata';
|
|
7
6
|
|
|
8
7
|
export async function issue({exchanger, exchange} = {}) {
|
|
9
8
|
// use any templates from exchanger and variables from exchange to produce
|
|
10
9
|
// credentials to be issued; issue via the configured issuer instance
|
|
11
10
|
const verifiableCredential = [];
|
|
12
11
|
const {credentialTemplates = []} = exchanger;
|
|
13
|
-
if(credentialTemplates) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// was validated when the exchanger was created
|
|
17
|
-
const credentials = await Promise.all(credentialTemplates.map(
|
|
18
|
-
({template: t}) => jsonata(t).evaluate(variables, variables)));
|
|
19
|
-
// issue all VCs
|
|
20
|
-
const vcs = await _issue({exchanger, credentials});
|
|
21
|
-
verifiableCredential.push(...vcs);
|
|
12
|
+
if(!credentialTemplates || credentialTemplates.length === 0) {
|
|
13
|
+
// nothing to issue
|
|
14
|
+
return {};
|
|
22
15
|
}
|
|
23
16
|
|
|
17
|
+
// evaluate template
|
|
18
|
+
const credentials = await Promise.all(credentialTemplates.map(
|
|
19
|
+
typedTemplate => evaluateTemplate({exchange, typedTemplate})));
|
|
20
|
+
// issue all VCs
|
|
21
|
+
const vcs = await _issue({exchanger, credentials});
|
|
22
|
+
verifiableCredential.push(...vcs);
|
|
23
|
+
|
|
24
24
|
// generate VP to return VCs
|
|
25
25
|
const verifiablePresentation = createPresentation();
|
|
26
26
|
// FIXME: add any encrypted VCs to VP
|
package/lib/verify.js
CHANGED
|
@@ -22,7 +22,7 @@ export async function createChallenge({exchanger} = {}) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export async function verify({
|
|
25
|
-
exchanger, presentation, expectedChallenge
|
|
25
|
+
exchanger, verifiablePresentationRequest, presentation, expectedChallenge
|
|
26
26
|
} = {}) {
|
|
27
27
|
// create zcap client for verifying
|
|
28
28
|
const {zcapClient, zcaps} = await getZcapClient({exchanger});
|
|
@@ -34,13 +34,16 @@ export async function verify({
|
|
|
34
34
|
checks.push('challenge');
|
|
35
35
|
}
|
|
36
36
|
const capability = zcaps.verifyPresentation;
|
|
37
|
-
const domain =
|
|
37
|
+
const domain = verifiablePresentationRequest.domain ??
|
|
38
|
+
new URL(exchanger.id).origin;
|
|
38
39
|
const result = await zcapClient.write({
|
|
39
40
|
capability,
|
|
40
41
|
json: {
|
|
41
42
|
options: {
|
|
42
43
|
// FIXME: support multi-proof presentations?
|
|
43
|
-
challenge: expectedChallenge ??
|
|
44
|
+
challenge: expectedChallenge ??
|
|
45
|
+
verifiablePresentationRequest.challenge ??
|
|
46
|
+
presentation?.proof?.challenge,
|
|
44
47
|
domain,
|
|
45
48
|
checks
|
|
46
49
|
},
|
|
@@ -55,6 +58,8 @@ export async function verify({
|
|
|
55
58
|
}
|
|
56
59
|
} = result;
|
|
57
60
|
|
|
61
|
+
// FIXME: ensure VP satisfies VPR
|
|
62
|
+
|
|
58
63
|
return {verified, challengeUses, verificationMethod};
|
|
59
64
|
}
|
|
60
65
|
|
package/package.json
CHANGED
|
@@ -109,8 +109,8 @@ export const createExchangeBody = {
|
|
|
109
109
|
}
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
const
|
|
113
|
-
title: '
|
|
112
|
+
const typedTemplate = {
|
|
113
|
+
title: 'Typed Template',
|
|
114
114
|
type: 'object',
|
|
115
115
|
required: ['type', 'template'],
|
|
116
116
|
additionalProperties: false,
|
|
@@ -129,13 +129,33 @@ export const credentialTemplates = {
|
|
|
129
129
|
title: 'Credential Templates',
|
|
130
130
|
type: 'array',
|
|
131
131
|
minItems: 1,
|
|
132
|
-
items:
|
|
132
|
+
items: typedTemplate
|
|
133
133
|
};
|
|
134
134
|
|
|
135
135
|
const step = {
|
|
136
136
|
title: 'Exchange Step',
|
|
137
137
|
type: 'object',
|
|
138
|
+
minProperties: 1,
|
|
138
139
|
additionalProperties: false,
|
|
140
|
+
// step can either use a template so it will be generated using variables
|
|
141
|
+
// associated with the exchange, or static values can be provided
|
|
142
|
+
oneOf: [{
|
|
143
|
+
// `stepTemplate` must be present and nothing else
|
|
144
|
+
required: ['stepTemplate'],
|
|
145
|
+
not: {
|
|
146
|
+
required: [
|
|
147
|
+
'createChallenge',
|
|
148
|
+
'verifiablePresentationRequest',
|
|
149
|
+
'jwtDidProofRequest',
|
|
150
|
+
'nextStep'
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
}, {
|
|
154
|
+
// anything except `stepTemplate` can be used
|
|
155
|
+
not: {
|
|
156
|
+
required: ['stepTemplate']
|
|
157
|
+
}
|
|
158
|
+
}],
|
|
139
159
|
properties: {
|
|
140
160
|
createChallenge: {
|
|
141
161
|
type: 'boolean'
|
|
@@ -174,10 +194,8 @@ const step = {
|
|
|
174
194
|
},
|
|
175
195
|
nextStep: {
|
|
176
196
|
type: 'string'
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// `jwtDidProofRequest` to more variables to be
|
|
180
|
-
// used when issuing VCs
|
|
197
|
+
},
|
|
198
|
+
stepTemplate: typedTemplate
|
|
181
199
|
}
|
|
182
200
|
};
|
|
183
201
|
|