Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add derive function and passes proofSet option #7

Merged
merged 1 commit into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading