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.
| Primitive | Role |
|---|---|
hash | SHA-256 of the canonical form of THIS row, computed from every anchor plus prev_hash. |
prev_hash | The hash of the previous row in the chain. Genesis row has an empty prev_hash. |
monotonic_seq | Per-chain total order counter. Strictly increasing; no gaps; single writer per chain. |
canonical_form_id | Version 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.
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.
monotonic_seq continuity).prev_hash won't match the surrounding chain).hash of last-verified boundary changes).emit() a decision, no chain integrity check can surface that gap. Audit-code review + registered-code enforcement is the compensating control.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).
The audit substrate for distributed systems, and the belief layer for the AI agents on top of them.