Every verification receipt Sign signs — and every admin action on the applications queue — appends a hash-chained, Ed25519-signed entry to this log. The chain is tamper-evident: insertion, deletion, or modification of any past entry breaks validation. You don't have to trust us. Fetch /v1/audit/log, re-derive the chain, and verify the Ed25519 signatures against the published authority pubkey.
The head hash below changes on every new entry — receipt issuance, exchange registration, or application review. Anchor it wherever you trust (Twitter, on-chain, internal Slack) to detect silent log rewrites.
Fetch once at deploy time, save in your config, use for every receipt verification. Rotated quarterly — old keys remain valid for receipts signed under them.
Every entry is the SHA-256 of:
seq || timestamp_unix || actor || action || target || prev_hash || canonical_json(payload)
Each field is length-prefixed (big-endian uint64) so domain separation is unambiguous — a 4-byte actor can't be confused with 4 bytes of a longer field. The hash is then Ed25519-signed with context sign_audit_entry. prev_hash is itself SHA-256 of the previous entry's (hash || signature), so rewriting any past entry breaks every subsequent entry.
| action | when emitted |
|---|---|
| genesis | log seed, one per fresh deployment |
| receipt_issue | every POST /v1/verify |
| exchange_register | approval of an application or direct admin call |
| application_submit | public POST /v1/applications |
| application_approve | admin approves a pending application |
| application_reject | admin rejects a pending application |
| authority_rotate | quarterly key rotation (signed by OLD key to anchor continuity) |
{
"exchange_id": "exch_...",
"status": "match" | "mismatch",
"risk_level": "safe" | "warning" | "critical",
"raw_tx_hash": "",
"mismatches":
}
Note: the raw transaction is never included in the payload — only its SHA-256. Exchanges' private tx bytes stay private while still being provably tied to the receipt.
The @zoza/sign npm package exports a VerifyEntries() helper matching the Go reference. Offline, zero Sign dependency:
import { SignClient, verifyReceipt } from '@zoza/sign';
const sign = new SignClient();
const log = await fetch('https://sign-api.zoza.world/v1/audit/log').then(r => r.json());
const pubkey = (await sign.authority()).public_key;
// Each entry has: seq, timestamp, actor, action, target, payload, prev_hash, hash, signature.
// Re-derive hash from canonical bytes, check Ed25519 sig with pubkey, walk prev_hash chain.
// See products/zoza-sign/audit.go::VerifyEntries for the reference Go impl.
If you want to skip the code entirely: hit /v1/audit/verify — Sign's own server re-validates the chain and returns {valid: true|false, reason}. Run this from your oncall dashboard every hour; page if it flips false.
All three gaps are shared with Shield's audit log design and are documented there identically.