jsonld-signatures
Version:
An implementation of the Linked Data Signatures specifications for JSON-LD in JavaScript.
136 lines (127 loc) • 5.04 kB
JavaScript
/*!
* Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
*/
;
const constants = require('../constants');
const jsonld = require('jsonld');
const ProofPurpose = require('./ProofPurpose');
// DID documents can be specially optimized
const DID_CONTEXT_V1 = 'https://www.w3.org/ns/did/v1';
// verification relationship terms that are known to appear in DID documents
const DID_VR_TERMS = [
'assertionMethod',
'authentication',
'capabilityInvocation',
'capabilityDelegation',
'keyAgreement',
'verificationMethod'
];
module.exports = class ControllerProofPurpose extends ProofPurpose {
/**
* Creates a proof purpose that will validate whether or not the verification
* method in a proof was authorized by its declared controller for the
* proof's purpose.
*
* @param term {string} the `proofPurpose` term, as defined in the
* SECURITY_CONTEXT_URL `@context` or a URI if not defined in such.
* @param [controller] {object} the description of the controller, if it
* is not to be dereferenced via a `documentLoader`.
* @param [date] {string or Date or integer} the expected date for
* the creation of the proof.
* @param [maxTimestampDelta] {integer} a maximum number of seconds that
* the date on the signature can deviate from, defaults to `Infinity`.
*/
constructor({term, controller, date, maxTimestampDelta = Infinity} = {}) {
super({term, date, maxTimestampDelta});
if(controller !== undefined) {
if(typeof controller !== 'object') {
throw new TypeError('"controller" must be an object.');
}
this.controller = controller;
}
this._termDefinedByDIDContext = DID_VR_TERMS.includes(term);
}
/**
* Validates the purpose of a proof. This method is called during
* proof verification, after the proof value has been checked against the
* given verification method (e.g. in the case of a digital signature, the
* signature has been cryptographically verified against the public key).
*
* @param proof
* @param verificationMethod
* @param documentLoader
* @param expansionMap
*
* @throws {Error} If verification method not authorized by controller
* @throws {Error} If proof's created timestamp is out of range
*
* @returns {Promise<{valid: boolean, error: Error}>}
*/
async validate(proof, {verificationMethod, documentLoader, expansionMap}) {
try {
const result = await super.validate(
proof, {verificationMethod, documentLoader, expansionMap});
if(!result.valid) {
throw result.error;
}
const {id: verificationId} = verificationMethod;
if(typeof verificationId !== 'string') {
throw new Error(
`Verification method ID "${verificationMethod.id}" ` +
'must be a string.');
}
const {term, _termDefinedByDIDContext} = this;
// if no `controller` specified, use verification method's
if(this.controller) {
result.controller = this.controller;
} else {
const {controller} = verificationMethod;
let controllerId;
if(controller) {
if(typeof controller === 'object') {
controllerId = controller.id;
} else if(typeof controller !== 'string') {
throw new TypeError(
'"controller" must be a string representing a URL.');
} else {
controllerId = controller;
}
}
// apply optimization to controller documents that are DID documents;
// if `term` is one of those defined by the DID context
let {document} = await documentLoader(controllerId);
const mustFrame = !(_termDefinedByDIDContext &&
document['@context'] === DID_CONTEXT_V1 ||
(Array.isArray(document['@context']) &&
document['@context'][0] === DID_CONTEXT_V1));
if(mustFrame) {
// Note: `expansionMap` is intentionally not passed; we can safely
// drop properties here and must allow for it
document = await jsonld.frame(document, {
'@context': constants.SECURITY_CONTEXT_URL,
id: controllerId,
// this term must be in the JSON-LD controller document or
// verification will fail
[term]: {
'@embed': '@never',
id: verificationId
}
}, {documentLoader, compactToRelative: false, safe: true});
}
result.controller = document;
}
const verificationMethods = jsonld.getValues(result.controller, term);
result.valid = verificationMethods.some(vm =>
vm === verificationId ||
(typeof vm === 'object' && vm.id === verificationId));
if(!result.valid) {
throw new Error(
`Verification method "${verificationMethod.id}" not authorized ` +
`by controller for proof purpose "${this.term}".`);
}
return result;
} catch(error) {
return {valid: false, error};
}
}
};