Skip to main content
Every log tool asks you to trust their dashboard that the record is real. Wardin signs each receipt, so you can prove the chain is authentic yourself — offline, with no Wardin account, and without trusting us. That’s the whole point of the evidence layer: the proof doesn’t depend on the vendor. The bundle format and the exact verification algorithm are documented below, so you — or an auditor — can reimplement the check in any language, depending on nothing from us. wardin-verify is a reference implementation of that spec: a standalone Go tool that recomputes every hash and checks every ED25519 signature locally.
The wardin-verify binary and its source are being prepared for public release. Until then, the format + algorithm below are the source of truth — a faithful reimplementation verifies real bundles independently. (Wardin builds it from the apps/gateway/cmd/verifier package via task build:verifier.)

Verify a bundle

# Export a bundle (any tenant admin key), then verify it — fully offline
curl "https://api.wardin.ai/v1/compliance/evidence-bundle?range=30d" \
  -H "Authorization: Bearer wardin_sk_ADMIN_KEY" > bundle.json

wardin-verify bundle.json
# ✓ INTEGRITY VERIFIED — all N receipts are untampered and signed by their declared keys.
  • wardin-verify --json bundle.json — machine-readable report.
  • cat bundle.json | wardin-verify — read from stdin.
  • Exit code 0 = every receipt verified (and, with --keys, every key pinned); 1 = a signature, hash, chain link, or seq gap failed; 2 = the bundle could not be read/parsed.

Integrity vs. authenticity — pin the keys

A clean result proves integrity: the chain is internally consistent, untampered, and each receipt is signed by the key the bundle declares for it. It does not by itself prove authenticity — the bundle carries its own key registry, so a forged bundle could be internally consistent under a fresh keypair. To close that gap, wardin-verify prints each signing key’s fingerprint (sha256(publicKey) truncated). Pin them against Wardin’s published key registry, or fail the run unless they match:
wardin-verify --keys=faf01f61caa36c4d,<other-fp> bundle.json
# ✗ KEY PIN FAILED — signing key(s) NOT in your pinned set: [...]

The Evidence Bundle format

A bundle is a single JSON object. Fields (camelCase):
FieldDescription
bundleVersionFormat version ("1").
generatedAtISO-8601 timestamp of export.
tenantIdThe tenant the receipts belong to.
range{ label, days, fromSeq, toSeq, fromTs, toTs, receiptCount } — the exported segment.
anchor{ type, prevHash, note } — the starting prev_hash the chain is verified from (see below).
signingKeys[{ keyId, publicKey, validFrom, validTo, revokedAt }] — every key the receipts reference. publicKey is a base64 raw 32-byte ED25519 key. Multiple entries cover key rotations.
unresolvedKeyIdskey_ids referenced by receipts with no resolvable public key — their receipts verify as unverifiable. Empty when complete.
packsCompliance packs in force (framework, version, packHash, …) — informational.
receiptsThe signed receipt rows (below), ordered by seq.
disclaimerHonesty posture — surfaced verbatim.
Each receipt row carries exactly the fields verification needs:
FieldNotes
seqPer-tenant monotonic sequence (chain order).
requestId, tenantId, actor, provider, modelCanonical string fields.
promptTokens, completionTokensUnsigned integers.
costUsdFloat, formatted to 6 decimal places in the canonical form.
checksOrdered [{ name, result }] — the enforcement decisions.
tsMillisReceipt timestamp as epoch milliseconds (UTC).
prevHashPrevious receipt’s thisHash (hex); empty string for genesis.
thisHashThis receipt’s chain hash (hex).
signatureBase64 ED25519 signature over thisHash.
keyIdWhich signingKeys entry verifies this row.
packVersionCompliance pack version in force (unsigned side channel).

The verification algorithm

For each receipt, in seq order, recompute and check:
  1. Canonical bytes — serialize the signed fields in this exact fixed order, each as key=value\n (no map iteration, no whitespace):
    request_id=<requestId>\n
    tenant_id=<tenantId>\n
    actor=<actor>\n
    provider=<provider>\n
    model=<model>\n
    prompt_tokens=<promptTokens>\n
    completion_tokens=<completionTokens>\n
    cost_usd=<costUsd, formatted to 6 decimals>\n
    checks=<name=result joined by ",", in order>\n
    ts=<tsMillis>\n
    
  2. Chain hashthisHash == sha256( canonicalBytes ‖ prevHashBytes ), where prevHashBytes is the raw (hex-decoded) previous hash — the prior receipt’s thisHash, or the bundle anchor.prevHash for the first row (empty bytes for true genesis). A mismatch means a field was tampered.
  3. Chain linkage — the receipt’s prevHash must equal the running previous hash (the anchor for the first row, the prior thisHash after). And seq must be contiguous (prevSeq + 1). A break here is a fork or a gap.
  4. Signature — ED25519-verify signature over the thisHash bytes, using the publicKey from the signingKeys entry whose keyId matches. Because keys resolve from the bundle, verification survives key rotation with no network call.
  5. Tenant — every receipt’s tenantId must equal the bundle’s tenantId.
Any receipt that fails any step is reported with its seq; the tool exits non-zero. The canonical serialization is byte-identical to what the gateway signs, so a bundle that verifies here is authentic byte-for-byte (subject to the key-pinning note above).
Trust anchor. Verification trusts the bundle’s anchor.prevHash as the starting link. A full audit should confirm the segment begins at true genesis (seq 1) or cross-check the anchor against an out-of-band record. A future release publishes a signed Merkle root as the anchor.

Why this matters

An auditor can take a bundle, run wardin-verify (or their own reimplementation from the spec above), and establish — with no access to Wardin, no account, and no trust in us — that every governed request’s signed record is intact and authentic. No log/observability tool can offer that, because none sign their records. See The Evidence Layer and Signed Receipts.