@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 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
- const step = exchanger.steps[currentStep];
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, presentation: receivedPresentation, expectedChallenge
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
- const {verifiablePresentation} = await issue({exchanger, exchange});
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 VP
264
- res.json({verifiablePresentation});
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
- const {variables = {}} = exchange;
15
- // run jsonata compiler; only `jsonata` template type is supported and this
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 = new URL(exchanger.id).origin;
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 ?? presentation?.proof?.challenge,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "3.4.1",
3
+ "version": "3.5.1",
4
4
  "type": "module",
5
5
  "description": "Bedrock Verifiable Credential Delivery",
6
6
  "main": "./lib/index.js",
@@ -109,8 +109,8 @@ export const createExchangeBody = {
109
109
  }
110
110
  };
111
111
 
112
- const credentialTemplate = {
113
- title: 'Credential Template',
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: credentialTemplate
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
- // FIXME: add jsonata template to convert VPR or
179
- // `jwtDidProofRequest` to more variables to be
180
- // used when issuing VCs
197
+ },
198
+ stepTemplate: typedTemplate
181
199
  }
182
200
  };
183
201