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.2Every 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:
business_register — a new business is provisioned (direct-register or via application approval). Logs name, domain, channels, public-key fingerprint — never the private key or API key.business_rotate — a business's Ed25519 keypair is rotated. Old public key marked superseded; new public key fingerprint appended.business_revoke — a business is deactivated. Consumer registry flags the business as inactive; any signatures from that business dated after the revocation timestamp are marked untrusted.business_verify_domain — the `Verified` flag is flipped after DNS TXT check confirms domain ownership.application_submit — a B2B apply-for-key form submission lands in the queue.application_approve — a pending application is approved, a new business + key are issued.application_reject — a pending application is rejected, with reason.admin_token_use — any call to an /admin/* endpoint (records actor + action, never payload).registry_snapshot — daily: a Merkle-root of the full public-key registry is appended. This lets consumers detect silent registry mutations between snapshots.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.
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.
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:
registry_snapshot, reconstruct the Merkle-root from the then-active business set and confirm match.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.
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.
POST /v1/verify server-side, the access log records the call (30-day rolling) but never the content or identities involved.Last updated 2026-04-17. © 2026 Zoza. Source code copyright LD-16949/2026-CO.