Audit log

Hash-chained, append-only record of every business registration, key rotation, admin action, and application decision. Verifiable in your browser with no API call required.

🟡 In progress · public write-through expected v0.2

What the log records

Every action on Verify that could materially affect which signatures a consumer ends up trusting gets appended to the audit chain. The registry is the heart of Verify's trust story — a silent mutation of the registry is indistinguishable from a compromise, so every mutation is logged with a cryptographic predecessor hash. Entries include:

Hash-chain format

Each entry includes the SHA-256 of the previous entry concatenated with its own content. Flipping any byte of any past entry breaks every entry after it. A daily Ed25519 signature over the current head hash is the canonical proof of state — embedded in our warrant canary and in the /v1/audit/head endpoint.

entry_n = {
  "seq":        n,
  "ts":         "2026-04-17T10:22:15Z",
  "actor":      "admin:sajid" | "business:biz_abc" | "public",
  "action":     "business_register",
  "target":     "biz_abc123",
  "detail":     { "name": "SBI Bank", "domain": "sbi.co.in",
                  "channels": ["sms","email"],
                  "pubkey_fpr": "ed25519:a4f2...e891" },
  "prev_hash":  "sha256(entry_n-1)",
  "self_hash":  "sha256(this entry without self_hash field)"
}

head_signature = Ed25519(root_key, self_hash_of_most_recent_entry)

The root Ed25519 key fingerprint is pinned in products/zoza-verify/OPS.md, echoed into the warrant canary, and served by GET /v1/audit/pubkey — three separate surfaces. An attacker who silently replaces the key on one surface can't replace it everywhere at once without tripping a canary.

Why registry snapshots matter

Verify's trust model depends on the consumer-side registry being the same set of public keys the business registered. If an attacker compromises the Verify DB and silently adds a malicious business called "SBI Bank" with their own public key, the attacker can sign fake SMS that verify as SBI.

The daily registry_snapshot entry puts a Merkle-root of the entire active registry into the hash chain. A third-party watcher who stores historical Merkle-roots can challenge any claim: "you claim SBI Bank's public key was X at time T. Here's the snapshot's Merkle-root at time T. Prove it using a Merkle path." If Verify can't produce a consistent path, the registry has been tampered with between snapshots.

How to verify (when v0.2 ships)

Call GET /v1/audit/head for the current head hash and signature. Then call GET /v1/audit/entries?from=0 (paginated) to pull the full chain. Our in-browser verifier at this page will:

  1. Fetch every entry in the chain.
  2. Recompute each entry's self_hash from its content.
  3. Confirm each entry's prev_hash matches the previous entry's self_hash.
  4. Verify the head signature against the pinned Ed25519 public key.
  5. For each registry_snapshot, reconstruct the Merkle-root from the then-active business set and confirm match.
  6. Show green if all five hold. One byte different anywhere — red.

All of this runs in your browser. No API call returns "yes this is valid" — only the raw entries. You do the checking. That's the whole point.

⚠ Current status (v0.1)

As of 2026-04-17, Verify logs admin actions to stderr and to the activity_logs table of the admin DB. The public write-through to an append-only hash-chained log, Ed25519 head signatures, daily registry-snapshot Merkle-roots, and the in-browser verification UI are implemented for Shield and planned for Verify v0.2. Until then, the log is not tamper-evident on the public side — the code path exists and logs to a local file, but the public API and the chain-verification UI ship with v0.2.

What the log does not record

Last updated 2026-04-17. © 2026 Zoza. Source code copyright LD-16949/2026-CO.