jsonld-signatures-merkleproof2019
Version:
A jsonld signature implementation to support MerkleProof2019 verification in Verifiable Credential context
286 lines (285 loc) • 11.4 kB
JavaScript
import { Decoder } from '@blockcerts/lds-merkle-proof-2019';
import jsigs from 'jsonld-signatures';
import { lookForTx } from '@blockcerts/explorer-lookup';
import getTransactionId from './helpers/getTransactionId.js';
import getChain from './helpers/getChain.js';
import { removeEntry } from './utils/array.js';
import { assertProofValidity, isTransactionIdValid, computeLocalHash, ensureHashesEqual, ensureMerkleRootEqual, ensureValidReceipt, deriveIssuingAddressFromPublicKey, compareIssuingAddress } from './inspectors/index.js';
import isMockChain from './helpers/isMockChain.js';
import VerifierError from './models/VerifierError.js';
const { LinkedDataProof } = jsigs.suites;
export class LDMerkleProof2019 extends LinkedDataProof {
/**
* @param [issuer] {string} A key id URL to the paired public key.
* @param [verificationMethod] {string} A key id URL to the paired public key.
* @param [proof] {object} a JSON-LD document with options to use for
* the `proof` node (e.g. any other custom fields can be provided here
* using a context different from security-v2).
* @param [document] {document} document used and signed by the MerkleProof2019 signature
*/
challenge;
domain;
type = 'MerkleProof2019';
issuer = null; // TODO: define issuer type
issuerEndpoint = '';
verificationMethod = null;
externalProof = null; // external proof passed to the constructor, used for verification
proof = null;
proofValue = null;
proofPurpose;
document = null;
explorerAPIs = [];
chain;
txData;
localDocumentHash;
derivedIssuingAddress;
documentLoader = null;
proofVerificationProcess = [
'assertProofValidity',
'getTransactionId',
'computeLocalHash',
'fetchRemoteHash',
'compareHashes',
'checkMerkleRoot',
'checkReceipt'
];
identityVerificationProcess = [
'deriveIssuingAddressFromPublicKey',
'ensureVerificationMethodValidity',
'compareIssuingAddress'
];
transactionId = '';
constructor({ issuer = null, verificationMethod = null, document = null, proof = null, options = {}, proofPurpose = 'assertionMethod', domain = [], challenge = '' }) {
super({ type: 'MerkleProof2019' });
this.issuer = issuer;
this.verificationMethod = verificationMethod;
this.document = document;
this.proofPurpose = proofPurpose;
this.domain = Array.isArray(domain) ? domain : [domain];
this.challenge = challenge;
this.externalProof = proof;
this.setOptions(options);
if (this.externalProof || this.document) {
this.setProof(this.externalProof);
this.getChain();
if (isMockChain(this.chain)) {
this.adaptProofVerificationProcessToMocknet();
this.adaptIdentityVerificationProcessToMocknet();
}
}
}
static decodeMerkleProof2019(proof) {
const base58Decoder = new Decoder(proof.proofValue);
return base58Decoder.decode();
}
createVerifier() { }
createVerifyData() { }
setProof(externalProof = null) {
const proof = externalProof ?? this.document.proof;
if (!proof) {
throw new Error('The passed document is not signed.');
}
this.proof = proof; // TODO: might be an error if externalProof is not defined and document has multiproof
this.proofValue = LDMerkleProof2019.decodeMerkleProof2019(this.proof);
}
async verifyProof({ documentLoader, verifyIdentity, document, proof } = {
documentLoader: (url) => { },
verifyIdentity: true
}) {
this.documentLoader = documentLoader;
let verified;
let error = '';
if (document) {
this.document = document;
}
if (proof) {
this.setProof(proof);
}
this.getChain();
if (isMockChain(this.chain)) {
this.adaptProofVerificationProcessToMocknet();
this.adaptIdentityVerificationProcessToMocknet();
}
if (!this.document) {
throw new Error('A document signed by MerkleProof2019 is required for the verification process.');
}
try {
await this.verifyProcess(this.proofVerificationProcess);
if (verifyIdentity) {
await this.verifyIdentity();
}
verified = true;
}
catch (e) {
console.error(e);
verified = false;
error = e.message;
}
return {
verificationMethod: this.verificationMethod,
verified,
error
};
}
async createProof({ document, purpose = 'assertionMethod' }) {
if (!this.issuerEndpoint) {
throw new Error('No issuer endpoint provided. ' +
'Please set the issuerEndpoint option when instantiating the suite.');
}
const response = await fetch(this.issuerEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
credential: document
})
})
.then(response => response.json())
.catch(error => {
console.error(error);
});
if (response.verifiableCredential) {
return response.verifiableCredential.proof;
}
else {
console.error('An error occurred while creating the proof:', response);
}
}
async createProofValue({ document }) {
const proof = await this.createProof({ document });
return proof ? proof.proofValue : 'error creating proof';
}
ensureSuiteContext() { }
async verifyIdentity() {
if (this.verificationMethod != null) {
try {
await this.verifyProcess(this.identityVerificationProcess);
}
catch (e) {
throw new Error(e);
}
}
}
getProofVerificationProcess() {
return this.proofVerificationProcess;
}
getIdentityVerificationProcess() {
return this.identityVerificationProcess;
}
getIssuerPublicKey() {
if (isMockChain(this.chain)) {
return 'This mock chain does not support issuing addresses';
}
return this.getTxData()?.issuingAddress ?? '';
}
getIssuanceTime() {
return this.getTxData()?.time ?? '';
}
getChain() {
if (!this.chain) {
this.chain = getChain(this.proofValue);
}
return this.chain;
}
getTxData() {
if (!this.txData) {
console.error('Trying to access issuing address when txData not available yet. Did you run the `verify` method yet?');
return null;
}
return this.txData;
}
adaptProofVerificationProcessToMocknet() {
removeEntry(this.proofVerificationProcess, 'getTransactionId');
removeEntry(this.proofVerificationProcess, 'fetchRemoteHash');
removeEntry(this.proofVerificationProcess, 'checkMerkleRoot');
}
adaptIdentityVerificationProcessToMocknet() {
this.identityVerificationProcess = [
'ensureVerificationMethodValidity'
];
}
setOptions(options) {
this.explorerAPIs = options.explorerAPIs ?? [];
if (options.executeStepMethod && typeof options.executeStepMethod === 'function') {
this.executeStep = options.executeStepMethod;
}
this.issuerEndpoint = options.issuerEndpoint ?? '';
}
async executeStep(step, action, verificationSuite = '') {
const res = await action();
return res;
}
async verifyProcess(process) {
for (const verificationStep of process) {
if (!this[verificationStep]) {
console.error('verification logic for', verificationStep, 'not implemented');
return;
}
await this[verificationStep]();
}
}
async assertProofValidity() {
await this.executeStep('assertProofValidity', () => assertProofValidity({
expectedProofPurpose: this.proofPurpose,
expectedDomain: this.domain,
expectedChallenge: this.challenge,
proof: this.proof,
issuer: this.issuer
}), this.type // do not remove here or it will break CVJS
);
}
async checkMerkleRoot() {
await this.executeStep('checkMerkleRoot', () => ensureMerkleRootEqual(this.proofValue.merkleRoot, this.txData.remoteHash), this.type // do not remove here or it will break CVJS
);
}
async compareHashes() {
await this.executeStep('compareHashes', () => ensureHashesEqual(this.localDocumentHash, this.proofValue.targetHash), this.type // do not remove here or it will break CVJS
);
}
async computeLocalHash() {
this.localDocumentHash = await this.executeStep('computeLocalHash', async () => await computeLocalHash(this.document, this.proof, this.documentLoader), this.type // do not remove here or it will break CVJS
);
}
async checkReceipt() {
await this.executeStep('checkReceipt', () => { ensureValidReceipt(this.proofValue); }, this.type);
}
async getTransactionId() {
this.transactionId = getTransactionId(this.proofValue);
const transactionId = await this.executeStep('getTransactionId', () => isTransactionIdValid(this.transactionId), this.type // do not remove here or it will break CVJS
);
return transactionId;
}
async fetchRemoteHash() {
this.txData = await this.executeStep('fetchRemoteHash', async () => {
const txData = await lookForTx({
transactionId: this.transactionId,
chain: this.chain?.code,
explorerAPIs: this.explorerAPIs
});
return txData;
}, this.type // do not remove here or it will break CVJS
);
}
// ##### DID CORRELATION #####
async deriveIssuingAddressFromPublicKey() {
this.derivedIssuingAddress = await this.executeStep('deriveIssuingAddressFromPublicKey', async () => await deriveIssuingAddressFromPublicKey(this.verificationMethod, this.chain), this.type);
}
async ensureVerificationMethodValidity() {
await this.executeStep('ensureVerificationMethodValidity', async () => {
if (this.verificationMethod.expires) {
const expirationDate = new Date(this.verificationMethod.expires).getTime();
if (expirationDate < Date.now()) {
throw new VerifierError('ensureVerificationMethodValidity', 'The verification key has expired');
}
}
if (this.verificationMethod.revoked) {
// waiting on clarification https://github.com/w3c/cid/issues/152
throw new VerifierError('ensureVerificationMethodValidity', 'The verification key has been revoked');
}
}, this.type);
}
async compareIssuingAddress() {
await this.executeStep('compareIssuingAddress', () => { compareIssuingAddress(this.getIssuerPublicKey(), this.derivedIssuingAddress); }, this.type);
}
}