diff --git a/lib/ProofSet.js b/lib/ProofSet.js index f0607dfc..99fef2e6 100644 --- a/lib/ProofSet.js +++ b/lib/ProofSet.js @@ -59,10 +59,13 @@ module.exports = class ProofSet { delete input.proof; + // get existing proof set, if any + const proofSet = _getProofs({document}); + // create the new proof (suites MUST output a proof using the security-v2 // `@context`) const proof = await suite.createProof({ - document: input, purpose, documentLoader + document: input, purpose, proofSet, documentLoader }); jsonld.addValue(document, 'proof', proof); @@ -70,6 +73,64 @@ module.exports = class ProofSet { return document; } + /** + * Derives a new Linked Data document with a new `proof` from an existing + * document with an existing proof set. + * + * 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 from which to derive a proof. + * @param options {object} Options hashmap. + * + * A `suite` option is required: + * + * @param options.suite {LinkedDataSignature} a signature suite instance + * that will derive the new document and new `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 documentLoader(url)`. + * + * @return {Promise} resolves with the new document, with a new + * top-level `proof` property. + */ + async derive(document, {suite, purpose, documentLoader} = {}) { + 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; + } + + // shallow copy document to allow removal of existing proofs + const input = {...document}; + delete input.proof; + + // get existing proof set, if any + const proofSet = _getProofs({document}); + + // create the new document and proof + const newDocument = await suite.derive({ + document: input, purpose, proofSet, documentLoader + }); + + return newDocument; + } + /** * Verifies Linked Data proof(s) on a document. The proofs to be verified * must match the given proof purpose. @@ -122,10 +183,13 @@ module.exports = class ProofSet { document = {...document}; // get proofs from document - const {proofSet, document: doc} = await _getProofs({ - document, documentLoader - }); - document = doc; + const proofSet = _getProofs({document}); + if(proofSet.length === 0) { + // no possible matches + throw new Error('No matching proofs found in the given document.'); + } + // clear proofs from shallow copy + delete document.proof; // verify proofs const results = await _verify({ @@ -158,16 +222,10 @@ module.exports = class ProofSet { } }; -async function _getProofs({document}) { +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; @@ -176,7 +234,7 @@ async function _getProofs({document}) { ...proof })); - return {proofSet, document}; + return proofSet; } async function _verify({ diff --git a/lib/jsonld-signatures.js b/lib/jsonld-signatures.js index 366ed672..1ff2b3b4 100644 --- a/lib/jsonld-signatures.js +++ b/lib/jsonld-signatures.js @@ -15,6 +15,63 @@ Object.assign(api, constants); const ProofSet = require('./ProofSet'); const VerificationError = require('./VerificationError'); +/** + * Derives a proof from the provided document, resulting in a new document + * with a new `proof` on it as generated by the given cryptographic suite. + * + * @param {object} document - The JSON-LD document from which to derive a + * new proof. + * + * @param {object} options - Options hashmap. + * @param {LinkedDataSignature} options.suite - The linked data signature + * cryptographic suite, containing private key material, with which to sign + * the document. + * + * @param {ProofPurpose} purpose - A proof purpose instance that will + * match proofs to be verified and ensure they were created according to + * the appropriate purpose. + * + * @param {function} documentLoader - A secure document loader (it is + * recommended to use one that provides static known documents, instead of + * fetching from the web) for returning contexts, controller documents, keys, + * and other relevant URLs needed for the proof. + * + * Advanced optional parameters and overrides: + * + * @param {function} [options.expansionMap] - NOT SUPPORTED; do not use. + * @param {boolean} [options.addSuiteContext=true] - Toggles the default + * behavior of each signature suite enforcing the presence of its own + * `@context` (if it is not present, it's added to the context list). + * + * @returns {Promise} Resolves with signed document. + */ +api.derive = async function derive(document, { + suite, purpose, documentLoader, addSuiteContext = true +} = {}) { + if(typeof document !== 'object') { + throw new TypeError('The "document" parameter must be an object.'); + } + // Ensure document contains the signature suite specific context URL + // or throw an error (in case an advanced user overrides the + // `addSuiteContext` flag to false). + suite.ensureSuiteContext({document, addSuiteContext}); + + try { + return await new ProofSet().derive( + document, {suite, purpose, documentLoader}); + } catch(e) { + if(!documentLoader && e.name === 'jsonld.InvalidUrl') { + const {details: {url}} = e; + const err = new Error( + `A URL "${url}" could not be fetched; you need to pass ` + + '"documentLoader" or resolve the URL before calling "derive".'); + err.cause = e; + throw err; + } + throw e; + } +}; + /** * Cryptographically signs the provided document by adding a `proof` section, * based on the provided suite and proof purpose. diff --git a/lib/suites/LinkedDataProof.js b/lib/suites/LinkedDataProof.js index 916fe3ae..ff4878e4 100644 --- a/lib/suites/LinkedDataProof.js +++ b/lib/suites/LinkedDataProof.js @@ -19,7 +19,7 @@ module.exports = class LinkedDataProof { * @returns {Promise} Resolves with the created proof object. */ async createProof({ - /* document, purpose, documentLoader */ + /* document, purpose, proofSet, documentLoader */ }) { throw new Error('"createProof" must be implemented in a derived class.'); } diff --git a/lib/suites/LinkedDataSignature.js b/lib/suites/LinkedDataSignature.js index 19860429..b727b2ba 100644 --- a/lib/suites/LinkedDataSignature.js +++ b/lib/suites/LinkedDataSignature.js @@ -58,10 +58,12 @@ module.exports = class LinkedDataSignature extends LinkedDataProof { * defaults to `now()`). * @param {boolean} [options.useNativeCanonize] - Whether to use a native * canonize algorithm. + * @param {object} [options.canonizeOptions] - Options to pass to + * canonize algorithm. */ constructor({ type, proof, LDKeyClass, date, key, signer, verifier, useNativeCanonize, - contextUrl + canonizeOptions, contextUrl } = {}) { super({type}); this.LDKeyClass = LDKeyClass; @@ -72,6 +74,7 @@ module.exports = class LinkedDataSignature extends LinkedDataProof { this.key = vm.key; this.signer = vm.signer; this.verifier = vm.verifier; + this.canonizeOptions = canonizeOptions; if(date) { this.date = new Date(date); if(isNaN(this.date)) { @@ -83,13 +86,15 @@ module.exports = class LinkedDataSignature extends LinkedDataProof { } /** - * @param document {object} to be signed. - * @param purpose {ProofPurpose} - * @param documentLoader {function} + * @param {object} options - The options to use. + * @param {object} options.document - The document to be signed. + * @param {ProofPurpose} options.purpose - The proof purpose instance. + * @param {Array} options.proofSet - Any existing proof set. + * @param {function} options.documentLoader - The document loader to use. * * @returns {Promise} Resolves with the created proof object. */ - async createProof({document, purpose, documentLoader}) { + async createProof({document, purpose, proofSet, documentLoader}) { // build proof (currently known as `signature options` in spec) let proof; if(this.proof) { @@ -123,7 +128,7 @@ module.exports = class LinkedDataSignature extends LinkedDataProof { // add any extensions to proof (mostly for legacy support) proof = await this.updateProof({ - document, proof, purpose, documentLoader + document, proof, proofSet, purpose, documentLoader }); // allow purpose to update the proof; the `proof` is in the @@ -134,7 +139,7 @@ module.exports = class LinkedDataSignature extends LinkedDataProof { // create data to sign const verifyData = await this.createVerifyData({ - document, proof, documentLoader + document, proof, proofSet, documentLoader }); // sign data @@ -145,9 +150,9 @@ module.exports = class LinkedDataSignature extends LinkedDataProof { } /** - * @param document {object} to be signed. - * @param purpose {ProofPurpose} - * @param documentLoader {function} + * @param {object} options - The options to use. + * @param {object} options.proof - The proof to be updated. + * @param {Array} options.proofSet - Any existing proof set. * * @returns {Promise} Resolves with the created proof object. */ @@ -157,17 +162,20 @@ module.exports = class LinkedDataSignature extends LinkedDataProof { } /** - * @param proof {object} the proof to be verified. - * @param document {object} the document the proof applies to. - * @param documentLoader {function} + * @param {object} options - The options to use. + * @param {object} options.proof - The proof to be verified. + * @param {object} options.document - The document the proof applies to. + * @param {ProofPurpose} options.purpose - The proof purpose instance. + * @param {Array} options.proofSet - Any existing proof set. + * @param {function} options.documentLoader - The document loader to use. * * @returns {Promise<{object}>} Resolves with the verification result. */ - async verifyProof({proof, document, documentLoader}) { + async verifyProof({proof, proofSet, document, documentLoader}) { try { // create data to verify const verifyData = await this.createVerifyData( - {document, proof, documentLoader}); + {document, proof, proofSet, documentLoader}); // fetch verification method const verificationMethod = await this.getVerificationMethod( @@ -190,10 +198,16 @@ module.exports = class LinkedDataSignature extends LinkedDataProof { async canonize(input, {documentLoader, skipExpansion}) { return jsonld.canonize(input, { algorithm: 'URDNA2015', + // do not resolve any relative URLs or terms, throw errors instead + base: null, format: 'application/n-quads', documentLoader, + // throw errors if any values would be dropped due to missing + // definitions or relative URLs + safe: true, skipExpansion, - useNative: this.useNativeCanonize + useNative: this.useNativeCanonize, + ...this.canonizeOptions }); } @@ -209,14 +223,17 @@ module.exports = class LinkedDataSignature extends LinkedDataProof { delete proof.proofValue; return this.canonize(proof, { documentLoader, - skipExpansion: false + skipExpansion: false, + ...this.canonizeOptions }); } /** - * @param document {object} to be signed/verified. - * @param proof {object} - * @param documentLoader {function} + * @param {object} options - The options to use. + * @param {object} options.document - The document to be signed/verified. + * @param {object} options.proof - The proof to be verified. + * @param {Array} options.proofSet - Any existing proof set. + * @param {function} options.documentLoader - The document loader to use. * * @returns {Promise<{Uint8Array}>}. */ @@ -250,9 +267,24 @@ module.exports = class LinkedDataSignature extends LinkedDataProof { } /** - * @param document {object} to be signed. + * @param verifyData {Uint8Array}. + * @param document {object} document from which to derive a new document + * and proof. * @param proof {object} + * @param proofSet {Array} * @param documentLoader {function} + * + * @returns {Promise<{object}>} The new document with `proof`. + */ + async derive() { + throw new Error('Must be implemented by a derived class.'); + } + + /** + * @param proof {object} + * @param documentLoader {function} + * + * @returns {Promise<{object}>} The new document with `proof`. */ async getVerificationMethod({proof, documentLoader}) { let {verificationMethod} = proof; @@ -269,7 +301,7 @@ module.exports = class LinkedDataSignature extends LinkedDataProof { '@context': constants.SECURITY_CONTEXT_URL, '@embed': '@always', id: verificationMethod - }, {documentLoader, compactToRelative: false}); + }, {documentLoader, compactToRelative: false, safe: true}); if(!framed) { throw new Error(`Verification method ${verificationMethod} not found.`); }