Evidence pack
The format, so you never need our tool.
An audit you can only check with the auditor's own software is not independent evidence. This page is the whole format: four checks, two standard algorithms, and a working verifier that imports nothing of ours.
What you can call today
| Capability | How | State | |
|---|---|---|---|
| Sample evidence packs | GET /demo-reports/<case>.pack.json | live | No account, no key, no rate limit. Signed packs from real audits of mock environments. |
| Our public signing key | GET /.well-known/zerorecall-signing-key.pem | live | Pin it. Without it a valid signature only tells you that somebody signed the file. |
| Signing keys as JWKS | GET /.well-known/jwks.json | live | The same trusted keys in RFC 7517 form, for JOSE tooling. Derived from one key list, never hand-copied. |
| Your own audit report | GET /api/evidence/<caseId> | live | Authenticated and scoped to your account. Returns the HTML report, not the pack. |
| Trigger an audit programmatically | not available | roadmap | There is no endpoint for this today. Runs are started by us or from the CLI. |
| MCP endpoint for agents | not available | roadmap | No MCP server exists in our codebase. Until it does, an agent can fetch and verify packs over plain HTTP. |
The last two rows used to be described elsewhere on this site as shipped. They were not, and we corrected those pages rather than quietly leaving them. If you are sizing an integration, size it against the rows marked live.
The four checks
A pack is a JSON object with a manifest and a signature over it. Four things are recomputed, and each answers a different question.
1 hash chain were findings added, removed or reordered after the run? 2 manifest hash does the signed payload still hash to the recorded value? 3 signature is this an ECDSA P-256 signature over that payload? 4 published key is the signing key OUR key, or just some key? Check 4 is the one people skip. A valid signature only says that somebody signed the file. Anyone can generate a key and sign anything.
Shape
{
"manifest": {
"caseId": "zerorecall-en-ilk",
"target": "who or what was to be erased",
"runAt": "ISO 8601",
"forgetScore": 0-100,
"scopeStatement": "what was and was not examined",
"chainRoot": "sha256 hex of the last link (GENESIS if empty)",
"chain": [ { "index", "prevHash", "finding", "hash" } ],
"stationsSummary":[ { "station", "ran", "score", "probes" } ],
"timeAnchor": { ... }, // optional
"supersedes": { "manifestHash", "reason" }, // optional
"targetProvenance": { // optional, absent in packs before 2026-08-20
"mode": "live" | "mock" | "unspecified",
"note": "what the producer knew about the target at run time"
},
"independentAnchor": { // optional, absent in packs before 2026-08-20,
"kind", "reference", "note" // and not produced yet by anything
}
},
"manifestHash": "sha256 hex of the canonical manifest",
"signature": "base64",
"algorithm": "ecdsa-p256-sha256",
"publicKeyPem": "-----BEGIN PUBLIC KEY----- ...",
"signedAt": "ISO 8601"
}
Optional fields may be absent in older packs. Absence is not a defect:
a verifier reports it as a missing observation, it does not fail the pack.
What "supersedes" means is a published rule, not a hint. See /archive-policy.
"targetProvenance" states what the producer knew about the target when the
run happened: a live endpoint, a mock/demo environment, or unspecified. It
sits inside the manifest, so it is covered by the signature and cannot be
edited after signing without failing the manifest-hash check. A verifier
shows it; it never rejects a pack over it. A "mock" pack is a valid pack
that honestly says what it audited.
"independentAnchor" is reserved. Today all four checks are internal
consistency: the pack agrees with its own signature and with our published
key. Nothing yet ties a pack to a system outside ours. When our transparency
log becomes publicly queryable, this field will carry that reference. Until
then no pack has it, and a verifier should say "not yet available" rather
than imply external anchoring exists.
The Python verifier below needs NO change for these fields: it
canonicalizes the whole manifest object, so any optional field present is
automatically part of the hashed and signed payload.The signing key as a JWKS
If your tooling speaks JOSE, the same trusted keys are published as a JWK Set (RFC 7517) at /.well-known/jwks.json. It is derived from the one key list our engine and browser verifier read, not maintained by hand, so a key rotation cannot update one surface and forget the other. Each entry carries kty (EC), crv (P-256), the public point x/y, use (sig), alg (ES256), and a kid: the first 16 hex characters of the SHA-256 fingerprint of the normalized PEM, the same short fingerprint we publish elsewhere (663787fa1bce77c6 for the current key). If we ever rotate, old keys stay in the set so old packs keep verifying. The PEM at /.well-known/zerorecall-signing-key.pem stays up unchanged; pin whichever form your stack prefers.
Canonicalization, and the one trap
Hashes are taken over a canonical JSON string: object keys sorted, no whitespace, and no ASCII escaping. Every chain link hashes the string prevHash|index|canonical(finding). This rule agrees with RFC 8785 (JCS) for the values that actually occur in packs; strings, integers, arrays and objects, so a JCS library produces the same bytes here.
That last rule costs people a day. Python's json.dumps escapes non-ASCII characters by default, which changes the bytes and therefore the hash. We measured it: a genuinely valid pack containing Turkish characters fails three of the four checks under the default, and passes all four with ensure_ascii=False. If your verifier says a pack is invalid, check this before you accuse anyone of tampering.
# WRONG: default escapes "ç" to "\u00e7" and the hash no longer matches
json.dumps(obj, sort_keys=True, separators=(",", ":"))
# RIGHT
json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)A verifier that uses none of our code
This was run against the live sample pack and the live published key before it was put on this page. It needs only the cryptography package.
curl -O https://www.zerorecall.ai/demo-reports/zerorecall-en-ilk.pack.json curl -O https://www.zerorecall.ai/.well-known/zerorecall-signing-key.pem
import base64, hashlib, json, sys
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.exceptions import InvalidSignature
GENESIS = "0" * 64
def canonical(v):
return json.dumps(v, sort_keys=True, separators=(",", ":"),
ensure_ascii=False) # see the trap above
def sha256(s):
return hashlib.sha256(s.encode("utf-8")).hexdigest()
pack = json.load(open(sys.argv[1]))
m = pack["manifest"]
prev, chain_ok = GENESIS, True
for i, link in enumerate(m["chain"]):
if link["index"] != i or link["prevHash"] != prev:
chain_ok = False; break
h = sha256("%s|%d|%s" % (prev, i, canonical(link["finding"])))
if h != link["hash"]:
chain_ok = False; break
prev = h
payload = canonical(m)
hash_ok = sha256(payload) == pack["manifestHash"]
pub = serialization.load_pem_public_key(pack["publicKeyPem"].encode())
try:
pub.verify(base64.b64decode(pack["signature"]),
payload.encode("utf-8"), ec.ECDSA(hashes.SHA256()))
sig_ok = True
except InvalidSignature:
sig_ok = False
norm = lambda p: "".join(p.split())
key_ok = norm(open(sys.argv[2]).read()) == norm(pack["publicKeyPem"])
for name, v in [("chain", chain_ok), ("manifest hash", hash_ok),
("signature", sig_ok), ("published key", key_ok)]:
print("%-15s %s" % (name, "OK" if v else "FAIL"))
print("=> %s" % ("VALID" if all([chain_ok, hash_ok, sig_ok, key_ok]) else "INVALID"))What a passing pack does not prove
All four checks green means the audit log is intact and was signed by us. It does not mean the data is permanently gone from a model's weights, and it does not by itself prove when the file was made. Some packs carry a time anchor drawn from public randomness beacons, which puts a lower bound on their age. Packs without one say so.
What the seal is for, and the limits we hold ourselves to, are on security and archive policy. You can also check a pack in the browser on the verify page.
The bar for anyone's evidence
Everything above describes our own pack. The same four checks generalize to any vendor's deletion or safety claim, ours or not, once four things are true of the artifact in front of you.
1 tamper-evident structure findings sit in a hash chain, a Merkle
root, or something equivalent, so adding,
removing or reordering one after the run
is detectable
2 a published canonicalization rule
two independent implementations must
produce the same bytes over the same
content, or "the signature checks out"
is only testable by the vendor itself
3 a signature over the canonical payload
not a checksum of the file. A checksum
proves the bytes did not rot in transit;
a signature proves who committed to them
4 a publicly published, pinnable key
independent of the tool doing the
checking. Without it, check 3 only proves
that somebody signed the fileNone of this requires our software, or the vendor's. If you are holding a benchmark page, a trust center export, or another vendor's signed pack, the assess tool walks the same four questions against it and places it on the Proof Ladder, whether or not it came from us.
Integrating
If you are building against this and something in the spec is wrong, ambiguous, or missing, write to hello@zerorecall.ai. A format only one implementation can read is not a format.