Skip to content

Commit

Permalink
Add derive API and pass existing proofSet to suite functions.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed May 13, 2023
1 parent f206a53 commit e05341a
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 28 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# jsonld-signatures ChangeLog

## 11.2.0 - 2023-05-dd

### Added
- Add support for `derive` function to be implemented by the given
cryptosuite. The `derive` function is used to derive a new document with
a new `proof` based on an existing `document` (and `proof`). The `derive`
function will be used when calling `derive` from `jsonld-signatures`.
- Any existing `proofSet` from the input document will be passed to the
`createProof` and `verifyProof` functions in `LinkedDataProof` and
`LinkedDataSignature`. It will also be passed to `createVerifyData` in
`LinkedDataSignature`.

## 11.1.0 - 2023-02-07

### Added
Expand Down
89 changes: 72 additions & 17 deletions lib/ProofSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,24 +52,81 @@ module.exports = class ProofSet {
documentLoader = strictDocumentLoader;
}

// preprocess document to prepare to remove existing proofs
// let input;
// shallow copy document to allow removal of existing proofs
const input = {...document};

delete input.proof;

// create the new proof (suites MUST output a proof using the security-v2
// `@context`)
// get existing proof set, if any
const proofSet = _getProofs({document});

// create the new proof
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,9 +179,13 @@ module.exports = class ProofSet {
document = {...document};

// get proofs from document
const {proofSet, document: doc} = await _getProofs(
{document, documentLoader});
document = doc;
const proofSet = await _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 @@ -160,12 +221,6 @@ async 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 @@ -174,7 +229,7 @@ async function _getProofs({document}) {
...proof
}));

return {proofSet, document};
return proofSet;
}

async function _verify({
Expand Down Expand Up @@ -211,7 +266,7 @@ async function _verify({
}
};
const {verified, verificationMethod, error} = await suite.verifyProof(
{proof, document, purpose, documentLoader});
{proof, document, purpose, proofSet, documentLoader});
if(!vm) {
vm = verificationMethod;
}
Expand Down
62 changes: 59 additions & 3 deletions lib/jsonld-signatures.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2010-2022 Digital Bazaar, Inc. All rights reserved.
* Copyright (c) 2010-2023 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';

Expand All @@ -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 All @@ -23,8 +80,7 @@ const VerificationError = require('./VerificationError');
*
* @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.
* cryptographic suite 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
Expand Down
25 changes: 22 additions & 3 deletions lib/suites/LinkedDataProof.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
* Copyright (c) 2018-2023 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';

Expand All @@ -15,29 +15,48 @@ module.exports = class LinkedDataProof {
* @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.
* @param {function} options.expansionMap - NOT SUPPORTED; do not use.
*
* @returns {Promise<object>} Resolves with the created proof object.
*/
async createProof({
/* document, purpose, documentLoader, expansionMap */
/* document, purpose, proofSet, documentLoader, expansionMap */
}) {
throw new Error('"createProof" must be implemented in a derived class.');
}

/**
* @param {object} options - The options to use.
* @param {object} options.document - The document from which to derive
* a new document and proof.
* @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 new document with a new
* `proof` field.
*/
async derive({
/* document, purpose, proofSet, documentLoader */
}) {
throw new Error('"deriveProof" must be implemented in a derived class.');
}

/**
* @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.
* @param {function} options.expansionMap - NOT SUPPORTED; do not use.
*
* @returns {Promise<{object}>} Resolves with the verification result.
*/
async verifyProof({
/* proof, document, purpose, documentLoader, expansionMap */
/* proof, document, purpose, proofSet, documentLoader, expansionMap */
}) {
throw new Error('"verifyProof" must be implemented in a derived class.');
}
Expand Down
31 changes: 26 additions & 5 deletions lib/suites/LinkedDataSignature.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,15 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
* @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.
* @param {function} options.expansionMap - NOT SUPPORTED; do not use.
*
* @returns {Promise<object>} Resolves with the created proof object.
*/
async createProof({document, purpose, documentLoader, expansionMap}) {
async createProof({
document, purpose, proofSet, documentLoader, expansionMap
}) {
if(expansionMap) {
throw new Error('"expansionMap" not supported.');
}
Expand Down Expand Up @@ -131,7 +134,8 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
proof.verificationMethod = this.verificationMethod;

// add any extensions to proof (mostly for legacy support)
proof = await this.updateProof({document, proof, purpose, documentLoader});
proof = await this.updateProof(
{document, proof, proofSet, purpose, documentLoader});

// allow purpose to update the proof; the `proof` is in the
// SECURITY_CONTEXT_URL `@context` -- therefore the `purpose` must
Expand All @@ -141,7 +145,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
proof = await this.sign({verifyData, document, proof, documentLoader});
Expand All @@ -152,6 +156,7 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
/**
* @param {object} options - The options to use.
* @param {object} options.proof - The proof to be updated.
* @param {Array} options.proofSet - Any existing proof set.
* @param {function} options.expansionMap - NOT SUPPORTED; do not use.
*
* @returns {Promise<object>} Resolves with the created proof object.
Expand All @@ -169,20 +174,21 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
* @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.
* @param {function} options.expansionMap - NOT SUPPORTED; do not use.
*
* @returns {Promise<{object}>} Resolves with the verification result.
*/
async verifyProof({proof, document, documentLoader, expansionMap}) {
async verifyProof({proof, document, proofSet, documentLoader, expansionMap}) {
if(expansionMap) {
throw new Error('"expansionMap" not supported.');
}

try {
// create data to verify
const verifyData = await this.createVerifyData(
{document, proof, documentLoader, expansionMap});
{document, proof, proofSet, documentLoader, expansionMap});

// fetch verification method
const verificationMethod = await this.getVerificationMethod(
Expand Down Expand Up @@ -246,6 +252,7 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
* @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.
* @param {function} options.expansionMap - NOT SUPPORTED; do not use.
*
Expand Down Expand Up @@ -283,6 +290,20 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
return util.concat(proofHash, docHash);
}

/**
* @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 document {object} to be signed.
* @param proof {object}
Expand Down

0 comments on commit e05341a

Please sign in to comment.