ctjs
Version:
CTjs is a full set of classes necessary to work with any kind of Certificate Transparency log (V1 as from RFC6962, or V2 as from RFC6962-bis). In CTjs you could find all necessary validation/verification functions for all related data shipped with full-fe
290 lines (270 loc) • 10 kB
JavaScript
import { getParametersValue, toBase64, arrayBufferToString, isEqualBuffer } from "pvutils";
import { utils } from "./utils.js";
//**************************************************************************************
export default class MerkleTree
{
//**********************************************************************************
/**
* Constructor for MerkleTree class
* @param {Object} [parameters={}]
* @property {Object} [schema] asn1js parsed value
*/
constructor(parameters = {})
{
//region Internal properties of the object
/**
* @type {Array.<MerkleTreeLeaf|ArrayBuffer|TransItem>}
* @description leafs Array of leafs for the tree
*/
this.leafs = getParametersValue(parameters, "leafs", MerkleTree.constants("leafs"));
/**
* @type {Array.<Array.<ArrayBuffer>>}
* @description nodes Array of array of nodes (calculated hashes) for the tree
* Each subarray represents next node level.
*/
this.nodes = getParametersValue(parameters, "nodes", MerkleTree.constants("nodes"));
/**
* @type {String}
* @description hashName Name of hashing function to apply, default = SHA-256
*/
this.hashName = getParametersValue(parameters, "hashName", MerkleTree.constants("hashName"));
//endregion
}
//**********************************************************************************
/**
* Return value for a constant by name
* @param {string} name String name for a constant
*/
static constants(name)
{
switch(name)
{
case "leafs":
case "nodes":
return [];
case "hashName":
return "SHA-256";
default:
throw new Error(`Invalid constant name for MerkleTree class: ${name}`);
}
}
//**********************************************************************************
/**
* Inialize Merkle Tree using provided set of Merkle Tree Leafs
* @param {Array.<MerkleTreeLeaf|ArrayBuffer|TransItem>} leafs Array of leafs for the tree
* @param {String} [hashName=SHA-256] Name of hashing function to apply, default = SHA-256
* @return {Promise<MerkleTree>}
*/
static async fromLeafs(leafs, hashName = "SHA-256")
{
//region Initial variables
const result = new MerkleTree({ leafs, hashName });
const stack = [];
const nodes = [];
//endregion
//region Initialize first level of nodes
for(let i = 0; i < result.leafs.length; i++)
{
const hash = await result.getHashByIndex(i);
if(i & 1)
nodes.push(await utils.hashChildren(stack.pop(), hash, result.hashName));
else
stack.push(hash);
}
if(stack.length)
nodes.push(stack.pop());
//endregion
//region Initialize all nodes
await result.initializeNodes(nodes);
//endregion
return result;
}
//**********************************************************************************
/**
* Inialize nodes in Merkle Tree given provided hashes from previous node's level
* @param {Array.<ArrayBuffer>} nodes Array of nodes from previous level
* @return {Promise<void>}
*/
async initializeNodes(nodes)
{
//region Initial variables
const stack = [];
const level = [];
//endregion
//region Put new level
this.nodes.push(nodes);
if((nodes.length === 1) || (nodes.length === 0))
return;
//endregion
//region Initialize another level
for(let i = 0; i < nodes.length; i++)
{
if(i & 1)
level.push(await utils.hashChildren(stack.pop(), nodes[i], this.hashName));
else
stack.push(nodes[i]);
}
//endregion
//region Push remainder to one level up
if(stack.length)
{
level.push(stack.pop());
this.nodes[this.nodes.length - 1].pop();
}
//endregion
await this.initializeNodes(level);
}
//**********************************************************************************
/**
* Represent Merkle Tree as a array with BASE-64 encoded values
* @return {Promise<[String[]]>}
*/
async asBase64()
{
//region Initial variables
const output = [[]];
//endregion
//region Store information about all leafs
for(let i = 0; i < this.leafs.length; i++)
output[0].push(toBase64(arrayBufferToString(await this.getHashByIndex(i))));
//endregion
//region Store information about all nodes
for(const level of this.nodes)
output.push(Array.from(level, element => toBase64(arrayBufferToString(element))));
//endregion
return output;
}
//**********************************************************************************
/**
* Append array of Merkel Tree Leafs to current set of leafs
* @param {Array.<MerkleTreeLeaf|ArrayBuffer|TransItem>} array Array of Merkle Tree Leafs
* @return {Promise<void>}
*/
async append(array)
{
const temp = await MerkleTree.fromLeafs([this.leafs, ...array], this.hashName);
this.leafs = temp.leafs.slice();
this.nodes = temp.nodes.slice();
}
//**********************************************************************************
/**
* Return hash for element given its index
* @param {Number} index Zero-based index of the element
* @return {Promise<ArrayBuffer>}
*/
async getHashByIndex(index)
{
//region Check input values
if(index > (this.leafs.length - 1))
return (new ArrayBuffer(0));
//endregion
if("byteLength" in this.leafs[index])
return this.leafs[index];
return this.leafs[index].hash(this.hashName);
}
//**********************************************************************************
/**
* Get array with inclusion proof given hash of element
* @param {ArrayBuffer} hash Hash of existing element
* @return {Promise<Array.<ArrayBuffer>>}
*/
async getProofByHash(hash)
{
for(let i = 0; i < this.leafs.length; i++)
{
if(isEqualBuffer(hash, await this.getHashByIndex(i)))
return this.getProofByIndex(i);
}
return [];
}
//**********************************************************************************
/**
* Get array with inclusion proof given index of element
* @param {Number} index Zero-based index of existing element
* @return {Promise<Array.<ArrayBuffer>>}
*/
async getProofByIndex(index)
{
return utils.PATH(index, this.leafs, this.hashName);
}
//**********************************************************************************
/**
* Verifying an Inclusion Proof for any MerkleTreeLeaf with specified index (RFC6962-bis, 2.1.3.2)
* @param {ArrayBuffer|MerkleTreeLeaf|TransItem} hash MerkleTreeLeaf object or hash of the MerkleTreeLeaf
* @param {Number} treeSize The Merkle tree size to which we need to proof existance
* @param {ArrayBuffer} rootHash Hash of the root to compare with
* @param {Array.<ArrayBuffer>} proof Array of hashes with proof
* @return {Promise<boolean>}
*/
async verifyProofByHash(hash, treeSize, rootHash, proof)
{
for(let i = 0; i < this.leafs.length; i++)
{
if(isEqualBuffer(hash, await this.getHashByIndex(i)))
return this.verifyProofByIndex(i, treeSize, rootHash, proof);
}
return false;
}
//**********************************************************************************
/**
* Verifying an Inclusion Proof for any MerkleTreeLeaf with specified index (RFC6962-bis, 2.1.3.2)
* @param {Number} index Index of MerkleTreeLeaf object
* @param {Number} treeSize The Merkle tree size to which we need to proof existance
* @param {ArrayBuffer} rootHash Hash of the root to compare with
* @param {Array.<ArrayBuffer>} proof Array of hashes with proof
* @return {Promise<boolean>}
*/
async verifyProofByIndex(index, treeSize, rootHash, proof)
{
const leafHash = await this.getHashByIndex(index);
return utils.verifyInclusionProof(leafHash, index, treeSize, rootHash, proof, this.hashName);
}
//**********************************************************************************
/**
* Get consistency proof given size of previous tree to compare with
* @param {Number} size Size of previous tree to compare with
* @return {Promise<Array.<ArrayBuffer>>}
*/
async getConsistency(size)
{
return utils.PROOF(size, this.leafs, this.hashName);
}
//**********************************************************************************
/**
* Verifying Consistency between Two Tree Heads (RFC6962-bis, 2.1.4.2)
* @param {Number} first First tree size to compare
* @param {ArrayBuffer} firstHash Hash of the root for first tree size
* @param {Number} second Second tree size to compare
* @param {ArrayBuffer} secondHash Hash of the root for second tree size
* @param {Array.<ArrayBuffer>} consistency Array of hashes necessary for consistency verification
* @return {Promise<boolean>}
*/
async verifyConsistency(first, firstHash, second, secondHash, consistency)
{
return utils.verifyConsistency(first, firstHash, second, secondHash, consistency, this.hashName);
}
//**********************************************************************************
/**
* Get hash of the tree head
* @return {Promise<ArrayBuffer>}
*/
async getRootHash()
{
return utils.MTH(this.leafs, this.hashName);
}
//**********************************************************************************
/**
* Calculate a Tree Head Given Proofs (RFC6962-bis, 2.1.3.2)
* @param {Number} index Index of the MerkleTreeLeaf
* @param {Number} treeSize The Merkle tree size to which we need to proof existance
* @param {Array.<ArrayBuffer>} proof Array of hashes with proof
* @return {Promise<ArrayBuffer>}
*/
async getRootHashByProof(index, treeSize, proof)
{
const leafHash = await this.getHashByIndex(index);
return utils.calculateRootHashByProof(leafHash, index, treeSize, proof, this.hashName);
}
//**********************************************************************************
}
//**************************************************************************************