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 a 'addSuiteContext' flag to LinkedDataSignature suite. #139

Merged
merged 6 commits into from
Apr 9, 2021
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
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

## 9.0.1 -

These changes were intended to be released in v9.0.0, so, releasing them as
a patch.

### Changed
- **BREAKING**: Implement automatic adding of the suite context to the document
to be signed, (if it's not present already).
- **BREAKING**: Remove the case where the `document` argument in `sign()` or
`verify()` is a URL (instead of an object), since this is an unused feature,
and a mixing of layers.

## 9.0.0 - 2021-04-06

### Changed
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@
- [Commercial Support](#commercial-support)
- [License](#license)

## Version Compatibility

`jsonld-signatures` **v9.0** is compatible with the following signature suites:

* [`ed25519-signature-2020`](https://github.com/digitalbazaar/ed25519-signature-2020)
`>= 2.1.0`.

and the following related libraries:

* `crypto-ld` `>= 5.0.0` (and related key crypto suites such as
[`ed25519-verification-key-2020`](https://github.com/digitalbazaar/ed25519-verification-key-2020)
`>= 2.1.0`).
* `vc-js` `>= 7.0` (currently, [branch `v7.x`](https://github.com/digitalbazaar/vc-js/pull/83))

## Background

A Linked Data Signature proof is created (or verified) by specifying a
Expand Down
26 changes: 5 additions & 21 deletions lib/ProofSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ module.exports = class ProofSet {
* document has the same definition as the `https://w3id.org/security/v2`
* JSON-LD @context.
*
* @param document {object|string} Object to be signed, either a string URL
* (resolved via the given `documentLoader`) or a plain object (JSON-LD
* document).
* @param document {object} - JSON-LD Document to be signed.
* @param options {object} Options hashmap.
*
* A `suite` option is required:
Expand Down Expand Up @@ -63,11 +61,6 @@ module.exports = class ProofSet {
expansionMap = strictExpansionMap;
}

if(typeof document === 'string') {
// fetch document
document = await documentLoader(document);
}

// preprocess document to prepare to remove existing proofs
// let input;
// shallow copy document to allow removal of existing proofs
Expand All @@ -94,9 +87,8 @@ module.exports = class ProofSet {
* document has the same definition as the `https://w3id.org/security/v2`
* JSON-LD @context.
*
* @param {object|string} document - Object with one or more proofs to be
* verified, either a string URL (resolved to an object via the given
* `documentLoader`) or a plain object (JSON-LD document).
* @param {object} document - The JSON-LD document with one or more proofs to
* be verified.
*
* @param {LinkedDataSignature|LinkedDataSignature[]} suite -
* Acceptable signature suite instances for verifying the proof(s).
Expand Down Expand Up @@ -143,16 +135,8 @@ module.exports = class ProofSet {
}

try {
if(typeof document === 'string') {
// fetch document
document = await documentLoader(document);
} else {
// TODO: consider in-place editing to optimize when `compactProof`
// is `false`

// shallow copy to allow for removal of proof set prior to canonize
document = {...document};
}
// shallow copy to allow for removal of proof set prior to canonize
document = {...document};

// get proofs from document
const {proofSet, document: doc} = await _getProofs({
Expand Down
87 changes: 73 additions & 14 deletions lib/jsonld-signatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,48 @@ const VerificationError = require('./VerificationError');
* Cryptographically signs the provided document by adding a `proof` section,
* based on the provided suite and proof purpose.
*
* @param {object|string} document - The document to be signed, either a string
* URL (resolved to an object via the given `documentLoader`) or a plain
* object (JSON-LD document).
* @param {object} document - The JSON-LD document to be signed.
*
* @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 {LinkedDataSignature} 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} [documentLoader] - A custom document loader,
* `Promise<RemoteDocument> documentLoader(url)`.
* @param {function} [expansionMap] - A custom expansion map that is
* @param {function} [options.expansionMap] - A custom expansion map that is
* passed to the JSON-LD processor; by default a function that will throw
* an error when unmapped properties are detected in the input, use `false`
* to turn this off and allow unmapped properties to be dropped or use a
* custom function.
* @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.sign = async function sign(document, {
suite, purpose, documentLoader, expansionMap
suite, purpose, documentLoader, expansionMap, 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).
_ensureSuiteContext({
document, contextUrl: suite.contextUrl, addSuiteContext
});
Copy link
Member

@dlongley dlongley Apr 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this will need to be moved into ProofSet.add to handle the document as a string use case. And we need a test for it too.


try {
return await new ProofSet().add(
document, {suite, purpose, documentLoader, expansionMap});
Expand All @@ -60,13 +77,11 @@ api.sign = async function sign(document, {
}
};

/* eslint-disable max-len */
/**
* Verifies the linked data signature on the provided document.
*
* @param {object|string} document - The document with one or more proofs to be
* verified, either a string URL (resolved to an object via the given
* `documentLoader`) or a plain object (JSON-LD document).
* @param {object} document - The JSON-LD document with one or more proofs to be
* verified.
*
* @param {LinkedDataSignature|LinkedDataSignature[]} suite -
* Acceptable signature suite instances for verifying the proof(s).
Expand All @@ -93,9 +108,11 @@ api.sign = async function sign(document, {
* if `false` an `error` property will be present, with `error.errors`
* containing all of the errors that occurred during the verification process.
*/
/* eslint-enable */
api.verify = async function verify(document, {
suite, purpose, documentLoader, expansionMap} = {}) {
if(typeof document !== 'object') {
throw new TypeError('The "document" parameter must be an object.');
}
const result = await new ProofSet().verify(
document, {suite, purpose, documentLoader, expansionMap});
const {error} = result;
Expand All @@ -121,3 +138,45 @@ api.purposes = require('./purposes').purposes;

// expose document loader helpers
Object.assign(api, require('./documentLoader'));

/**
* Ensures the document to be signed contains the required signature suite
* specific `@context`, by either adding it (if `addSuiteContext` is true),
* or throwing an error if it's missing.
*
* @param {object} options - Options hashmap.
* @param {object} options.document - A JSON-LD document.
* @param {string} options.contextUrl - A context url.
* @param {boolean} options.addSuiteContext - Add suite context?
*/
function _ensureSuiteContext({document, contextUrl, addSuiteContext}) {
if(_includesContext({document, contextUrl})) {
// document already includes the required context
return;
}

if(!addSuiteContext) {
throw new TypeError(
`The document to be signed must contain this suite's @context, ` +
`"${contextUrl}".`);
}

// enforce the suite's context by adding it to the document
document['@context'] = [...document['@context'] || [], contextUrl];
}

/**
* Tests whether a provided JSON-LD document includes a context url in its
* `@context` property.
*
* @param {object} options - Options hashmap.
* @param {object} options.document - A JSON-LD document.
* @param {string} options.contextUrl - A context url.
*
* @returns {boolean} Returns true if document includes context.
*/
function _includesContext({document, contextUrl}) {
const context = document['@context'];
return context === contextUrl ||
(Array.isArray(context) && context.includes(contextUrl));
}
108 changes: 59 additions & 49 deletions lib/suites/LinkedDataSignature.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const LinkedDataProof = require('./LinkedDataProof');

module.exports = class LinkedDataSignature extends LinkedDataProof {
/**
* Parent class from which the various LinkDataSignature suites (such as
* `Ed25519Signature2020`) inherit.
* NOTE: Developers are never expected to use this class directly, but to
* only work with individual suites.
*
* @param {object} options - Options hashmap.
* @param {string} options.type - Suite name, provided by subclass.
* @typedef LDKeyPair
Expand All @@ -19,6 +24,10 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
* during the `verifySignature` operation, to create an instance (containing
* a `verifier()` property) of a public key fetched via a `documentLoader`.
*
* @param {string} contextUrl - JSON-LD context url that corresponds to this
* signature suite. Provided by subclass. Used for enforcing suite context
* during the `sign()` operation.
*
* For `sign()` operations, either a `key` OR a `signer` is required.
* For `verify()` operations, you can pass in a verifier (from KMS), or
* the public key will be fetched via documentLoader.
Expand All @@ -45,18 +54,20 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
* for the `proof` node (e.g. any other custom fields can be provided here
* using a context different from security-v2). If not provided, this is
* constructed during signing.
* @param {string|Date} [options.date] - Signing date to use if not passed.
* @param {string|Date} [options.date] - Signing date to use (otherwise
* defaults to `now()`).
* @param {boolean} [options.useNativeCanonize] - Whether to use a native
* canonize algorithm.
*/
constructor({
type, proof, LDKeyClass, date, key, signer, verifier,
useNativeCanonize
type, proof, LDKeyClass, date, key, signer, verifier, useNativeCanonize,
contextUrl
} = {}) {
super({type});
this.LDKeyClass = LDKeyClass;
this.contextUrl = contextUrl;
this.proof = proof;
const vm = this._processSignatureParams({key, signer, verifier});
const vm = _processSignatureParams({key, signer, verifier});
this.verificationMethod = vm.verificationMethod;
this.key = vm.key;
this.signer = vm.signer;
Expand All @@ -78,8 +89,7 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
*
* @returns {Promise<object>} Resolves with the created proof object.
*/
async createProof(
{document, purpose, documentLoader, expansionMap}) {
async createProof({document, purpose, documentLoader, expansionMap}) {
// build proof (currently known as `signature options` in spec)
let proof;
if(this.proof) {
Expand Down Expand Up @@ -301,52 +311,52 @@ module.exports = class LinkedDataSignature extends LinkedDataProof {
async verifySignature() {
throw new Error('Must be implemented by a derived class.');
}
};

/**
* See constructor docstring for param details.
*
* @returns {{verificationMethod: string, key: LDKeyPair,
* signer: {sign: Function, id: string},
* verifier: {verify: Function, id: string}}} - Validated and initialized
* key-related parameters.
*/
_processSignatureParams({key, signer, verifier}) {
// We are explicitly not requiring a key or signer/verifier param to be
// present, to support the verify() use case where the verificationMethod
// is being fetched by the documentLoader

const vm = {};
if(key) {
vm.key = key;
vm.verificationMethod = key.id;
if(typeof key.signer === 'function') {
vm.signer = key.signer();
}
if(typeof key.verifier === 'function') {
vm.verifier = key.verifier();
}
if(!(vm.signer || vm.verifier)) {
throw new TypeError(
'The "key" parameter must contain a "signer" or "verifier" method.');
}
} else {
vm.verificationMethod = (signer && signer.id) ||
(verifier && verifier.id);
vm.signer = signer;
vm.verifier = verifier;
/**
* See constructor docstring for param details.
*
* @returns {{verificationMethod: string, key: LDKeyPair,
* signer: {sign: Function, id: string},
* verifier: {verify: Function, id: string}}} - Validated and initialized
* key-related parameters.
*/
function _processSignatureParams({key, signer, verifier}) {
// We are explicitly not requiring a key or signer/verifier param to be
// present, to support the verify() use case where the verificationMethod
// is being fetched by the documentLoader

const vm = {};
if(key) {
vm.key = key;
vm.verificationMethod = key.id;
if(typeof key.signer === 'function') {
vm.signer = key.signer();
}

if(vm.signer) {
if(typeof vm.signer.sign !== 'function') {
throw new TypeError('A signer API has not been specified.');
}
if(typeof key.verifier === 'function') {
vm.verifier = key.verifier();
}
if(vm.verifier) {
if(typeof vm.verifier.verify !== 'function') {
throw new TypeError('A verifier API has not been specified.');
}
if(!(vm.signer || vm.verifier)) {
throw new TypeError(
'The "key" parameter must contain a "signer" or "verifier" method.');
}
} else {
vm.verificationMethod = (signer && signer.id) ||
(verifier && verifier.id);
vm.signer = signer;
vm.verifier = verifier;
}

return vm;
if(vm.signer) {
if(typeof vm.signer.sign !== 'function') {
throw new TypeError('A signer API has not been specified.');
}
}
};
if(vm.verifier) {
if(typeof vm.verifier.verify !== 'function') {
throw new TypeError('A verifier API has not been specified.');
}
}

return vm;
}