UNPKG

jsonld-signatures-merkleproof2019

Version:

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

358 lines (315 loc) 11.2 kB
import { Decoder } from '@vaultie/lds-merkle-proof-2019'; import jsigs from 'jsonld-signatures'; import { lookForTx, type ExplorerAPI, type TransactionData, type IBlockchainObject } from '@blockcerts/explorer-lookup'; import { type DecodedProof, type VCProof } from './models/Proof'; import getTransactionId from './helpers/getTransactionId'; import getChain from './helpers/getChain'; import { removeEntry } from './utils/array'; import { assertProofValidity, isTransactionIdValid, computeLocalHash, ensureHashesEqual, ensureMerkleRootEqual, ensureValidReceipt, deriveIssuingAddressFromPublicKey, compareIssuingAddress } from './inspectors'; import isMockChain from './helpers/isMockChain'; import VerifierError from './models/VerifierError'; import type IVerificationMethod from './models/VerificationMethod'; const { LinkedDataProof } = jsigs.suites; export interface MerkleProof2019Options { explorerAPIs?: ExplorerAPI[]; executeStepMethod?: (step: string, action: () => any, verificationSuite?: string, type?: string) => Promise<any>; } export interface VCDocument { proof: VCProof | VCProof[]; } export interface MerkleProof2019API { options?: MerkleProof2019Options; issuer?: any; // TODO: define issuer type verificationMethod?: IVerificationMethod; document: VCDocument; proof?: VCProof; // the purpose of proof that the verifier will be used for, defaults to assertionMethod proofPurpose?: string; domain?: string | string[]; challenge?: string; } export interface MerkleProof2019VerificationResult { verified: boolean; error?: string; verificationMethod: IVerificationMethod; } export interface MerkleProof2019VerifyProofAPI { documentLoader?: (url: string) => any; // jsonld document loader hook verifyIdentity?: boolean; // allow splitting verification process for more control } 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 */ public challenge: string; public domain: string[]; public type: string = 'MerkleProof2019'; public issuer: any = null; // TODO: define issuer type public verificationMethod: IVerificationMethod = null; public proof: VCProof = null; public proofValue: DecodedProof = null; public proofPurpose: string; public document: VCDocument = null; public explorerAPIs: ExplorerAPI[] = []; public chain: IBlockchainObject; public txData: TransactionData; public localDocumentHash: string; public derivedIssuingAddress: string; public documentLoader = null; public proofVerificationProcess = [ 'assertProofValidity', 'getTransactionId', 'computeLocalHash', 'fetchRemoteHash', 'compareHashes', 'checkMerkleRoot', 'checkReceipt' ]; public identityVerificationProcess = [ 'deriveIssuingAddressFromPublicKey', 'ensureVerificationMethodValidity', 'compareIssuingAddress' ]; private transactionId: string = ''; constructor ({ issuer = null, verificationMethod = null, document = null, proof = null, options = {}, proofPurpose = 'assertionMethod', domain = [], challenge = '' }: MerkleProof2019API) { super({ type: 'MerkleProof2019' }); if (!document) { throw new Error('A document signed by MerkleProof2019 is required for the verification process.'); } this.issuer = issuer; this.verificationMethod = verificationMethod; this.document = document; this.proofPurpose = proofPurpose; this.domain = Array.isArray(domain) ? domain : [domain]; this.challenge = challenge; this.setProof(proof); this.setOptions(options); this.getChain(); if (isMockChain(this.chain)) { this.adaptProofVerificationProcessToMocknet(); this.adaptIdentityVerificationProcessToMocknet(); } } static decodeMerkleProof2019 (proof: VCProof): DecodedProof { const base58Decoder = new Decoder(proof.proofValue); return base58Decoder.decode(); } setProof (externalProof: VCProof = null): void { const proof = externalProof ?? this.document.proof; if (!proof) { throw new Error('The passed document is not signed.'); } this.proof = proof as any; // TODO: might be an error if externalProof is not defined and document has multiproof this.proofValue = LDMerkleProof2019.decodeMerkleProof2019(this.proof); } async verifyProof ({ documentLoader, verifyIdentity }: MerkleProof2019VerifyProofAPI = { documentLoader: (url): any => {}, verifyIdentity: true }): Promise<MerkleProof2019VerificationResult> { this.documentLoader = documentLoader; let verified: boolean; let error: string = ''; 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 verifyIdentity (): Promise<void> { if (this.verificationMethod != null) { try { await this.verifyProcess(this.identityVerificationProcess); } catch (e) { throw new Error(e); } } } getProofVerificationProcess (): string[] { return this.proofVerificationProcess; } getIdentityVerificationProcess (): string[] { return this.identityVerificationProcess; } getIssuerPublicKey (): string { if (isMockChain(this.chain)) { return 'This mock chain does not support issuing addresses'; } return this.getTxData()?.issuingAddress ?? ''; } getIssuanceTime (): string { return this.getTxData()?.time as string ?? ''; } getChain (): IBlockchainObject { if (!this.chain) { this.chain = getChain(this.proofValue); } return this.chain; } private getTxData (): TransactionData { 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; } private adaptProofVerificationProcessToMocknet (): void { removeEntry(this.proofVerificationProcess, 'getTransactionId'); removeEntry(this.proofVerificationProcess, 'fetchRemoteHash'); removeEntry(this.proofVerificationProcess, 'checkMerkleRoot'); } private adaptIdentityVerificationProcessToMocknet (): void { this.identityVerificationProcess = [ 'ensureVerificationMethodValidity' ]; } private setOptions (options: MerkleProof2019Options): void { this.explorerAPIs = options.explorerAPIs ?? []; if (options.executeStepMethod && typeof options.executeStepMethod === 'function') { this.executeStep = options.executeStepMethod; } } private async executeStep (step: string, action, verificationSuite = ''): Promise<any> { const res: any = await action(); return res; } private async verifyProcess (process: string[]): Promise<void> { for (const verificationStep of process) { if (!this[verificationStep]) { console.error('verification logic for', verificationStep, 'not implemented'); return; } await this[verificationStep](); } } private async assertProofValidity (): Promise<void> { 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 ); } private async checkMerkleRoot (): Promise<void> { await this.executeStep( 'checkMerkleRoot', () => ensureMerkleRootEqual(this.proofValue.merkleRoot, this.txData.remoteHash), this.type // do not remove here or it will break CVJS ); } private async compareHashes (): Promise<void> { await this.executeStep( 'compareHashes', () => ensureHashesEqual(this.localDocumentHash, this.proofValue.targetHash), this.type // do not remove here or it will break CVJS ); } private async computeLocalHash (): Promise<void> { 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 ); } private async checkReceipt (): Promise<void> { await this.executeStep( 'checkReceipt', () => { ensureValidReceipt(this.proofValue); }, this.type ); } private async getTransactionId (): Promise<string> { this.transactionId = getTransactionId(this.proofValue); const transactionId: string = await this.executeStep( 'getTransactionId', () => isTransactionIdValid(this.transactionId), this.type // do not remove here or it will break CVJS ); return transactionId; } private async fetchRemoteHash (): Promise<void> { 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 ##### private async deriveIssuingAddressFromPublicKey (): Promise<void> { this.derivedIssuingAddress = await this.executeStep( 'deriveIssuingAddressFromPublicKey', async () => await deriveIssuingAddressFromPublicKey(this.verificationMethod, this.chain), this.type ); } private async ensureVerificationMethodValidity (): Promise<void> { await this.executeStep( 'ensureVerificationMethodValidity', async (): Promise<void> => { 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 ); } private async compareIssuingAddress (): Promise<void> { await this.executeStep( 'compareIssuingAddress', () => { compareIssuingAddress(this.getIssuerPublicKey(), this.derivedIssuingAddress); }, this.type ); } }