UNPKG

@cronstamp/clientlib

Version:

Client library for cronstamp, a blockchain-based document timestamping and verification service.

182 lines 9.61 kB
import { CertificateHelper, getCertificateFromFile, getCertificateFromString } from '../certificate.js'; import { AdditionalDataType, CertificateProcess, InputDataType, MessageType, Step } from './manager.js'; import { hashSiblingHashes, isValidHash, VersionToHashAlgorithm } from '../util/hash.js'; import { blockchainNames } from '../blockchains/blockchains.js'; import { Xrp } from '../blockchains/xrp.js'; import { Solana } from '../blockchains/solana.js'; /** * Start certificate verification for a string * @param certificate either as type Certificate, Json string or File * @param message */ export function verifyCertificateForString(certificate, message) { return new VerificationProcess(certificate, { type: InputDataType.STRING, data: message }); } /** * Start certificate verification for a file * @param certificate either as type Certificate, Json string or File * @param file */ export function verifyCertificateForFile(certificate, file) { return new VerificationProcess(certificate, { type: InputDataType.FILE, data: file }); } /** * Start certificate verification for an existing hash * @param certificate * @param hash base64 encoded hash */ export function verifyCertificateForHash(certificate, hash) { return new VerificationProcess(certificate, { type: InputDataType.HASH, data: hash }); } /** * Start verify process * @param certificate * @param type * @param data */ export function verifyCertificate(certificate, type, data) { return new VerificationProcess(certificate, { type: type, data: data }); } export class VerificationProcess extends CertificateProcess { // the certificate to verify, is not modified certificateToVerify; certificateToParse; skipBlockchains; constructor(certificate, data) { super(data, 'Verify'); if (certificate instanceof File || typeof certificate === 'string') { this.certificateToParse = certificate; } else { this.certificateToVerify = certificate; } this.skipBlockchains = []; } async start() { // the general idea is to incrementally build up the internal cert of CertificateProcess and compare it to the user provided certificate and blockchain // after super.start() a certificate with document_hash is available let preparation = await super.start(); if (preparation.step != Step.HASH_INPUT_DATA || preparation.type != MessageType.SUCCESS) { return preparation; } try { // hash data again and compare resulting hash to user provided certificate this.step = Step.VERIFICATION_CHECK_CERTIFICATE_SYNTAX; if (this.certificateToParse !== undefined) { if (this.certificateToParse instanceof File) { this.certificateToVerify = await getCertificateFromFile(this.certificateToParse); } else { this.certificateToVerify = await getCertificateFromString(this.certificateToParse); } } if (!new CertificateHelper(this.certificateToVerify).hasData()) { return this.sendError('Missing or invalid data in certificate!', AdditionalDataType.INVALID_SYNTAX_CERTIFICATE, this.certificateToVerify); } let unknownBlockchains = this.certificateToVerify.meta.blockchains.filter((blockchain) => !blockchainNames.includes(blockchain)); if (unknownBlockchains.length > 0) { return this.sendError('Unknown blockchain name found in certificate: ' + unknownBlockchains.join(', ')); } this.sendMessage('Fully loaded certificate from file or directly.', MessageType.SUCCESS, AdditionalDataType.PARSED_CERTIFICATE, this.certificateToVerify); // hash data again and compare resulting hash to user provided certificate this.step = Step.VERIFICATION_COMPARE_SALTED_HASHES; if (this.certHelper.cert.document_hash != this.certificateToVerify.document_hash) { return this.sendError('Locally calculated data hash does not match hash in provided certificate: ' + this.certHelper.cert.document_hash + ' != ' + this.certificateToVerify.document_hash); } this.certHelper.cert.meta.salt = this.certificateToVerify.meta.salt; this.certHelper.cert.meta.blockchains = this.certificateToVerify.meta.blockchains; let saltedHash = await this.certHelper.calculateSaltedHash(); this.sendMessage('Locally calculated data hash matches hash in provided certificate.', MessageType.SUCCESS); // blockchains are only added to the cert, once they are verified this.certHelper.cert.blockchains = {}; // verify in all blockchains in the certificate for (let blockchain of this.certificateToVerify.meta.blockchains) { if (this.skipBlockchains.includes(blockchain)) { continue; } try { const result = await this.verifySingleBlockchain(blockchain, saltedHash); if (result.type == MessageType.SUCCESS && result.step === Step.VERIFICATION_VERIFY_SINGLE_BLOCKCHAIN) { this.certHelper.cert.blockchains[blockchain] = this.certificateToVerify.blockchains[blockchain]; } } catch (error) { this.handleException(error); } finally { } } // done handling blockchain specific verification this.currentBlockchain = undefined; // get timestamp from certificate ( based on all blockchains ) let timestamp = new CertificateHelper(this.certificateToVerify).getCertificateUnixTime(); //require at least one verified blockchain const requiredBlockchains = this.certHelper.cert.meta.blockchains.filter((b) => !this.skipBlockchains.includes(b)); const blockchainsVerified = Object.keys(this.certHelper.cert.blockchains); if (blockchainsVerified.length < requiredBlockchains.length) { if (blockchainsVerified.length == 0) { return this.sendError(`The blockchains [${requiredBlockchains.join(', ')}] included in this certificate were not verified!`); } if (this.skipBlockchains.length > 0) { return this.sendError(`The blockchains [${blockchainsVerified.join(', ')}] of the blockchains [${requiredBlockchains.join(', ')}] included in this certificate were successfully verified!`); } else { return this.sendError(`Only the blockchains [${blockchainsVerified.join(', ')}] of the required blockchains [${requiredBlockchains.join(', ')}] included in this certificate were successfully verified!`); } } // this step only has type success and always means verification was fully successful this.step = Step.VERIFICATION_ALL_BLOCKCHAINS_SUCCEEDED; return this.sendMessage('Verified root hash successfully in ' + blockchainsVerified.length + ' blockchains.', MessageType.SUCCESS, AdditionalDataType.TIMESTAMP, timestamp); } catch (e) { return this.handleException(e); } } async verifySingleBlockchain(blockchain, saltedHash) { // start verification for one specific blockchain this.currentBlockchain = blockchain; this.step = Step.VERIFICATION_VERIFY_SINGLE_BLOCKCHAIN; let blockchainData = this.certificateToVerify.blockchains[blockchain]; if (!(blockchain in this.certificateToVerify.blockchains && this.certificateToVerify?.blockchains[blockchain]?.transaction !== undefined && this.certificateToVerify.blockchains[blockchain]?.block_timestamp !== undefined && this.certificateToVerify.blockchains[blockchain]?.merkle_tree_splice !== undefined)) { return this.sendError(`Missing data for blockchain ${blockchain} expected keys "transaction", "block_timestamp" and "merkle_tree_splice".`); } // calculate root node hash if (!blockchainData.merkle_tree_splice.every((hash) => hash === '' || isValidHash(hash, VersionToHashAlgorithm[this.certificateToVerify.meta.version]))) { return this.sendError('Invalid hash in merkletree splice for blockchain: ' + blockchain); } let rootHash = saltedHash; for (let siblingHash of blockchainData.merkle_tree_splice) { rootHash = await hashSiblingHashes(rootHash, siblingHash); } this.sendMessage('Calculated merkletree root hash: ' + rootHash, MessageType.PROGRESS, AdditionalDataType.HASH, rootHash); switch (blockchain) { case 'xrp': { return await new Xrp(this.log).verifyRootHash(rootHash, blockchainData.block_timestamp, blockchainData.transaction, this); } case 'solana': { return await new Solana(this.log).verifyRootHash(rootHash, blockchainData.block_timestamp, blockchainData.transaction, this); } default: return this.sendError('Unknown blockchain provided in certificate: ' + blockchain); } } } //# sourceMappingURL=verify.js.map