-
Notifications
You must be signed in to change notification settings - Fork 41
/
Copy pathProofSet.js
371 lines (341 loc) · 13.1 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
364
365
366
367
368
369
370
371
/*!
* 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');
const PublicKeyProofPurpose = require('./purposes/PublicKeyProofPurpose');
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|string} Object to be signed, either a string URL
* (resolved via the given `documentLoader`) or a plain object (JSON-LD
* document).
* @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.
* @param [compactProof] {boolean} `true` instructs this call to compact
* the resulting proof to the same JSON-LD `@context` as the input
* document; this is the default behavior. Setting this flag to `false` can
* be used as an optimization to prevent an unnecessary compaction when the
* caller knows that all used proof terms have the same definition in the
* document's `@context` as the `constants.SECURITY_CONTEXT_URL` `@context`.
*
* @return {Promise<object>} resolves with the signed document, with
* the signature in the top-level `proof` property.
*/
async add(document, {
suite, purpose, documentLoader, expansionMap,
compactProof = true} = {}) {
if(!suite) {
throw new TypeError('"options.suite" is required.');
}
if(!purpose) {
throw new TypeError('"options.purpose" is required.');
}
if(suite.legacy) {
if(!(purpose instanceof PublicKeyProofPurpose)) {
throw new TypeError(
`The "${suite.type}" suite requires "options.purpose" to be ` +
'an instance of "PublicKeyProofPurpose".');
}
}
if(documentLoader) {
documentLoader = extendContextLoader(documentLoader);
} else {
documentLoader = strictDocumentLoader;
}
if(expansionMap !== false) {
expansionMap = strictExpansionMap;
}
if(typeof document === 'string') {
// fetch document
document = await documentLoader(document);
}
// preprocess document to prepare to remove existing proofs
let input;
if(compactProof) {
// cannot assume security context terms, so do full compaction
input = await jsonld.compact(
document, constants.SECURITY_CONTEXT_URL,
{documentLoader, expansionMap, compactToRelative: false});
} else {
// TODO: optimize to modify document in place to maximize optimization
// shallow copy document to allow removal of existing proofs
input = {...document};
}
// save but exclude any existing proof(s)
const proofProperty = suite.legacy ? 'signature' : 'proof';
//const existingProofs = input[proofProperty];
delete input[proofProperty];
// create the new proof (suites MUST output a proof using the security-v2
// `@context`)
const proof = await suite.createProof({
document: input, purpose, documentLoader,
expansionMap, compactProof});
if(compactProof) {
// compact proof to match document's context
let expandedProof;
if(suite.legacy) {
expandedProof = {
[constants.SECURITY_SIGNATURE_URL]: proof
};
} else {
expandedProof = {
[constants.SECURITY_PROOF_URL]: {'@graph': proof}
};
}
// account for type-scoped `proof` definition by getting document types
const {types, alias} = await _getTypeInfo(
{document, documentLoader, expansionMap});
expandedProof['@type'] = types;
const ctx = jsonld.getValues(document, '@context');
const compactProof = await jsonld.compact(
expandedProof, ctx,
{documentLoader, expansionMap, compactToRelative: false});
delete compactProof[alias];
delete compactProof['@context'];
// add proof to document
const key = Object.keys(compactProof)[0];
jsonld.addValue(document, key, compactProof[key]);
} else {
// in-place restore any existing proofs
/*if(existingProofs) {
document[proofProperty] = existingProofs;
}*/
// add new proof
delete proof['@context'];
jsonld.addValue(document, proofProperty, proof);
}
return document;
}
/**
* Verify 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 document {object|string} Object with one or more proofs to be
* verified, either a string URL (resolved to an object via the given
* `documentLoader`) or a plain object (JSON-LD document).
* @param options {object} Options hashmap.
*
* A `suite` option is required:
*
* @param options.suite {LinkedDataSignature or Array of LinkedDataSignature}
* acceptable signature suite instances for verifying the proof(s).
*
* A `purpose` option is required:
*
* @param options.purpose {ProofPurpose} 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 [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.
* @param [compactProof] {boolean} `true` indicates that this method cannot
* assume that the incoming document has defined all proof terms in the
* same way as the `constants.SECURITY_CONTEXT_URL` JSON-LD `@context`.
* This means that this method must compact any found proofs to this
* context for internal and extension processing; this is the default
* behavior. To override this behavior and optimize away this step because
* the caller knows that the input document's JSON-LD `@context` defines
* the proof terms in the same way, set this flag to `false`.
*
* @return {Promise<object>} 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,
compactProof = true} = {}) {
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.');
}
const legacy = suites.some(s => s.legacy);
if(legacy) {
if(suites.some(s => !s.legacy)) {
throw new Error(
'Legacy suites may not be combined with current suites.');
} else if(!(purpose instanceof PublicKeyProofPurpose)) {
throw new TypeError(
'"options.purpose" must be an instance of "PublicKeyProofPurpose"' +
'to use a legacy suite.');
}
}
if(documentLoader) {
documentLoader = extendContextLoader(documentLoader);
} else {
documentLoader = strictDocumentLoader;
}
if(expansionMap !== false) {
expansionMap = strictExpansionMap;
}
try {
if(typeof document === 'string') {
// fetch document
document = await documentLoader(document);
} else {
// TODO: consider in-place editing to optimize when `compactProof`
// is `false`
// 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, legacy, documentLoader, expansionMap, compactProof});
document = doc;
// verify proofs
const results = await _verify({
document, suites, proofSet,
purpose, documentLoader, expansionMap, compactProof});
if(results.length === 0) {
throw new Error(
'Could not verify any proofs; no proofs matched the required ' +
'suite and purpose.');
}
// 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) {
_addToJSON(error);
return {verified: false, error};
}
}
};
async function _getProofs({
document, legacy, documentLoader, expansionMap, compactProof}) {
// handle document preprocessing to find proofs
const proofProperty = legacy ? 'signature' : 'proof';
let proofSet;
if(compactProof) {
// if we must compact the proof(s) then we must first compact the input
// document to find the proof(s)
document = await jsonld.compact(
document, constants.SECURITY_CONTEXT_URL,
{documentLoader, expansionMap, compactToRelative: false});
}
proofSet = jsonld.getValues(document, proofProperty);
delete document[proofProperty];
if(proofSet.length === 0) {
// no possible matches
throw new Error('No matching proofs found in the given document.');
}
// TODO: consider in-place editing to optimize
// shallow copy proofs and add SECURITY_CONTEXT_URL
proofSet = proofSet.map(proof => ({
'@context': constants.SECURITY_CONTEXT_URL,
...proof
}));
return {proofSet, document};
}
async function _verify({
document, suites, proofSet, purpose,
documentLoader, expansionMap, compactProof}) {
// filter out matching proofs
const result = await Promise.all(proofSet.map(proof =>
purpose.match(proof, {document, documentLoader, expansionMap})));
const matches = proofSet.filter((value, index) => result[index]);
if(matches.length === 0) {
// no matches, nothing to verify
return [];
}
// verify each matching proof
return (await Promise.all(matches.map(async proof => {
for(const s of suites) {
if(await s.matchProof({proof, document, documentLoader, expansionMap})) {
return s.verifyProof({
proof, document, purpose, documentLoader, expansionMap,
compactProof}).catch(error => ({verified: false, error}));
}
}
}))).map((r, i) => {
if(!r) {
return null;
}
if(r.error) {
_addToJSON(r.error);
}
return {proof: matches[i], ...r};
}).filter(r => r);
}
// add a `toJSON` method to an error which allows for errors in validation
// reports to be serialized properly by `JSON.stringify`.
function _addToJSON(error) {
Object.defineProperty(error, 'toJSON', {
value: function() {
return serializeError(this);
},
configurable: true,
writable: true
});
}
async function _getTypeInfo({document, documentLoader, expansionMap}) {
// determine `@type` alias, if any
const ctx = jsonld.getValues(document, '@context');
const compacted = await jsonld.compact(
{'@type': '_:b0'}, ctx, {documentLoader, expansionMap});
delete compacted['@context'];
const alias = Object.keys(compacted)[0];
// optimize: expand only `@type` and `type` values
const toExpand = {'@context': ctx};
toExpand['@type'] = jsonld.getValues(document, '@type')
.concat(jsonld.getValues(document, alias));
const expanded = (await jsonld.expand(
toExpand, {documentLoader, expansionMap}))[0] || {};
return {types: jsonld.getValues(expanded, '@type'), alias};
}