UNPKG

jsonld-signatures-merkleproof2019

Version:

A jsonld signature implementation to support MerkleProof2019 verification in Verifiable Credential context

286 lines (285 loc) 11.4 kB
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); } }