-
Notifications
You must be signed in to change notification settings - Fork 41
/
ProofSet.js
363 lines (330 loc) · 12.2 KB
/
ProofSet.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
/*!
* Copyright (c) 2018 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';
const constants = require('./constants');
const jsonld = require('jsonld');
const {extendContextLoader, strictDocumentLoader} = require('./documentLoader');
const {serializeError} = require('serialize-error');
const strictExpansionMap = require('./expansionMap');
module.exports = class ProofSet {
/**
* Adds a Linked Data proof to a document. If the document contains other
* proofs, the new proof will be appended to the existing set of proofs.
*
* Important note: This method assumes that the term `proof` in the given
* document has the same definition as the `https://w3id.org/security/v2`
* JSON-LD @context.
*
* @param document {object} - JSON-LD Document to be signed.
* @param options {object} Options hashmap.
*
* A `suite` option is required:
*
* @param options.suite {LinkedDataSignature} a signature suite instance
* that will create the proof.
*
* A `purpose` option is required:
*
* @param options.purpose {ProofPurpose} a proof purpose instance that will
* augment the proof with information describing its intended purpose.
*
* Advanced optional parameters and overrides:
*
* @param [documentLoader] {function} a custom document loader,
* `Promise<RemoteDocument> documentLoader(url)`.
* @param [expansionMap] {function} A custom expansion map that is
* passed to the JSON-LD processor; by default a function that will throw
* an error when unmapped properties are detected in the input, use `false`
* to turn this off and allow unmapped properties to be dropped or use a
* custom function.
*
* @return {Promise<object>} resolves with the signed document, with
* the signature in the top-level `proof` property.
*/
async add(document, {suite, purpose, documentLoader, expansionMap} = {}) {
if(!suite) {
throw new TypeError('"options.suite" is required.');
}
if(!purpose) {
throw new TypeError('"options.purpose" is required.');
}
if(documentLoader) {
documentLoader = extendContextLoader(documentLoader);
} else {
documentLoader = strictDocumentLoader;
}
if(expansionMap !== false) {
expansionMap = strictExpansionMap;
}
// preprocess document to prepare to remove existing proofs
// let input;
// shallow copy document to allow removal of existing proofs
const input = {...document};
delete input.proof;
// create the new proof (suites MUST output a proof using the security-v2
// `@context`)
const proof = await suite.createProof({
document: input, purpose, documentLoader, expansionMap
});
jsonld.addValue(document, 'proof', proof);
return document;
}
/**
* Verifies Linked Data proof(s) on a document. The proofs to be verified
* must match the given proof purpose.
*
* Important note: This method assumes that the term `proof` in the given
* document has the same definition as the `https://w3id.org/security/v2`
* JSON-LD @context.
*
* @param {object} document - The JSON-LD document with one or more proofs to
* be verified.
*
* @param {LinkedDataSignature|LinkedDataSignature[]} suite -
* Acceptable signature suite instances for verifying the proof(s).
*
* @param {ProofPurpose} purpose - A proof purpose instance that will
* match proofs to be verified and ensure they were created according to
* the appropriate purpose.
*
* Advanced optional parameters and overrides:
*
* @param {function} [documentLoader] a custom document loader,
* `Promise<RemoteDocument> documentLoader(url)`.
* @param {function} [expansionMap] - A custom expansion map that is
* passed to the JSON-LD processor; by default a function that will throw
* an error when unmapped properties are detected in the input, use `false`
* to turn this off and allow unmapped properties to be dropped or use a
* custom function.
*
* @return {Promise<{verified: boolean, results: Array, error: *}>} resolves
* with an object with a `verified`boolean property that is `true` if at
* least one proof matching the given purpose and suite verifies and `false`
* otherwise; a `results` property with an array of detailed results;
* if `false` an `error` property will be present.
*/
async verify(document, {suite, purpose, documentLoader, expansionMap} = {}) {
if(!suite) {
throw new TypeError('"options.suite" is required.');
}
if(!purpose) {
throw new TypeError('"options.purpose" is required.');
}
const suites = Array.isArray(suite) ? suite : [suite];
if(suites.length === 0) {
throw new TypeError('At least one suite is required.');
}
if(documentLoader) {
documentLoader = extendContextLoader(documentLoader);
} else {
documentLoader = strictDocumentLoader;
}
if(expansionMap !== false) {
expansionMap = strictExpansionMap;
}
try {
// shallow copy to allow for removal of proof set prior to canonize
document = {...document};
// get proofs from document
const {proofSet, document: doc} = await _getProofs({
document, documentLoader, expansionMap
});
document = doc;
// verify proofs
const results = await _verify({
document, suites, proofSet, purpose, documentLoader, expansionMap
});
if(results.length === 0) {
const error = new Error(
'Did not verify any proofs; insufficient proofs matched the ' +
'acceptable suite(s) and required purpose(s).');
error.name = 'NotFoundError';
throw error;
}
// combine results
const verified = results.some(r => r.verified);
if(!verified) {
const errors = [].concat(
...results.filter(r => r.error).map(r => r.error));
const result = {verified, results};
if(errors.length > 0) {
result.error = errors;
}
return result;
}
return {verified, results};
} catch(error) {
_makeSerializable(error);
return {verified: false, error};
}
}
};
async function _getProofs({document}) {
// handle document preprocessing to find proofs
let proofSet;
proofSet = jsonld.getValues(document, 'proof');
delete document.proof;
if(proofSet.length === 0) {
// no possible matches
throw new Error('No matching proofs found in the given document.');
}
// shallow copy proofs and add document context or SECURITY_CONTEXT_URL
const context = document['@context'] || constants.SECURITY_CONTEXT_URL;
proofSet = proofSet.map(proof => ({
'@context': context,
...proof
}));
return {proofSet, document};
}
async function _verify({
document, suites, proofSet, purpose, documentLoader, expansionMap
}) {
// map each purpose to at least one proof to verify
const purposes = Array.isArray(purpose) ? purpose : [purpose];
const purposeToProofs = new Map();
const proofToSuite = new Map();
const suiteMatchQueue = new Map();
await Promise.all(purposes.map(purpose => _matchProofSet({
purposeToProofs, proofToSuite, purpose, proofSet, suites,
suiteMatchQueue, document, documentLoader, expansionMap
})));
// every purpose must have at least one matching proof or verify will fail
if(purposeToProofs.size < purposes.length) {
// insufficient proofs to verify, so don't bother verifying any
return [];
}
// verify every proof in `proofToSuite`; these proofs matched a purpose
const verifyResults = new Map();
await Promise.all([...proofToSuite.entries()].map(async ([proof, suite]) => {
let result;
try {
// create backwards-compatible deferred proof purpose to capture
// verification method from old-style suites
let vm;
const purpose = {
async validate(proof, {verificationMethod}) {
vm = verificationMethod;
return {valid: true};
}
};
const {verified, verificationMethod, error} = await suite.verifyProof({
proof, document, purpose, documentLoader, expansionMap
});
if(!vm) {
vm = verificationMethod;
}
result = {proof, verified, verificationMethod: vm, error};
} catch(error) {
result = {proof, verified: false, error};
}
if(result.error) {
// ensure error is serializable
_makeSerializable(result.error);
}
verifyResults.set(proof, result);
}));
// validate proof against each purpose that matched it
await Promise.all([...purposeToProofs.entries()].map(
async ([purpose, proofs]) => {
for(const proof of proofs) {
const result = verifyResults.get(proof);
if(!result.verified) {
// if proof was not verified, so not bother validating purpose
continue;
}
// validate purpose
const {verificationMethod} = result;
const suite = proofToSuite.get(proof);
let purposeResult;
try {
purposeResult = await purpose.validate(proof, {
document, suite, verificationMethod, documentLoader, expansionMap
});
} catch(error) {
purposeResult = {valid: false, error};
}
// add `purposeResult` to verification result regardless of validity
// to ensure that all purposes are represented
if(result.purposeResult) {
if(Array.isArray(result.purposeResult)) {
result.purposeResult.push(purposeResult);
} else {
result.purposeResult = [result.purposeResult, purposeResult];
}
} else {
result.purposeResult = purposeResult;
}
if(!purposeResult.valid) {
// ensure error is serializable
_makeSerializable(purposeResult.error);
// if no top level error set yet, set it
if(!result.error) {
result.verified = false;
result.error = purposeResult.error;
}
}
}
}));
return [...verifyResults.values()];
}
// add a `toJSON` method to an error which allows for errors in validation
// reports to be serialized properly by `JSON.stringify`.
function _makeSerializable(error) {
Object.defineProperty(error, 'toJSON', {
value: function() {
return serializeError(this);
},
configurable: true,
writable: true
});
}
async function _matchProofSet({
purposeToProofs, proofToSuite, purpose, proofSet, suites,
suiteMatchQueue, document, documentLoader, expansionMap
}) {
for(const proof of proofSet) {
// first check if the proof matches the purpose; if it doesn't continue
if(!await purpose.match(proof, {document, documentLoader, expansionMap})) {
continue;
}
// next, find the suite that can verify the proof; if found, `matched`
// will be set to `true` and the proof will be added to `purposeToProofs`
// and `proofToSuite` to be processed -- otherwise it will not be; if
// no proofs are added for a given purpose, an exception will be thrown
let matched = false;
for(const s of suites) {
// `matchingProofs` is a map of promises that resolve to whether a
// proof matches a suite; multiple purposes and suites may be checked
// in parallel so a promise queue is used to prevent duplicate work
let matchingProofs = suiteMatchQueue.get(s);
if(!matchingProofs) {
suiteMatchQueue.set(s, matchingProofs = new Map());
}
let promise = matchingProofs.get(proof);
if(!promise) {
promise = s.matchProof({proof, document, documentLoader, expansionMap});
matchingProofs.set(proof, promise);
}
if(await promise) {
// found the matching suite for the proof; there should only be one
// suite that can verify a particular proof; add the proof to the
// map of proofs to be verified along with the matching suite
matched = true;
proofToSuite.set(proof, s);
break;
}
}
if(matched) {
// note proof was a match for the purpose and an acceptable suite; it
// will need to be verified by the suite and then validated against the
// purpose
const matches = purposeToProofs.get(purpose);
if(matches) {
matches.push(proof);
} else {
purposeToProofs.set(purpose, [proof]);
}
}
}
}