Skip to content

Commit

Permalink
adds derive function and passes proofSet option
Browse files Browse the repository at this point in the history
  • Loading branch information
kezike committed Mar 9, 2024
1 parent 7385905 commit 4b451bd
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 36 deletions.
84 changes: 71 additions & 13 deletions lib/ProofSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,78 @@ 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);

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<RemoteDocument> documentLoader(url)`.
*
* @return {Promise<object>} 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.
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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;
Expand All @@ -176,7 +234,7 @@ async function _getProofs({document}) {
...proof
}));

return {proofSet, document};
return proofSet;
}

async function _verify({
Expand Down
57 changes: 57 additions & 0 deletions lib/jsonld-signatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>} 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.
Expand Down
2 changes: 1 addition & 1 deletion lib/suites/LinkedDataProof.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = class LinkedDataProof {
* @returns {Promise<object>} 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.');
}
Expand Down
76 changes: 54 additions & 22 deletions lib/suites/LinkedDataSignature.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)) {
Expand All @@ -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<object>} 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) {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<object>} Resolves with the created proof object.
*/
Expand All @@ -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(
Expand All @@ -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
});
}

Expand All @@ -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}>}.
*/
Expand Down Expand Up @@ -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;
Expand All @@ -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.`);
}
Expand Down

0 comments on commit 4b451bd

Please sign in to comment.