Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.knoxcall.com/llms.txt

Use this file to discover all available pages before exploring further.

SHA-256 Hash Chain — Critical Audit Chain

For sensitive operations — BYOK KMS lifecycle, secret store migrations, and tenant impersonation — each write goes into a separate, append-only table (audit_log_critical) that is distinct from the standard audit_logs table. The critical chain is protected by:
  • Application-layer SHA-256 hash chain — each row commits to the prior row via prev_hash (32 bytes) and row_hash = SHA-256(canonical_bytes_of_this_row). Tampering with any row changes its row_hash, which breaks every subsequent prev_hash link. A verifier can detect the exact row that was altered.
  • Append-only enforcement — database triggers prevent UPDATE and DELETE at the Postgres level. There is no retention pruning on this table; records exist indefinitely.
  • Serialized writes — an advisory lock (pg_advisory_xact_lock) on every write ensures monotonic, gap-free sequence_number values. A gap in sequence numbers indicates a deletion attempt.

Critical chain row schema

FieldTypeDescription
sequence_numberBIGINTMonotonically increasing; gaps indicate tampering
prev_hashBYTEA (32)SHA-256 of the previous row’s canonical bytes; all-zero for the genesis row
row_hashBYTEA (32)SHA-256 of this row’s canonical bytes
atTIMESTAMPTZWall-clock timestamp of the action
tenant_idUUIDTenant subject of the action (nullable for operator-level events)
actor_idUUIDUser who performed the action (nullable for system-triggered events)
actionTEXTOne of the 15 action values listed above
resource_typeTEXTOne of the 7 resource types listed above
resource_idTEXTUUID or other identifier of the affected resource
detailsJSONBAction-specific context. Never contains plaintext secret values.

Canonical serialization

The row_hash is computed over a deterministic length-prefixed byte sequence:
sequence_number (8 bytes, big-endian)
|| prev_hash    (32 bytes, verbatim)
|| len(at_iso) + at_iso + sentinel
|| len(tenant_id) + tenant_id + sentinel
|| len(actor_id) + actor_id + sentinel
|| len(action) + action + sentinel
|| len(resource_type) + resource_type + sentinel
|| len(resource_id) + resource_id + sentinel
|| len(details_canonical_json) + details_canonical_json + sentinel
Where details_canonical_json is JSON with keys recursively sorted so the hash is deterministic regardless of insertion order. The sentinel byte distinguishes null (\x00) from empty string (\x01). The authoritative implementation is computeRowHash() in src/audit/critical.ts. An external verifier can re-derive any row’s hash from raw DB values and this spec.

Verifying the chain

# Verify the critical chain from sequence 1 to the head
curl https://api.knoxcall.com/admin/audit-logs/verify-chain \
  -H "Authorization: Bearer $KC_ADMIN_JWT"
# → { "ok": true, "rows_checked": 142, "first_break_at_sequence": null, "first_break_reason": null }

# Verify a specific range
curl "https://api.knoxcall.com/admin/audit-logs/verify-chain?from=1&to=100" \
  -H "Authorization: Bearer $KC_ADMIN_JWT"
A failed verification response identifies the exact break point:
{
  "ok": false,
  "rows_checked": 71,
  "first_break_at_sequence": "72",
  "first_break_reason": "row_hash mismatch at sequence_number=72 — row contents were tampered with after insertion"
}
The verification runs nightly automatically. Call the endpoint on demand before a compliance audit or after any suspected incident.