ROOTVIELROOTVIEL
← Academy

Module 7 · Labs & experiments

Lab — build a hash-chain audit log

A guided, self-contained lab on synthetic data. You append entries that each include the previous entry hash, compute a head hash that fixes the whole history, then tamper with a middle entry and recompute to watch the chain break from that point forward. Tamper-evidence detects change; it does not by itself prove who wrote an entry or that the entry was authorized.

Learning objective

Build an append-only log where each entry carries the hash of the previous one, then detect a silent edit by recomputing the chain — making tamper-evidence concrete: you cannot change the past without breaking every hash after it.

Track
Engineering
Module
7 · Labs & experiments
Prerequisites
Hash functions · Hash chains and transparency · Node.js 16+ (built-in crypto)

Lab goal

This teaching exercise, offline and synthetic, builds the simplest tamper-evident structure: a hash chain. Each entry binds to its predecessor by hash, so the latest hash (the head) is a fingerprint of the entire ordered history. Editing any past entry changes its hash, which changes the next entry, and so on — the break is detectable by anyone who recomputes. You will create the break and locate it.

Note

A hash chain makes tampering detectable. It does not prevent it, does not identify the author of an entry (that needs signatures), and does not say an entry was a correct or authorized action.

The model

Entry
A record { index, data, prevHash } where prevHash is the hash of the entry before it (a fixed genesis value for the first).
Entry hash
H(index || data || prevHash). Because it includes prevHash, each hash depends on the entire history behind it.
Head
The hash of the most recent entry. Publishing or signing the head commits to the whole ordered log.

Step by step

  1. 1Define hashEntry(entry) = SHA-256 over its canonical bytes including prevHash.
  2. 2Append entries, each taking prevHash from the hash of the previous entry; the first uses a fixed genesis string.
  3. 3Record the head (hash of the last entry).
  4. 4Write verify(log) that recomputes each hash and checks that every entry prevHash matches the recomputed hash of the entry before it.
  5. 5Tamper: change the data of a middle entry without updating later hashes, then re-run verify and see where it breaks.
lab4.js — synthetic append-only hash chain (Node.js crypto)
const crypto = require('crypto'); const H = (s) => crypto.createHash('sha256').update(s).digest('hex'); const GENESIS = 'genesis'; // Stable serialization: sort object keys recursively so nested content is captured. const canonical = (o) => JSON.stringify(o, (k, v) => v && typeof v === 'object' && !Array.isArray(v) ? Object.fromEntries(Object.keys(v).sort().map((key) => [key, v[key]])) : v); function hashEntry(e) { return H(canonical({ index: e.index, data: e.data, prevHash: e.prevHash })); } function append(log, data) { const prevHash = log.length ? hashEntry(log[log.length - 1]) : GENESIS; log.push({ index: log.length, data, prevHash }); return log; } function verify(log) { for (let i = 0; i < log.length; i++) { const expectedPrev = i === 0 ? GENESIS : hashEntry(log[i - 1]); if (log[i].prevHash !== expectedPrev) return { ok: false, brokeAt: i }; } return { ok: true }; } const log = []; append(log, { action: 'login', who: 'agent-A' }); append(log, { action: 'read', who: 'agent-A', path: 'vault/lab/x' }); append(log, { action: 'write', who: 'agent-B', path: 'vault/lab/y' }); const head = hashEntry(log[log.length - 1]); console.log('verify fresh:', verify(log), 'head:', head.slice(0, 12) + '...'); // Tamper: silently rewrite entry 1's data, leave later prevHash values untouched. log[1].data.path = 'vault/lab/SECRET'; console.log('verify after edit:', verify(log)); console.log('head now :', hashEntry(log[log.length - 1]).slice(0, 12) + '...');

Expected result

expected console output (head digests will differ on your run)
verify fresh: { ok: true } head: 9f2c1a0b7d3e... verify after edit: { ok: false, brokeAt: 2 } head now : 9f2c1a0b7d3e...

Editing entry 1 changes its recomputed hash, so entry 2 prevHash no longer matches — verify reports the break at index 2, the first entry whose stored prevHash disagrees with the recomputed hash of its predecessor. Note the head field printed from the stale last entry is unchanged in the output because entry 2 data was not touched; the point is that a correct verifier recomputes from the edited entry and detects the inconsistency regardless of what the stored head says. (Exact digests vary per run only if you change the data; the structure of the result does not.)

Note

A thorough recompute-from-scratch would also derive a fresh head over the edited history and show it differs from a separately trusted head. The lab keeps it minimal by detecting the first broken link.

Common mistakes

  • ▸Hashing only the data and not prevHash: entries stop depending on history, so editing a past entry no longer breaks anything downstream.
  • ▸Trusting the head value stored next to the log. The head must be obtained independently (signed, or published elsewhere); a tamperer who can also rewrite the stored head defeats a naive check.
  • ▸Non-canonical serialization: if hashEntry serializes differently on write and on verify, honest entries appear broken.
  • ▸Expecting the chain to tell you who wrote an entry or whether it was allowed. It only tells you the ordered history has not been altered; authorship needs signatures, authorization needs policy.

Exercise

Note

Suppose an attacker edits a middle entry AND recomputes every later prevHash so verify(log) returns ok again. What single additional artifact, obtained independently, still lets an auditor detect the tampering — and why does recomputing the chain internally not save them without it?

Expected direction: An independently obtained, trusted head (or a signed/published checkpoint) from before the tampering. The attacker can make the log internally consistent again by recomputing all later hashes, so internal verify alone passes. But recomputing the head over the edited history yields a value that differs from the trusted head the auditor already holds; comparing against that external commitment exposes the change. Without an external anchor, an internally consistent rewrite is undetectable.

Check your understanding

What makes a hash chain tamper-evident?
Each entry hash includes the previous entry hash, so editing any past entry changes its hash and breaks the prevHash link of every entry after it — detectable by recomputation.
Why must the head (latest hash) be obtained independently?
Because an attacker who can rewrite both the log and its stored head can make a tampered log internally consistent; only an independently trusted head or signed checkpoint exposes a full rewrite.
Does a hash chain prove who wrote an entry or that the entry was authorized?
No. It proves only that the ordered history has not been altered. Authorship requires signatures; authorization requires a separate policy check.

References

  1. [1]NIST FIPS 180-4 — Secure Hash Standard (SHA-256)
  2. [2]RFC 6962 — Certificate Transparency (append-only, verifiable logs)
  3. [3]Haber & Stornetta — How to Time-Stamp a Digital Document (hash linking)

References point to public, well-known technical sources for further study.

Related articles