> ## 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

> Tamper-evident append-only audit chain for sensitive operations including BYOK KMS lifecycle, secret store migrations, and tenant impersonation

## 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

| Field             | Type        | Description                                                                 |
| ----------------- | ----------- | --------------------------------------------------------------------------- |
| `sequence_number` | BIGINT      | Monotonically increasing; gaps indicate tampering                           |
| `prev_hash`       | BYTEA (32)  | SHA-256 of the previous row's canonical bytes; all-zero for the genesis row |
| `row_hash`        | BYTEA (32)  | SHA-256 of this row's canonical bytes                                       |
| `at`              | TIMESTAMPTZ | Wall-clock timestamp of the action                                          |
| `tenant_id`       | UUID        | Tenant subject of the action (nullable for operator-level events)           |
| `actor_id`        | UUID        | User who performed the action (nullable for system-triggered events)        |
| `action`          | TEXT        | One of the 15 action values listed above                                    |
| `resource_type`   | TEXT        | One of the 7 resource types listed above                                    |
| `resource_id`     | TEXT        | UUID or other identifier of the affected resource                           |
| `details`         | JSONB       | Action-specific context. Never contains plaintext secret values.            |

### Canonical serialization

The `row_hash` is computed over a deterministic length-prefixed byte sequence:

```text theme={"dark"}
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 chains

The critical chain (`audit_log_critical`, described above) is verified by `verifyChainSegment()` in `src/audit/critical.ts`, which walks the chain in `sequence_number` order, recomputes each `row_hash`, and stops at the first break. It runs nightly via the `verify-audit-chain` cron job. There is currently **no HTTP endpoint** that exposes the critical-chain verifier.

A separate verifier covers the standard `audit_logs` table. It is exposed through the dashboard admin API:

```bash theme={"dark"}
# Verify the current tenant's standard audit_logs hash chain.
# /admin/* lives on the dashboard host (admin.knoxcall.com / any
# knoxcall.com host), gated by your admin session JWT + X-Tenant-ID.
curl https://admin.knoxcall.com/admin/audit-logs/verify-chain \
  -H "Authorization: Bearer $KC_SESSION_JWT" \
  -H "X-Tenant-ID: $KC_TENANT_ID"
```

The endpoint scans the entire tenant chain (every `audit_logs` row with a `row_hash`, ordered by `id`) and returns the verification result verbatim:

```json theme={"dark"}
{
  "tenantId": "00000000-0000-0000-0000-000000000000",
  "rowsChecked": 142,
  "tampered": 0,
  "brokenChainAt": [],
  "invalidHashAt": [],
  "valid": true
}
```

A failed verification sets `valid` to `false` and lists the offending row `id`s — `brokenChainAt` for rows whose `prev_hash` does not match the prior row's `row_hash`, and `invalidHashAt` for rows whose stored `row_hash` does not match the recomputed value:

```json theme={"dark"}
{
  "tenantId": "00000000-0000-0000-0000-000000000000",
  "rowsChecked": 142,
  "tampered": 1,
  "brokenChainAt": [],
  "invalidHashAt": [72],
  "valid": false
}
```

Note that this endpoint verifies the standard `audit_logs` chain, whose `row_hash` is `SHA-256(prev_hash + tenant_id + user_id + action + resource_type + resource_id + JSON.stringify(details) + ip_address)` — a plain string concatenation, distinct from the length-prefixed canonical serialization of the critical chain above. The endpoint takes no range parameters; it always scans the whole tenant chain.
