← back to Sign overview
Public audit log

Verify every Sign receipt ever issued.

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.

Live chain head

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.

head_hash (sign-api.zoza.world)
fetching…
entry count
fetching…
fetched at (server time)
fetching…
download full log → server-side verify →

Authority pubkey (pin this)

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.

algorithm
fetching…
public_key (Ed25519, 64-hex)
fetching…
domain separation context
fetching…

How the chain works

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.

Actions recorded

actionwhen emitted
genesislog seed, one per fresh deployment
receipt_issueevery POST /v1/verify
exchange_registerapproval of an application or direct admin call
application_submitpublic POST /v1/applications
application_approveadmin approves a pending application
application_rejectadmin rejects a pending application
authority_rotatequarterly key rotation (signed by OLD key to anchor continuity)

Payload content (receipt_issue)

{
  "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.

Verify the chain yourself

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.

What this does NOT defend against

All three gaps are shared with Shield's audit log design and are documented there identically.