Merkle trees: inclusion and consistency proofs (RFC 6962)

RFC 6962 (Certificate Transparency) / RFC 9162 (CT 2.0) Merkle-tree hash and proof-verification core: the load-bearing primitive a static-CT client, a Merkle-Tree-Certificates relying party, and a sigstore / Rekor inclusion check all compose. Every verb is strict verification over SHA-256: zero new crypto.

leafHash / nodeHash / emptyRootHash build the tree hashes with the two domain-separation prefixes fixed by the spec: a leaf is SHA-256(0x00 || entry), an interior node is SHA-256(0x01 || left || right), the empty tree is SHA-256(""). Those 0x00 / 0x01 prefixes are the second-preimage defense: without them a leaf whose bytes equal a valid interior node's preimage could be smuggled in as present.

verifyInclusion folds an audit path back to a root and constant-time- compares it to a trusted checkpoint root; verifyConsistency reconstructs both the old and the new root from a consistency proof (the append-only guarantee lives in the old-root leg). Both are fail-closed: a malformed coordinate, an out-of-range index, an inverted window, a wrong hash length, or a proof whose node count does not match the tree geometry throws a typed merkle/* error; the one boolean-false result is the final root comparison ("root matched" vs "did not"). A false from verifyInclusion means "not proven present against this root", never "validly absent": an inclusion proof cannot express absence. Tree coordinates are uint64, carried as BigInt so a large index is never Number-narrowed. This is not a DER format. Like pki.ct it is a companion module reached explicitly, never routed by the detect-and-parse orchestrator.

pki.merkle.leafHash

since 0.1.28 stable
pki.merkle.leafHash(entry) -> Buffer

The Merkle leaf hash MTH({d}) = SHA-256(0x00 || entry). The 0x00 prefix is the leaf-domain second-preimage separation and is applied unconditionally. Throws merkle/bad-input if entry is not a Buffer / Uint8Array.

Example

pki.merkle.leafHash(Buffer.from("leaf data")); // -> <Buffer 32-byte leaf hash>

References

pki.merkle.nodeHash

since 0.1.28 stable
pki.merkle.nodeHash(left, right) -> Buffer

The Merkle interior-node hash SHA-256(0x01 || left || right). Both operands must be 32-byte hashes; the 0x01 prefix is applied unconditionally. Throws merkle/bad-input on a non-buffer operand, merkle/bad-hash-length on an operand that is not exactly 32 bytes.

Example

var l = pki.merkle.leafHash(Buffer.from([0]));
var r = pki.merkle.leafHash(Buffer.from([1]));
pki.merkle.nodeHash(l, r); // -> <Buffer 32-byte node hash>

References

pki.merkle.emptyRootHash

since 0.1.28 stable
pki.merkle.emptyRootHash() -> Buffer

The Merkle tree head of the empty tree, MTH({}) = SHA-256("") (e3b0c442...b855). A fresh Buffer each call.

Example

pki.merkle.emptyRootHash(); // -> <Buffer e3 b0 c4 42 ...>

References

pki.merkle.verifyInclusion

since 0.1.28 stable
pki.merkle.verifyInclusion(opts) -> boolean

Verify an RFC 6962 / RFC 9162 audit (inclusion) proof: fold leafHash up the audit path and constant-time-compare the reconstructed root to rootHash. Returns true iff the proof binds the leaf to the root; a well-formed proof that does not match returns false, meaning "not proven present against this root" and never "validly absent". A malformed input throws a typed merkle/* error: a coordinate that is not a non-negative integer (or a Number >= 2^53), treeSize 0, leafIndex >= treeSize, a non-32-byte hash, or a proof whose node count does not match the tree geometry.

Options

leafIndex:  number | bigint,  // 0-based leaf position (uint64; pass BigInt above 2^53)
treeSize:   number | bigint,  // total leaf count of the tree the root commits to
leafHash:   Buffer,           // 32-byte leaf hash (e.g. from pki.merkle.leafHash)
proof:      Buffer[],         // the audit path, each node a 32-byte hash
rootHash:   Buffer,           // 32-byte trusted checkpoint root

Example

var lh = pki.merkle.leafHash(Buffer.from([0]));
pki.merkle.verifyInclusion({ leafIndex: 0, treeSize: 1, leafHash: lh, proof: [], rootHash: lh }); // -> true

References

pki.merkle.verifyConsistency

since 0.1.28 stable
pki.merkle.verifyConsistency(opts) -> boolean

Verify an RFC 6962 / RFC 9162 consistency proof between an older tree of oldSize leaves (root oldRoot) and a newer tree of newSize leaves (root newRoot). Reconstructs both roots from the proof and constant-time-compares each; returns true iff both match. The append-only guarantee lives in the old-root leg: a proof that yields a valid newRoot but the wrong oldRoot is a rewritten history and returns false. Equal non-zero sizes require an empty proof and oldRoot == newRoot.

An oldSize of 0 with a non-empty newer tree is refused as merkle/no-consistency-claim. RFC 6962 sec. 2.1.2 defines the proof for 0 < oldSize < newSize: the empty tree is a prefix of every tree by definition, so there is no proof to check and nothing at all binds newRoot. Two empty trees are still answered: that is the degenerate identity case, and both roots must be emptyRootHash().

A malformed input throws a typed merkle/* error: oldSize > newSize, a non-empty proof where the geometry requires none (or empty where it requires one), a non-32-byte hash, or a wrong node count.

Options

oldSize:  number | bigint,  // leaf count of the older tree (uint64)
newSize:  number | bigint,  // leaf count of the newer tree (>= oldSize)
oldRoot:  Buffer,           // 32-byte root of the older tree
newRoot:  Buffer,           // 32-byte root of the newer tree
proof:    Buffer[],         // the consistency proof, each node a 32-byte hash

Example

var r = pki.merkle.leafHash(Buffer.from([0]));
pki.merkle.verifyConsistency({ oldSize: 1, newSize: 1, oldRoot: r, newRoot: r, proof: [] }); // -> true

References

pki.merkle.root

since 0.8.34 stable
pki.merkle.root(leafHashes) -> Buffer

The RFC 6962 Merkle Tree Hash MTH(D[n]) over a whole tree, returned as a 32-byte root ready to pass to verifyInclusion as rootHash. Takes leaf HASHES, the same form verifyInclusion takes as leafHash; map entries through pki.merkle.leafHash first. An empty array is the empty tree and returns emptyRootHash().

Throws merkle/bad-input if leafHashes is not an array or a leaf is not a Buffer / Uint8Array, merkle/bad-hash-length if a leaf is not exactly 32 bytes, and merkle/too-many-leaves above C.LIMITS.MERKLE_MAX_LEAVES.

Example

var leaves = [Buffer.from([0]), Buffer.from([1])].map(pki.merkle.leafHash);
pki.merkle.root(leaves); // -> <Buffer 32-byte tree head>

References

pki.merkle.inclusionProof

since 0.8.34 stable
pki.merkle.inclusionProof(opts) -> Buffer[]

Produce the RFC 6962 sec. 2.1.1 audit path PATH(leafIndex, D[n]) for one leaf of a tree held in memory. The returned array is the proof that verifyInclusion folds against root(leafHashes).

The tree size is the length of leafHashes and is not a separate option, so a size and a leaf array cannot disagree. Throws merkle/empty-tree on an empty tree, merkle/index-out-of-range for a leafIndex at or above the leaf count, and the leafHashes errors root throws.

Options

leafHashes:  Buffer[],       // the whole tree, each leaf a 32-byte hash
leafIndex:   number | bigint, // 0-based position of the leaf to prove

Example

var leaves = [Buffer.from([0]), Buffer.from([1])].map(pki.merkle.leafHash);
pki.merkle.inclusionProof({ leafHashes: leaves, leafIndex: 0 }); // -> [<Buffer ...>]

References

pki.merkle.consistencyProof

since 0.8.34 stable
pki.merkle.consistencyProof(opts) -> Buffer[]

Produce the RFC 6962 sec. 2.1.2 consistency proof PROOF(oldSize, D[n]) between an older tree of oldSize leaves and the tree held in leafHashes. The returned array is the proof that verifyConsistency checks against root(leafHashes.slice(0, oldSize)) and root(leafHashes).

The newer size is the length of leafHashes and is not a separate option. Equal sizes produce the empty proof, which is what the verifier requires there. Throws merkle/old-size-exceeds-new when oldSize is past the leaf count and merkle/no-consistency-claim for an oldSize of 0 against a non-empty tree, matching verifyConsistency: RFC 6962 sec. 2.1.2 defines the proof for 0 < oldSize < newSize, so there is no proof to produce.

Options

leafHashes:  Buffer[],        // the newer tree, each leaf a 32-byte hash
oldSize:     number | bigint, // leaf count of the older tree

Example

var leaves = [Buffer.from([0]), Buffer.from([1])].map(pki.merkle.leafHash);
pki.merkle.consistencyProof({ leafHashes: leaves, oldSize: 1 }); // -> [<Buffer ...>]

References