HMAC audit chain
Chain construction
Section titled “Chain construction”Every audit row stores:
id, ts, actor_user_id, project_id, action, target_type, target_id, details,row_hmac, prev_row_hmacWhere:
row_hmac = HMAC-SHA256( Z4J_AUDIT_SECRET, canonical_json({ id, ts, actor_user_id, project_id, action, target_type, target_id, details, prev_row_hmac }))canonical_json is sorted-keys, no whitespace, UTF-8. prev_row_hmac is the row_hmac of the previous row in id order. Genesis row has prev_row_hmac = null.
Properties
Section titled “Properties”- Tamper-evident - change any field →
row_hmacno longer verifies. - Insertion-evident - inserting a row in the middle breaks the next row’s
prev_row_hmaclink. - Deletion-evident - deleting a row breaks the chain at the gap.
Not prevented:
- Append-only suffix replacement - someone with
Z4J_AUDIT_SECRETand DB access can truncate trailing rows and re-sign. Mitigation: periodically export row_hmac externally (e.g. to an append-only S3 bucket or a second DB).
Verification
Section titled “Verification”z4j-brain audit verifyWalks the full log; returns (ok, rows_verified, first_broken_id). Runs:
- On demand (CLI / API / dashboard button).
- Optionally on schedule: set
Z4J_AUDIT_VERIFY_INTERVAL=3600to run hourly.
Secret rotation
Section titled “Secret rotation”Rotating Z4J_AUDIT_SECRET breaks verification for old rows. Strategies:
- Dual-key window - support both keys during rotation (planned post-v1.1).
- Snapshot & start fresh - export old rows with their HMACs (still verifiable using the old secret externally), rotate, begin a new chain.
Most operators don’t rotate the audit secret; the sensitive rotation targets are Z4J_SECRET and Z4J_SESSION_SECRET.
Export integrity
Section titled “Export integrity”CSV / NDJSON export includes row_hmac + prev_row_hmac. Downstream verification requires Z4J_AUDIT_SECRET (share it with the verifier out-of-band).
Performance
Section titled “Performance”HMAC-SHA256 on a small canonical payload is microseconds; the chain does not meaningfully slow writes. Verification of a million rows takes ~10 seconds on modest hardware.