Chain integrity

Every mutation in Sukrit Nidhi writes a row to the audit log. Those rows form a hash chain: each row records the hash of the previous row, so an attacker who modifies one row in the middle must also re-hash every row after it. Changing a row at the database level without re-hashing is detectable by anyone with a read copy of the chain.

How the hash is computed

For each event we compute:

record_hash = SHA-256( prev_hash || canonical_json(event) )

where canonical_json serialises the event fields deterministically (sorted keys, compact separators, UTF-8). The first event for a tenant uses a fixed genesis string as its prev_hash.

Verification workflow

Load eventsordered by ts_utc
Recompute hashprev ⊕ canonical
Comparerecomputed vs stored
OK or problemsper-row diff

Verification is used in three places: on the Centre dashboard (live badge), in the daily scheduled task (audit.verify_all_chains), and in the offline verifier CLI shipped in the audit bundle.

What “red” means

If the chain is reported as failed, the verifier tells you exactly which event diverges, what the expected hash is, and what the stored hash is. Two kinds of problems surface:

  • Hash mismatch on a row — the row’s fields don’t match its stored record hash. Someone changed the row after it was written.
  • Broken prev_hash — a row’s prev_hash doesn’t equal the previous row’s record hash. Rows were inserted out of order, or one was deleted.

Chain anchors

The scheduled task audit.anchor_chain_heads writes a daily anchor per active tenant:

  • Timestamp, event count, chain head at the moment of the snapshot.
  • Ed25519 signature over the canonical anchor body.
  • Stored in the append-only chain_anchor table.

Anchors give you a second, independently-signed timeline against which any later replay can be cross-checked. If an attacker later manipulates an event dated before an anchor, the replay won’t reproduce that anchor’s stored head — the tamper window is bounded to the gap between anchors.

Anchors are write-only to the scheduled task. Sukrit Nidhi’s append-only guard on ChainAnchor.save() prevents overwrite, so the model is a single source of truth.

If you get a real red-light event

Investigate, don’t close the case by unlocking things:

  1. Snapshot everything first. Take a full database dump right now, before any investigation. Chain problems disappear if someone panics and fixes rows.
  2. Read the problem report. The verifier lists the first five divergent rows with both hashes. Usually the diff points at a single entry; check when it was written and by whom in the audit log.
  3. Compare with the previous anchor. If the divergent event is dated before the most recent anchor, and the anchor’s head doesn’t replay, you have a tamper event between the anchor and now.
  4. Escalate. Chain divergence is a security incident, not an operational one. Your audit policy should mandate a security-officer review.