Skip to main content

Tamper-evident chain design

Part of the Universal Audit Log Specification. The cryptographic hash chain across the audit.audit_entries table — each row's hash is H(prev_hash || canonical_payload), so any tampered or out-of-order row breaks the chain at the next verification scan. This is what makes the audit log forensically reliable.

11. Tamper-Evident Chain Design

11.1 Overview

Tamper-evident chaining provides cryptographic proof that audit entries have not been modified, reordered, or deleted after creation. The chain is per-tenant to avoid global serialisation bottlenecks.

11.2 Hash Algorithm

SHA-256 SHALL be used for all chain hashes.

11.3 Hash Computation

entry_hash = SHA256(
previous_hash ||
tenant_id ||
actor_id ||
action ||
resource_type ||
resource_id ||
SHA256(changes) ||
created_at
)

The changes field is hashed separately to bound the input size.

11.4 Chain Head Management

An audit.chain_heads table tracks the latest hash per tenant:

CREATE TABLE audit.chain_heads (
tenant_id UUID PRIMARY KEY, last_hash TEXT NOT NULL,
last_entry_id UUID NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Concurrency: SELECT ... FOR UPDATE on the tenant's chain_heads row serialises hash computation per tenant. Alternative for high-throughput tenants: assign a per-tenant sequence number at write time, then compute hashes in a background job that processes entries in sequence order (decouples chaining from the write path).

11.5 Integrity Verification

A nightly job SHALL walk each tenant's chain: load entries in created_at order, recompute each entry_hash, and compare against the stored value. On mismatch, alert immediately and record the break point.

11.6 Chain Recovery After PITR

After point-in-time recovery, identify the last verified hash ("known-good" point), re-seal all subsequent entries by recomputing hashes forward, and record the re-seal event in the audit log.