Skip to content

Commit 631ac7b

Browse files
committed
Support multiple proof purposes in verify.
- Include cache of document c14n hash to avoid duplicate work when verifying multiple proofs against the same document.
1 parent 3cc5789 commit 631ac7b

File tree

3 files changed

+203
-53
lines changed

3 files changed

+203
-53
lines changed

CHANGELOG.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# jsonld-signatures ChangeLog
22

3+
## 9.2.0 - 2021-xx-xx
4+
5+
### Added
6+
- Support passing multiple purposes in a single verify call.
7+
- Add `NotFoundError` name to error thrown when there are not enough proofs
8+
to match the passed supported suites and purposes during verification.
9+
LD suite implementations should not be relying the error message but can
10+
rely on the `name` property of the error instead.
11+
12+
### Changed
13+
- `LinkedDataSignature` no longer calls `purpose.validate`; this function
14+
is instead called after `verifyProof()`. This removes the responsibility
15+
of calling this function from LD suite implementations and places it in
16+
the main verify call from within jsigs instead. LD suites will still be
17+
passed a dummy `purpose` in this version for backwards compatibility
18+
purposes that will successfully return a promise that resolves to
19+
`true` from `purpose.validate()`. Decoupling this from the suites both
20+
establishes a better separation of concerns and simplifies LD suites by
21+
reducing their responsibilities. LD suites are responsible for returing
22+
the `verificationMethod` used in their results so it can be passed to
23+
`purpose.validate()`.
24+
- Add cache for hash of canonicalized document to enable its reuse when
25+
verifying multiple proofs on a single document.
26+
327
## 9.1.1 - 2021-06-29
428

529
### Fixed
@@ -39,7 +63,7 @@ a patch.
3963
Increase validation on either key or signer/verifier parameters.
4064

4165
### Fixed
42-
- Add missing `signer` and `verifier` parameters to the `LinkedDataSignature`
66+
- Add missing `signer` and `verifier` parameters to the `LinkedDataSignature`
4367
constructor. This issue caused `this.signer` in subclasses to be `undefined`.
4468

4569
## 8.0.2 - 2021-03-19

lib/ProofSet.js

Lines changed: 148 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,14 @@ module.exports = class ProofSet {
146146

147147
// verify proofs
148148
const results = await _verify({
149-
document, suites, proofSet, purpose, documentLoader, expansionMap
149+
document, suites, proofSet, purpose, documentLoader, expansionMap,
150150
});
151151
if(results.length === 0) {
152-
throw new Error(
153-
'Could not verify any proofs; no proofs matched the required ' +
154-
'suite and purpose.');
152+
const error = new Error(
153+
'Did not verify any proofs; insufficient proofs matched the ' +
154+
'acceptable suite(s) and required purpose(s).');
155+
error.name = 'NotFoundError';
156+
throw error;
155157
}
156158

157159
// combine results
@@ -167,7 +169,7 @@ module.exports = class ProofSet {
167169
}
168170
return {verified, results};
169171
} catch(error) {
170-
_addToJSON(error);
172+
_makeSerializable(error);
171173
return {verified: false, error};
172174
}
173175
}
@@ -197,38 +199,108 @@ async function _getProofs({document}) {
197199
async function _verify({
198200
document, suites, proofSet, purpose, documentLoader, expansionMap
199201
}) {
200-
// filter out matching proofs
201-
const result = await Promise.all(proofSet.map(proof =>
202-
purpose.match(proof, {document, documentLoader, expansionMap})));
203-
const matches = proofSet.filter((value, index) => result[index]);
204-
if(matches.length === 0) {
205-
// no matches, nothing to verify
202+
// map each purpose to at least one proof to verify
203+
const purposes = Array.isArray(purpose) ? purpose : [purpose];
204+
const purposeToProofs = new Map();
205+
const proofToSuite = new Map();
206+
const suiteMatchQueue = new Map();
207+
await Promise.all(purposes.map(purpose => _matchProofSet({
208+
purposeToProofs, proofToSuite, purpose, proofSet, suites,
209+
suiteMatchQueue, document, documentLoader, expansionMap
210+
})));
211+
212+
// every purpose must have at least one matching proof or verify will fail
213+
if(purposeToProofs.length < purposes.length) {
214+
// insufficient proofs to verify, so don't bother verifying any
206215
return [];
207216
}
208217

209-
// verify each matching proof
210-
return (await Promise.all(matches.map(async proof => {
211-
for(const s of suites) {
212-
if(await s.matchProof({proof, document, documentLoader, expansionMap})) {
213-
return s.verifyProof({
214-
proof, document, purpose, documentLoader, expansionMap
215-
}).catch(error => ({verified: false, error}));
218+
// verify every proof in `proofToSuite`; these proofs matched a purpose
219+
const verifyResults = new Map();
220+
await Promise.all([...proofToSuite.entries()].map(async ([proof, suite]) => {
221+
let result;
222+
try {
223+
// create backwards-compatible deferred proof purpose to capture
224+
// verification method from old-style suites
225+
let vm;
226+
const purpose = {
227+
async validate(proof, {verificationMethod}) {
228+
vm = verificationMethod;
229+
return {valid: true};
230+
}
231+
};
232+
const {verified, verificationMethod, error} = await suite.verifyProof({
233+
proof, document, purpose, documentLoader, expansionMap
234+
});
235+
if(!vm) {
236+
vm = verificationMethod;
216237
}
238+
result = {proof, verified, verificationMethod: vm, error};
239+
} catch(error) {
240+
result = {proof, verified: false, error};
217241
}
218-
}))).map((r, i) => {
219-
if(!r) {
220-
return null;
221-
}
222-
if(r.error) {
223-
_addToJSON(r.error);
242+
243+
if(result.error) {
244+
// ensure error is serializable
245+
_makeSerializable(result.error);
224246
}
225-
return {proof: matches[i], ...r};
226-
}).filter(r => r);
247+
248+
verifyResults.set(proof, result);
249+
}));
250+
251+
// validate proof against each purpose that matched it
252+
await Promise.all([...purposeToProofs.entries()].map(
253+
async ([purpose, proofs]) => {
254+
for(const proof of proofs) {
255+
const result = verifyResults.get(proof);
256+
if(!result.verified) {
257+
// if proof was not verified, so not bother validating purpose
258+
continue;
259+
}
260+
261+
// validate purpose
262+
const {verificationMethod} = result;
263+
const suite = proofToSuite.get(proof);
264+
let purposeResult;
265+
try {
266+
purposeResult = await purpose.validate(proof, {
267+
document, suite, verificationMethod, documentLoader, expansionMap
268+
});
269+
} catch(error) {
270+
purposeResult = {valid: false, error};
271+
}
272+
273+
// add `purposeResult` to verification result regardless of validity
274+
// to ensure that all purposes are represented
275+
if(result.purposeResult) {
276+
if(Array.isArray(result.purposeResult)) {
277+
result.purposeResult.push(purposeResult);
278+
} else {
279+
result.purposeResult = [result.purposeResult, purposeResult];
280+
}
281+
} else {
282+
result.purposeResult = purposeResult;
283+
}
284+
285+
if(!purposeResult.valid) {
286+
// ensure error is serializable
287+
_makeSerializable(purposeResult.error);
288+
289+
// if no top level error set yet, set it
290+
if(!result.error) {
291+
result.verified = false;
292+
result.error = purposeResult.error;
293+
}
294+
}
295+
}
296+
}));
297+
298+
return [...verifyResults.values()];
227299
}
228300

229301
// add a `toJSON` method to an error which allows for errors in validation
230302
// reports to be serialized properly by `JSON.stringify`.
231-
function _addToJSON(error) {
303+
function _makeSerializable(error) {
232304
Object.defineProperty(error, 'toJSON', {
233305
value: function() {
234306
return serializeError(this);
@@ -237,3 +309,52 @@ function _addToJSON(error) {
237309
writable: true
238310
});
239311
}
312+
313+
async function _matchProofSet({
314+
purposeToProofs, proofToSuite, purpose, proofSet, suites,
315+
suiteMatchQueue, document, documentLoader, expansionMap
316+
}) {
317+
for(const proof of proofSet) {
318+
// first check if the proof matches the purpose; if it doesn't continue
319+
if(!await purpose.match(proof, {document, documentLoader, expansionMap})) {
320+
continue;
321+
}
322+
323+
// next, find the suite that can verify the proof
324+
let matched = false;
325+
for(const s of suites) {
326+
// `matchingProofs` is a map of promises that resolve to whether a
327+
// proof matches a suite; multiple purposes and suites may be checked
328+
// in parallel so a promise queue is used to prevent duplicate work
329+
let matchingProofs = suiteMatchQueue.get(s);
330+
if(!matchingProofs) {
331+
suiteMatchQueue.set(s, matchingProofs = new Map());
332+
}
333+
let promise = matchingProofs.get(proof);
334+
if(!promise) {
335+
promise = s.matchProof({proof, document, documentLoader, expansionMap});
336+
matchingProofs.set(proof, promise);
337+
}
338+
if(await promise) {
339+
// found the matching suite for the proof; there should only be one
340+
// suite that can verify a particular proof; add the proof to the
341+
// map of proofs to be verified along with the matching suite
342+
matched = true;
343+
proofToSuite.set(proof, s);
344+
break;
345+
}
346+
}
347+
348+
if(matched) {
349+
// note proof was a match for the purpose and an acceptable suite; it
350+
// will need to be verified by the suite and then validated against the
351+
// purpose
352+
const matches = purposeToProofs.get(purpose);
353+
if(matches) {
354+
matches.push(proof);
355+
} else {
356+
purposeToProofs.set(purpose, [proof]);
357+
}
358+
}
359+
}
360+
}

lib/suites/LinkedDataSignature.js

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*!
2-
* Copyright (c) 2017-2018 Digital Bazaar, Inc. All rights reserved.
2+
* Copyright (c) 2017-2021 Digital Bazaar, Inc. All rights reserved.
33
*/
44
'use strict';
55

@@ -79,6 +79,7 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
7979
}
8080
}
8181
this.useNativeCanonize = useNativeCanonize;
82+
this._hashCache = null;
8283
}
8384

8485
/**
@@ -160,15 +161,12 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
160161
/**
161162
* @param proof {object} the proof to be verified.
162163
* @param document {object} the document the proof applies to.
163-
* @param purpose {ProofPurpose}
164164
* @param documentLoader {function}
165165
* @param expansionMap {function}
166166
*
167167
* @returns {Promise<{object}>} Resolves with the verification result.
168168
*/
169-
async verifyProof({
170-
proof, document, purpose, documentLoader, expansionMap,
171-
}) {
169+
async verifyProof({proof, document, documentLoader, expansionMap}) {
172170
try {
173171
// create data to verify
174172
const verifyData = await this.createVerifyData(
@@ -186,15 +184,7 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
186184
throw new Error('Invalid signature.');
187185
}
188186

189-
// ensure proof was performed for a valid purpose
190-
const purposeResult = await purpose.validate(
191-
proof, {document, suite: this, verificationMethod,
192-
documentLoader, expansionMap});
193-
if(!purposeResult.valid) {
194-
throw purposeResult.error;
195-
}
196-
197-
return {verified: true, purposeResult};
187+
return {verified: true, verificationMethod};
198188
} catch(error) {
199189
return {verified: false, error};
200190
}
@@ -236,18 +226,33 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
236226
*
237227
* @returns {Promise<{Uint8Array}>}.
238228
*/
239-
async createVerifyData({
240-
document, proof, documentLoader, expansionMap}) {
229+
async createVerifyData({document, proof, documentLoader, expansionMap}) {
230+
// get cached document hash
231+
let cachedDocHash;
232+
const {_hashCache} = this;
233+
if(_hashCache && _hashCache.document === document) {
234+
cachedDocHash = _hashCache.hash;
235+
} else {
236+
this._hashCache = {
237+
document,
238+
// canonize and hash document
239+
hash: cachedDocHash =
240+
this.canonize(document, {documentLoader, expansionMap})
241+
.then(c14nDocument => sha256digest({string: c14nDocument}))
242+
};
243+
}
244+
245+
// await both c14n proof hash and c14n document hash
246+
const [proofHash, docHash] = await Promise.all([
247+
// canonize and hash proof
248+
this.canonizeProof(
249+
proof, {document, documentLoader, expansionMap})
250+
.then(c14nProofOptions => sha256digest({string: c14nProofOptions})),
251+
cachedDocHash
252+
]);
253+
241254
// concatenate hash of c14n proof options and hash of c14n document
242-
const c14nProofOptions = await this.canonizeProof(
243-
proof, {document, documentLoader, expansionMap});
244-
const c14nDocument = await this.canonize(document, {
245-
documentLoader,
246-
expansionMap
247-
});
248-
return util.concat(
249-
await sha256digest({string: c14nProofOptions}),
250-
await sha256digest({string: c14nDocument}));
255+
return util.concat(proofHash, docHash);
251256
}
252257

253258
/**

0 commit comments

Comments
 (0)