Skip to content

Repository files navigation

crypto-lab-merkle-vault

crypto-lab portfolio Deploy to GitHub Pages

What It Is

crypto-lab-merkle-vault implements binary Merkle trees and inclusion proofs using SHA-256 via the Web Crypto API. A Merkle tree is a binary hash tree where leaf nodes contain hashes of data items and internal nodes contain hashes of their children, producing a single root hash that cryptographically commits to the entire dataset. An inclusion proof demonstrates that a specific item is in the tree using only O(log n) hashes - for a million-item dataset, 20 hashes suffice. The security model is collision resistance of SHA-256: an attacker cannot produce a valid proof for an item not in the tree without finding a SHA-256 collision. Domain separation prefixes (0x00 for leaves, 0x01 for internal nodes) prevent second preimage attacks on the tree structure per RFC 6962.

The live tree is drawn — SVG connector lines run from every parent node to its two children — and generating a proof highlights the exact sibling consumed at each level as the target leaf climbs to the root. A step-by-step "walk the proof" mode advances one level at a time, showing the running hash, the sibling being combined, and the resulting parent, with the corresponding tree nodes lighting up in sync — so O(log n) siblings become an internalized mechanism rather than a text list. When the climb reaches the top, the recomputed root and the committed root are shown side by side and asserted equal byte-for-byte (turning green on a match, red after a tamper) instead of a bare "VALID" line.

The second-preimage attack is mounted, not described. A domain-separation toggle rebuilds the same items under the naive convention — leaf = SHA-256(data), node = SHA-256(L || R), no prefixes — and then runs the attack the prefixes exist to stop. Take the internal node above leaves 0 and 1: its 64 raw child bytes L || R are themselves a valid leaf payload whose leaf hash is that node's hash. Presented with the sibling path from that node upward, the ordinary verifyProof() rebuilds the genuine committed root for 64 bytes that were never in the list. The page shows the impersonated node, the forged payload, its leaf hash, the collision check, the recomputed root, and the verifier's own verdict — then the identical computation under RFC 6962, where the leaf hashes into a disjoint space and the forgery is refused. One byte of prefix is the entire difference, and both outcomes are computed on every render.

Consistency (append-only) proofs. An inclusion proof says an item is in the tree you published; it says nothing about whether today's tree is last week's tree plus new entries. The RFC 6962 §2.1.2 consistency proof is the check a log auditor actually runs, and it now has its own panel: choose what the log operator did between the two published roots — append honestly, rewrite an old entry, delete one, or reorder two — and the RFC 9162 verification algorithm runs against the old root the auditor already holds. Every tampering case still grows the log, so nothing is caught merely because the size shrank. The panel prints the old and new roots, the O(log n) proof hashes, and the roots the replay actually rebuilds, each flagged against the root it is supposed to equal — so a rejection shows its own reasoning instead of asserting a verdict.

Odd-node handling is selectable, and the default is the safe one. When a level has an odd number of nodes, the demo defaults to the RFC 6962 rule — the lone node is promoted (carried up unchanged), never duplicated. A "Bitcoin — duplicate" mode is also provided, which instead hashes the lone node with a copy of itself. That Bitcoin convention is the exact CVE-2012-2459 block-malleability pattern: the leaf lists [a,b,c] and [a,b,c,c] hash to the same root. The app computes both roots live from real SHA-256 so you can see the collision appear in Bitcoin mode and disappear in RFC 6962 mode. To keep the core build/prove/tamper loop uncluttered for a first read, the odd-node selector and this malleability demonstration live in a collapsed "Advanced" subsection beneath the main loop. This is why the "per RFC 6962" framing above is honest for the default construction, and the vulnerable mode is clearly labelled as such.

Where This Sits Next to crypto-lab-merkle-proofs

Two labs in this catalog cover Merkle trees, and they are not interchangeable. This one is about the structure: the tree is drawn with parent→child connectors, a single proof is walked one level at a time with the corresponding nodes lighting up as the running hash rises, the recomputed and committed roots are asserted equal byte for byte at the top, and the odd-node convention, the second-preimage attack, and the append-only audit are each mounted against the tree you personally built. crypto-lab-merkle-proofs is about proof semantics: the trust model of who supplies the root, byte-level hash preimages for every step, RFC 9162 index-based verification, and a pinned real Certificate Transparency entry checked against Google's Argon log. Start here for what a Merkle tree is and how one climb works; go there for what a proof does and does not entitle you to believe.

When to Use It

  • Use Merkle trees when you need to commit to a large dataset and later prove membership of individual items efficiently - blockchain transactions, certificate logs, software package manifests.
  • Use inclusion proofs when verifiers cannot download the full dataset but need cryptographic assurance that a specific item is present.
  • Use append-only Merkle logs (as in Certificate Transparency) when you need a tamper-evident audit trail where deletions are detectable — the consistency-proof panel on the page runs exactly that check, including the rewrite, delete, and reorder cases it catches.
  • Do not use Merkle trees for exclusion proofs without additional structure - proving an item is NOT in a tree requires sorted Merkle trees or accumulators.
  • Do not omit domain separation prefixes - the second preimage attack on trees without 0x00/0x01 prefixes is a real structural vulnerability, not a theoretical concern.
  • Do NOT treat this as production code - it is a teaching demo for exploring Merkle tree structure and proofs, not a hardened transparency-log library.

Live Demo

systemslibrarian.github.io/crypto-lab-merkle-vault

Enter up to 16 items (or use the library catalog, Git commits, or transaction presets), build the tree with real SHA-256, select any leaf, and generate an inclusion proof. The tree is drawn with parent→child connector lines, and generating a proof highlights the sibling pulled in at each level. Press "Walk the proof" to climb one level at a time — the running hash, the sibling being combined, and the resulting parent are shown while the matching tree nodes light up. At the top, the recomputed root is asserted equal to the committed root byte-for-byte. Click "Tamper leaf" to modify one item and watch the root change, the highlighted proof path flag as tampered, and the equality assertion turn red. The gated Advanced subsection exposes the odd-node convention (RFC 6962 promote vs Bitcoin duplicate): in Bitcoin mode a proof that lands on a self-copied position is flagged as an "odd-leaf duplication leak," and the live malleability panel recomputes root([a,b,c]) vs root([a,b,c,c]) under both conventions. The proof size calculator shows how O(log n) scales to billions of items.

What Can Go Wrong

  • Missing domain separation: without 0x00/0x01 prefixes, an internal node hash can be presented as a leaf hash, constructing a valid-looking proof for data not in the tree (second preimage attack on tree structure). You can run this on the page — flip the convention to "No domain separation" and the forged 64-byte payload is accepted by the real verifier; flip it back and the same payload is refused.
  • Odd-leaf duplication leaks: duplicating the last leaf to even the tree can allow an attacker to forge proofs for the duplicated position - implementations should track which positions are real vs. duplicated. This demo lets you switch into that (Bitcoin) mode and see it: [a,b,c] and [a,b,c,c] collide on one root (CVE-2012-2459), and any proof step that is a self-copy is flagged. The RFC 6962 default (promote, not duplicate) removes the leak entirely.
  • Root confusion across trees: a valid inclusion proof is only meaningful relative to a specific root. Without binding the root to a signed, timestamped commitment, an attacker can substitute a different tree with the same structure.
  • Hash function weakness: Merkle tree security reduces entirely to collision resistance of the underlying hash. SHA-1-based Merkle trees (early Git) are weakened by SHAttered - Git is migrating to SHA-256.
  • Unbalanced trees and depth confusion: non-binary or unbalanced tree implementations can produce ambiguous proof paths where the same proof validates against multiple roots.

Real-World Usage

  • Git object model: a commit object references a tree object by hash, and each tree references its subtrees and blobs by hash, forming a Merkle DAG over the directory structure rather than a balanced binary tree. Changing one file changes every hash above it, making the entire repository history tamper-evident. The default object hash is still SHA-1 (hardened against SHAttered since v2.13); SHA-256 is available as an opt-in repository format.
  • Bitcoin SPV: lightweight Bitcoin clients verify transaction inclusion using Merkle proofs against the block header's Merkle root, downloading only 80-byte headers rather than full blocks.
  • Certificate Transparency (RFC 6962): publicly trusted TLS certificates are logged in append-only Merkle logs; Chrome and Safari require Signed Certificate Timestamps (a log's signed promise to include the certificate) at connection time, and auditors later check the corresponding Merkle inclusion proofs against the log's signed tree head.
  • Package managers, as a contrast: npm, Yarn, and Cargo do not build Merkle trees. They pin a flat, independent hash per artifact - npm and Yarn record a Subresource-Integrity integrity string (normally sha512) for each tarball in package-lock.json / yarn.lock, and Cargo records a sha256 checksum per package in Cargo.lock. That detects a tampered download, but there is no root and no O(log n) inclusion proof: verifying one package means holding a hash for every package. The tree structure in this demo is exactly what buys you the logarithmic proof.
  • Ethereum state trie: Ethereum's Patricia-Merkle trie commits to the entire world state (all account balances and contract storage) in each block header.

How to Run Locally

git clone https://github.com/systemslibrarian/crypto-lab-merkle-vault
cd crypto-lab-merkle-vault
npm install
npm run dev      # dev server
npm test         # crypto unit tests (KATs, round-trip, forgery, CVE-2012-2459, second-preimage, consistency, fuzz)
npm run build    # tsc --noEmit + vite build
npm run test:a11y  # Playwright: WCAG A/AA gate (axe-core) + e2e/demo.spec.ts behaviour gate

Related Demos


Part of the Crypto Lab suite.

"So whether you eat or drink or whatever you do, do it all for the glory of God." — 1 Corinthians 10:31

About

Browser-based Merkle tree and inclusion proof demo — build trees up to 16 leaves with real SHA-256, generate O(log n) membership proofs, tamper any leaf and watch the root change, with Git, Bitcoin, and Certificate Transparency walkthroughs. Part of crypto-lab.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages