← Back to docs

How the hash chain proves tamper-evidence

Every fasten audit row carries a SHA-256 hash chained to the row before it. Rewriting any earlier row breaks the chain at that point, and verify_chain() returns the position deterministically. This page walks the primitives and a worked example an auditor can reproduce.

Audience. Auditors, security engineers, and compliance officers who need to answer "how do we PROVE a row was not rewritten" without taking the vendor's word for it.
row #1 seq=1 hash=a3f… row #2 seq=2 prev=a3f… hash=b7c… row #3 · tampered seq=3 prev=b7c… hash=??? row #4 seq=4 prev=(old) mismatch row #5 seq=5 prev=e1d… hash=f9a… first_break_at = 3

The four primitives

PrimitiveRole
hashSHA-256 of the canonical form of THIS row, computed from every anchor plus prev_hash.
prev_hashThe hash of the previous row in the chain. Genesis row has an empty prev_hash.
monotonic_seqPer-chain total order counter. Strictly increasing; no gaps; single writer per chain.
canonical_form_idVersion of the canonical serialization used to compute the hash. Bumped on any change to the anchor set so old rows verify with the old rules.

The chain property: any row's hash is a deterministic function of every anchor plus the previous row's hash. Rewrite any earlier row and its hash changes; the next row's prev_hash no longer matches; verify_chain() stops at that row and reports its monotonic_seq.

Worked example · modify a row, watch verify_chain catch it

Reproducible in Python. Create three rows, tamper with the middle one directly in the SQL store, then verify.

from fasten import emit, audit_store, verify_chain, register, Domain, Meta, Severity, RetentionClass

register(Domain("demo"), {"NOTE_WRITTEN": Meta(summary="A note was written.", severity=Severity.INFO, retention_class=RetentionClass.SHORT)})

for i in range(3):
    emit(code="NOTE_WRITTEN", target=f"note_{i}", actor="demo", actor_kind="service", detail={"i": i})

rows = audit_store().query(limit=10); rows.reverse()  # oldest-first
print(verify_chain(rows).ok)                          # → True

# Simulate tampering: change the middle row's detail directly in SQL
audit_store()._conn.execute("UPDATE audit_log SET detail = '{\"i\": 99}' WHERE monotonic_seq = 2")

rows = audit_store().query(limit=10); rows.reverse()
result = verify_chain(rows)
print(result.ok, result.first_break_at, result.reason)
# → False 2 chain hash mismatch at seq=2

The auditor gets three verifiable facts from one call: (1) the chain broke, (2) the exact row (monotonic_seq = 2), (3) the reason (hash mismatch). No log analysis, no forensic reconstruction, no vendor tooling required.

The example uses _conn.execute directly for illustration. Real tampering scenarios include: DBA edits under the covers, SQL injection, backup restore that overwrites recent rows, a compromised database process. All produce the same verify_chain output.

What this catches

What this does NOT catch

Reading more

See the auditor's runbook for reproducible scripts (Python one-liner, reusable script with exit codes, CI job template, break-recovery playbook, and the full threat-model table).

fasten

The audit substrate for distributed systems, and the belief layer for the AI agents on top of them.

Products
fastenmembranefasten fleet
Resources
DocsHow It WorksWhy fastenContact
© 2026 fasten · nerdAppLabs Software Solutions Pvt. Ltd.SDK Apache-2.0 · membrane & fleet commercial