@bedrock/vc-delivery 5.1.0 → 5.3.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/exchanges.js +185 -48
- package/lib/helpers.js +16 -0
- package/lib/http.js +2 -2
- package/lib/oid4/http.js +13 -0
- package/lib/oid4/oid4vci.js +195 -128
- package/lib/oid4/oid4vp.js +101 -77
- package/lib/vcapi.js +176 -146
- package/lib/verify.js +5 -17
- package/package.json +3 -2
package/lib/vcapi.js
CHANGED
|
@@ -90,179 +90,209 @@ export async function createExchange({workflow, exchange}) {
|
|
|
90
90
|
return exchange;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export async function processExchange({req, res, workflow,
|
|
94
|
-
|
|
95
|
-
let
|
|
93
|
+
export async function processExchange({req, res, workflow, exchangeRecord}) {
|
|
94
|
+
const {exchange, meta} = exchangeRecord;
|
|
95
|
+
let {updated: lastUpdated} = meta;
|
|
96
|
+
try {
|
|
97
|
+
// get any `verifiablePresentation` from the body...
|
|
98
|
+
let receivedPresentation = req?.body?.verifiablePresentation;
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
// process exchange step(s)
|
|
101
|
+
let i = 0;
|
|
102
|
+
let currentStep = exchange.step;
|
|
103
|
+
let step;
|
|
104
|
+
while(true) {
|
|
105
|
+
if(i++ > MAXIMUM_STEPS) {
|
|
106
|
+
throw new BedrockError('Maximum steps exceeded.', {
|
|
107
|
+
name: 'DataError',
|
|
108
|
+
details: {httpStatusCode: 500, public: true}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
108
111
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
// no step present, break out to complete exchange
|
|
113
|
+
if(!currentStep) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// get current step details
|
|
118
|
+
step = workflow.steps[currentStep];
|
|
119
|
+
if(step.stepTemplate) {
|
|
120
|
+
// generate step from the template; assume the template type is
|
|
121
|
+
// `jsonata` per the JSON schema
|
|
122
|
+
step = await evaluateTemplate(
|
|
123
|
+
{workflow, exchange, typedTemplate: step.stepTemplate});
|
|
124
|
+
if(Object.keys(step).length === 0) {
|
|
125
|
+
throw new BedrockError('Empty step detected.', {
|
|
126
|
+
name: 'DataError',
|
|
127
|
+
details: {httpStatusCode: 500, public: true}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
113
131
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// generate step from the template; assume the template type is
|
|
118
|
-
// `jsonata` per the JSON schema
|
|
119
|
-
step = await evaluateTemplate(
|
|
120
|
-
{workflow, exchange, typedTemplate: step.stepTemplate});
|
|
121
|
-
if(Object.keys(step).length === 0) {
|
|
122
|
-
throw new BedrockError('Empty step detected.', {
|
|
132
|
+
// if next step is the same as the current step, throw an error
|
|
133
|
+
if(step.nextStep === currentStep) {
|
|
134
|
+
throw new BedrockError('Cyclical step detected.', {
|
|
123
135
|
name: 'DataError',
|
|
124
136
|
details: {httpStatusCode: 500, public: true}
|
|
125
137
|
});
|
|
126
138
|
}
|
|
127
|
-
}
|
|
128
139
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
135
|
-
}
|
|
140
|
+
// handle VPR: if step requires it, then `verifiablePresentation` must
|
|
141
|
+
// be in the request
|
|
142
|
+
if(step.verifiablePresentationRequest) {
|
|
143
|
+
const {createChallenge} = step;
|
|
144
|
+
const isInitialStep = exchange.step === workflow.initialStep;
|
|
136
145
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
// if no presentation was received in the body...
|
|
147
|
+
if(!receivedPresentation) {
|
|
148
|
+
const verifiablePresentationRequest = klona(
|
|
149
|
+
step.verifiablePresentationRequest);
|
|
150
|
+
if(createChallenge) {
|
|
151
|
+
/* Note: When creating a challenge, the initial step always
|
|
152
|
+
uses the local exchange ID because the initial step itself
|
|
153
|
+
is one-time use. Subsequent steps, which only VC-API (as opposed
|
|
154
|
+
to other protocols) supports creating additional challenges via
|
|
155
|
+
the VC-API verifier API. */
|
|
156
|
+
let challenge;
|
|
157
|
+
if(isInitialStep) {
|
|
158
|
+
challenge = exchange.id;
|
|
159
|
+
} else {
|
|
160
|
+
// generate a new challenge using verifier API
|
|
161
|
+
({challenge} = await _createChallenge({workflow}));
|
|
162
|
+
}
|
|
163
|
+
verifiablePresentationRequest.challenge = challenge;
|
|
164
|
+
}
|
|
165
|
+
// send VPR and return
|
|
166
|
+
res.json({verifiablePresentationRequest});
|
|
167
|
+
// if exchange is pending, mark it as active out-of-band
|
|
168
|
+
if(exchange.state === 'pending') {
|
|
169
|
+
exchange.state = 'active';
|
|
170
|
+
exchange.sequence++;
|
|
171
|
+
exchanges.update({workflowId: workflow.id, exchange}).catch(
|
|
172
|
+
error => logger.error(
|
|
173
|
+
'Could not mark exchange active: ' + error.message, {error}));
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
142
177
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
to other protocols) supports creating additional challenges via
|
|
152
|
-
the VC-API verifier API. */
|
|
153
|
-
let challenge;
|
|
154
|
-
if(isInitialStep) {
|
|
155
|
-
challenge = exchange.id;
|
|
178
|
+
const {presentationSchema} = step;
|
|
179
|
+
if(presentationSchema) {
|
|
180
|
+
// if the VP is enveloped, get the presentation from the envelope
|
|
181
|
+
let presentation;
|
|
182
|
+
if(receivedPresentation?.type === 'EnvelopedVerifiablePresentation') {
|
|
183
|
+
({presentation} = await unenvelopePresentation({
|
|
184
|
+
envelopedPresentation: receivedPresentation
|
|
185
|
+
}));
|
|
156
186
|
} else {
|
|
157
|
-
|
|
158
|
-
({challenge} = await _createChallenge({workflow}));
|
|
187
|
+
presentation = receivedPresentation;
|
|
159
188
|
}
|
|
160
|
-
verifiablePresentationRequest.challenge = challenge;
|
|
161
|
-
}
|
|
162
|
-
// send VPR and return
|
|
163
|
-
res.json({verifiablePresentationRequest});
|
|
164
|
-
// if exchange is pending, mark it as active out-of-band
|
|
165
|
-
if(exchange.state === 'pending') {
|
|
166
|
-
exchange.state = 'active';
|
|
167
|
-
exchange.sequence++;
|
|
168
|
-
exchanges.update({workflowId: workflow.id, exchange}).catch(
|
|
169
|
-
error => logger.error(
|
|
170
|
-
'Could not mark exchange active: ' + error.message, {error}));
|
|
171
|
-
}
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
189
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}));
|
|
183
|
-
} else {
|
|
184
|
-
presentation = receivedPresentation;
|
|
190
|
+
// validate the received VP
|
|
191
|
+
const {jsonSchema: schema} = presentationSchema;
|
|
192
|
+
const validate = compile({schema});
|
|
193
|
+
const {valid, error} = validate(presentation);
|
|
194
|
+
if(!valid) {
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
185
197
|
}
|
|
186
198
|
|
|
187
|
-
//
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const {
|
|
191
|
-
|
|
192
|
-
|
|
199
|
+
// verify the received VP
|
|
200
|
+
const expectedChallenge = isInitialStep ? exchange.id : undefined;
|
|
201
|
+
const {allowUnprotectedPresentation = false} = step;
|
|
202
|
+
const {verificationMethod} = await verify({
|
|
203
|
+
workflow,
|
|
204
|
+
verifiablePresentationRequest: step.verifiablePresentationRequest,
|
|
205
|
+
presentation: receivedPresentation,
|
|
206
|
+
allowUnprotectedPresentation,
|
|
207
|
+
expectedChallenge
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// store VP results in variables associated with current step
|
|
211
|
+
if(!exchange.variables.results) {
|
|
212
|
+
exchange.variables.results = {};
|
|
193
213
|
}
|
|
194
|
-
|
|
214
|
+
exchange.variables.results[currentStep] = {
|
|
215
|
+
// common use case of DID Authentication; provide `did` for ease
|
|
216
|
+
// of use in templates and consistency with OID4VCI which only
|
|
217
|
+
// receives `did` not verification method nor VP
|
|
218
|
+
did: verificationMethod?.controller || null,
|
|
219
|
+
verificationMethod,
|
|
220
|
+
verifiablePresentation: receivedPresentation
|
|
221
|
+
};
|
|
195
222
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const {allowUnprotectedPresentation = false} = step;
|
|
199
|
-
const {verificationMethod} = await verify({
|
|
200
|
-
workflow,
|
|
201
|
-
verifiablePresentationRequest: step.verifiablePresentationRequest,
|
|
202
|
-
presentation: receivedPresentation,
|
|
203
|
-
allowUnprotectedPresentation,
|
|
204
|
-
expectedChallenge
|
|
205
|
-
});
|
|
223
|
+
// clear received presentation as it has been processed
|
|
224
|
+
receivedPresentation = null;
|
|
206
225
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
exchange.variables.results[currentStep] = {
|
|
212
|
-
// common use case of DID Authentication; provide `did` for ease
|
|
213
|
-
// of use in templates and consistency with OID4VCI which only
|
|
214
|
-
// receives `did` not verification method nor VP
|
|
215
|
-
did: verificationMethod?.controller || null,
|
|
216
|
-
verificationMethod,
|
|
217
|
-
verifiablePresentation: receivedPresentation
|
|
218
|
-
};
|
|
226
|
+
// if there is no next step, break out to complete exchange
|
|
227
|
+
if(!step.nextStep) {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
219
230
|
|
|
220
|
-
|
|
221
|
-
|
|
231
|
+
// update the exchange to go to the next step, then loop to send
|
|
232
|
+
// next VPR
|
|
233
|
+
currentStep = exchange.step = step.nextStep;
|
|
234
|
+
// ensure exchange state is active
|
|
235
|
+
if(exchange.state === 'pending') {
|
|
236
|
+
exchange.state = 'active';
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
exchange.sequence++;
|
|
240
|
+
await exchanges.update({workflowId: workflow.id, exchange});
|
|
241
|
+
lastUpdated = Date.now();
|
|
242
|
+
} catch(e) {
|
|
243
|
+
exchange.sequence--;
|
|
244
|
+
throw e;
|
|
245
|
+
}
|
|
222
246
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
247
|
+
// FIXME: there may be VCs to issue during this step, do so before
|
|
248
|
+
// sending the VPR above
|
|
249
|
+
} else if(step.nextStep) {
|
|
250
|
+
// next steps without VPRs are prohibited
|
|
251
|
+
throw new BedrockError(
|
|
252
|
+
'Invalid step detected; continuing exchanges must include VPRs.', {
|
|
253
|
+
name: 'DataError',
|
|
254
|
+
details: {httpStatusCode: 500, public: true}
|
|
255
|
+
});
|
|
226
256
|
}
|
|
257
|
+
}
|
|
227
258
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// ensure exchange state is active
|
|
232
|
-
if(exchange.state === 'pending') {
|
|
233
|
-
exchange.state = 'active';
|
|
234
|
-
}
|
|
259
|
+
// mark exchange complete
|
|
260
|
+
exchange.state = 'complete';
|
|
261
|
+
try {
|
|
235
262
|
exchange.sequence++;
|
|
236
|
-
await exchanges.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
} else if(step.nextStep) {
|
|
241
|
-
// next steps without VPRs are prohibited
|
|
242
|
-
throw new BedrockError(
|
|
243
|
-
'Invalid step detected; continuing exchanges must include VPRs.', {
|
|
244
|
-
name: 'DataError',
|
|
245
|
-
details: {httpStatusCode: 500, public: true}
|
|
246
|
-
});
|
|
263
|
+
await exchanges.complete({workflowId: workflow.id, exchange});
|
|
264
|
+
} catch(e) {
|
|
265
|
+
exchange.sequence--;
|
|
266
|
+
throw e;
|
|
247
267
|
}
|
|
248
|
-
|
|
268
|
+
lastUpdated = Date.now();
|
|
249
269
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
await exchanges.complete({workflowId: workflow.id, exchange});
|
|
270
|
+
// FIXME: decide what the best recovery path is if delivery fails (but no
|
|
271
|
+
// replay attack detected) after exchange has been marked complete
|
|
253
272
|
|
|
254
|
-
|
|
255
|
-
|
|
273
|
+
// issue any VCs; may return an empty response if the step defines no
|
|
274
|
+
// VCs to issue
|
|
275
|
+
const {response} = await issue({workflow, exchange});
|
|
256
276
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
277
|
+
// if last `step` has a redirect URL, include it in the response
|
|
278
|
+
if(step?.redirectUrl) {
|
|
279
|
+
response.redirectUrl = step.redirectUrl;
|
|
280
|
+
}
|
|
260
281
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
282
|
+
// send response
|
|
283
|
+
res.json(response);
|
|
284
|
+
} catch(e) {
|
|
285
|
+
if(e.name === 'InvalidStateError') {
|
|
286
|
+
throw e;
|
|
287
|
+
}
|
|
288
|
+
// write last error if exchange hasn't been frequently updated
|
|
289
|
+
const {id: workflowId} = workflow;
|
|
290
|
+
const copy = {...exchange};
|
|
291
|
+
copy.sequence++;
|
|
292
|
+
copy.lastError = e;
|
|
293
|
+
exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
|
|
294
|
+
.catch(error => logger.error(
|
|
295
|
+
'Could not set last exchange error: ' + error.message, {error}));
|
|
296
|
+
throw e;
|
|
264
297
|
}
|
|
265
|
-
|
|
266
|
-
// send response
|
|
267
|
-
res.json(response);
|
|
268
298
|
}
|
package/lib/verify.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';
|
|
6
6
|
import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey';
|
|
7
|
+
import {getZcapClient, stripStacktrace} from './helpers.js';
|
|
7
8
|
import {importJWK, jwtVerify} from 'jose';
|
|
8
9
|
import {didIo} from '@bedrock/did-io';
|
|
9
|
-
import {getZcapClient} from './helpers.js';
|
|
10
10
|
|
|
11
11
|
const {util: {BedrockError}} = bedrock;
|
|
12
12
|
|
|
@@ -77,17 +77,17 @@ export async function verify({
|
|
|
77
77
|
if(credentialResults) {
|
|
78
78
|
credentialResults.forEach(result => {
|
|
79
79
|
if(result.error) {
|
|
80
|
-
result.error =
|
|
80
|
+
result.error = stripStacktrace(result.error);
|
|
81
81
|
}
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
if(presentationResult.error) {
|
|
85
|
-
presentationResult.error =
|
|
85
|
+
presentationResult.error = stripStacktrace(presentationResult.error);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// generate useful error to return to client
|
|
89
89
|
const {name, errors, message} = cause.data.error;
|
|
90
|
-
const causeError =
|
|
90
|
+
const causeError = stripStacktrace({...cause.data.error});
|
|
91
91
|
delete causeError.errors;
|
|
92
92
|
const error = new BedrockError(message ?? 'Verification error.', {
|
|
93
93
|
name: (name === 'VerificationError' || name === 'DataError') ?
|
|
@@ -102,7 +102,7 @@ export async function verify({
|
|
|
102
102
|
}
|
|
103
103
|
});
|
|
104
104
|
if(Array.isArray(errors)) {
|
|
105
|
-
error.details.errors = errors.map(
|
|
105
|
+
error.details.errors = errors.map(stripStacktrace);
|
|
106
106
|
}
|
|
107
107
|
throw error;
|
|
108
108
|
}
|
|
@@ -251,15 +251,3 @@ export async function verifyDidProofJwt({workflow, exchange, jwt} = {}) {
|
|
|
251
251
|
|
|
252
252
|
return {verified: true, did: issuer, verifyResult};
|
|
253
253
|
}
|
|
254
|
-
|
|
255
|
-
function _stripStacktrace(error) {
|
|
256
|
-
error = {...error};
|
|
257
|
-
delete error.stack;
|
|
258
|
-
if(error.errors) {
|
|
259
|
-
error.errors = error.errors.map(_stripStacktrace);
|
|
260
|
-
}
|
|
261
|
-
if(error.cause) {
|
|
262
|
-
error.cause = _stripStacktrace(error.cause);
|
|
263
|
-
}
|
|
264
|
-
return error;
|
|
265
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock/vc-delivery",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bedrock Verifiable Credential Delivery",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"cors": "^2.8.5",
|
|
48
48
|
"jose": "^5.6.3",
|
|
49
49
|
"jsonata": "^2.0.5",
|
|
50
|
-
"klona": "^2.0.6"
|
|
50
|
+
"klona": "^2.0.6",
|
|
51
|
+
"serialize-error": "^11.0.3"
|
|
51
52
|
},
|
|
52
53
|
"peerDependencies": {
|
|
53
54
|
"@bedrock/app-identity": "4.0.0",
|