Status: Implemented. The verifier side shipped in CLI 1.6.0: the
bundle command, the standalone-command rules and the online witness check
(section 10.2). The platform side (section 10.1) shipped on 2026-09-30: the
bundle endpoint signs manifest.json and embeds CLI 1.6.0, and the README and
verify.command run bundle. An export whose scope carries no org still has
no manifest and reads as pre-v1 (section 8.2), as do all bundles issued
before that date.
Owner: CTO
Last reviewed: 2026-09-30
"Self-contained exports" appears in product and strategy copy, but until now it has not been defined precisely enough to test. This document is the contract that makes it testable. It:
- Defines the Case File ZIP that
GET /api/v1/audit/report/bundlereturns, and the format of each artifact inside it (sections 3 to 6). - Defines "self-contained" as a checkable property, not an adjective (section 2).
- Defines the ordered, fail-closed procedures a third party runs against a bundle, and what each one establishes (sections 7 and 9).
- Names the boundary: which checks need only public tools, which need a secret, which need the network, and which no third party can run at all.
Companion documents, all in docs/forensics/:
attestation-format.md: the DSSE session-attestation wire formattrust-model.md: what a valid attestation proves and does not proveretention-tombstones-in-exports.md: theretention/tombstones.jsoncontractevidence-anchor-reference-notes.md: the checkpoint and inclusion-proof formatsevidence-anchor-public-feed.md: the public checkpoint feed and mirrorkey-rotation.md: signer key versions and the JWKS
Every audit row carries two HMAC chains (see trust-model.md):
| Field pair | Chain | Key | Who holds the key |
|---|---|---|---|
entry_hash / prev_hash |
platform chain | AUDIT_HMAC_KEY |
AI Identity only (runtime configuration, never in the database) |
entry_hash_org / prev_hash_org |
per-org chain | the org's forensic_verify_key (plus retired epoch keys) |
the org's admins, via the dashboard |
The evidence-anchor Merkle leaf is entry_hash, the platform-chain hash.
Nobody outside AI Identity can recompute it from a row's content. This one fact
shapes the whole spec: an inclusion proof shows that a 32-byte value was
committed under a signed root, and on its own says nothing about what the row
beside it contains. Section 9.3 states the consequence; the signed manifest
(section 4) is how v1 binds content to something a stranger can check.
Definition. A Case File bundle is self-contained iff a verifier holding only the bundle file and public tools can establish every Tier P claim in section 9.1, with zero network calls and zero secrets.
Public tools means: a Python 3.9+ interpreter, the cryptography package
from PyPI (needed for ECDSA verification), and the published JWKS
(/.well-known/ai-identity-public-keys.json) or a copy saved at collection
time (section 7.6). "Public" does not mean "dependency-free".
Three tiers. The bundle carries independent integrity layers with different trust inputs. The tiers are reported separately, never blended (section 7.5):
| Tier | Name | Checks | Inputs | Network | Secrets |
|---|---|---|---|---|---|
| P | Public | Manifest signature and inventory; checkpoint signatures; inclusion proofs bound to exported rows; as-issued org-slice structure; tombstone accounting | Bundle + JWKS | None | None |
| K | Key-holder | Per-row HMAC recomputation on the org chain; report signature | Bundle + org verify key(s) | None | forensic_verify_key and retired epoch keys |
| W | Witness (optional) | Every checkpoint the bundle relies on appears, byte-identical, in the public feed or mirror | Bundle + network | Public feed or mirror | None |
Each tier fixes the evidence at a different moment:
- Tier P fixes the bundle at issuance: every file is byte-identical to
what the platform signed at
generated_at. It also fixes anchored hashes at anchoring time. - Tier K fixes each row's content at write time, for rows written under a key the holder has.
- Tier W shows the checkpoints were public, so the holder was not shown a private fork.
Tier K is key-holder verifiable, not publicly verifiable: the HMAC verify key is also a forge key. "Self-contained" therefore always means Tier P. Tier W is a network call to AI Identity infrastructure (the feed) or its GitHub organization (the mirror), so it sits outside the definition above: it strengthens a result and is never required for one.
The disappearance test. Suppose AI Identity (the company, the API, the JWKS endpoint, the dashboard) vanished tomorrow. Then:
- A bundle plus a saved JWKS still verifies at Tier P, in full. A stranger can establish that the bundle is exactly as issued, that its anchored hashes were committed under signed roots, and that the as-issued org slice is structurally complete with every pruned row accounted for.
- Tier K still verifies iff the org verify key(s) were saved with the bundle, and only for rows written under an org key. Rows written under the platform key before the org key existed are beyond Tier K permanently.
- Tier W degrades to the GitHub mirror, if it survives, or to the Tier W result recorded at collection time (section 7.6).
- What does not survive: platform-chain verification, which is
recomputing
entry_hash. After disappearance nobody can run it, so the protectiontrust-model.mddescribes ("a re-forged org chain still fails platform-chain verification") is gone. The signed manifest covers the case it covered after issuance (section 9.3).
GET /api/v1/audit/report/bundle returns a ZIP named
ai-identity-case-file-<short>-<date>.zip with this layout:
ai-identity-case-file-<short>-<date>.zip
manifest.json signed manifest (section 4)
case-file-<short>-<date>.json forensic report (section 5.1)
ai_identity_verify.py embedded verifier (section 5.5)
verify.command macOS runner, mode 0755 (section 5.6)
README.md human verification guide (section 5.6)
evidence-anchor/ present iff >= 1 exported event is anchored
checkpoints.json signed Merkle checkpoints (section 5.2)
inclusion-proofs.json per-event proofs and pending list (section 5.3)
retention/ present iff >= 1 row in the id range was pruned
tombstones.json tombstones, receipts, cited checkpoints (section 5.4)
Layout rules:
- The verifier MUST be embedded. A bundle without its verifier is
unverifiable by the people most likely to receive it. The builder MUST fail
loudly (HTTP 500, never a silent omission) if
cli/ai_identity_verify.pyis unavailable at build time. This is current behavior; the spec locks it in. - The manifest MUST be signed. The builder MUST fail loudly (HTTP 500) if the manifest cannot be signed. An unsigned v1 bundle is never issued.
- Every file is inventoried. Every file in the ZIP except
manifest.jsonitself appears exactly once inmanifest.files. An extra file or a missing file is a REJECT. - Empty optional folders MUST NOT ship. Absence is meaningful: no
retention/folder means no row in[first_audit_id, last_audit_id]has a tombstone, and noevidence-anchor/folder means no exported event is anchored yet. In v1 the absence is authenticated, because the signed inventory (andmanifest.counts) says what should be there. Deleting a folder from a v1 bundle is a REJECT, not a downgrade. - Coverage MUST be explicit. When
evidence-anchor/is present, every exported event id appears in exactly one ofproofs[].audit_idorpending. A verifier MUST NOT infer completeness from silence. - Filenames are fixed except the case file. A v1 verifier locates it by
its manifest
role(report); a pre-v1 verifier locates it by the globcase-file-*.json, asverify.commanddoes. Never by a hardcoded name. - ZIP hygiene. Entry paths MUST be relative, MUST NOT contain
..segments, and MUST NOT be symlinks or duplicates. A verifier MUST reject a ZIP that violates any of these before extracting anything. - Extracted directories. A verifier MAY accept the directory a bundle was
extracted into. The same inventory rule applies, except that desktop
metadata files named
.DS_Storeare ignored, because opening the folder is enough to create one. Symlinks in the directory are a REJECT.
manifest.json is a DSSE envelope, in the same shape as a checkpoint or
attestation envelope:
{
"payloadType": "application/vnd.ai-identity.case-file-manifest+json",
"payload": "<base64(JCS-canonical manifest JSON)>",
"signatures": [{"keyid": "<kms-key-version-path>", "sig": "<base64(DER ECDSA-P256)>"}]
}The decoded payload:
{
"schema_version": 1,
"format": "ai-identity-case-file/v1",
"bundle_id": "9f2c...",
"generated_at": "2026-09-29T15:04:11Z",
"generator": "ai-identity-platform/0.5.0",
"signer_key_id": "<kms-key-version-path>",
"org_id": "0f2c...",
"scope": {"type": "org", "org_id": "0f2c..."},
"range": {"first_audit_id": 48151000, "last_audit_id": 48155999},
"counts": {"events": 4990, "anchored": 4800, "pending": 190, "tombstoned": 10},
"verifier_version": "x.y.z",
"files": [
{"path": "case-file-0f2c-2026-09-29.json", "sha256": "...", "role": "report"},
{"path": "ai_identity_verify.py", "sha256": "...", "role": "verifier"},
{"path": "verify.command", "sha256": "...", "role": "runner"},
{"path": "README.md", "sha256": "...", "role": "readme"},
{"path": "evidence-anchor/checkpoints.json", "sha256": "...", "role": "checkpoints"},
{"path": "evidence-anchor/inclusion-proofs.json", "sha256": "...", "role": "proofs"},
{"path": "retention/tombstones.json", "sha256": "...", "role": "tombstones"}
]
}Rules:
- Signature. ECDSA P-256 over SHA-256, DER-encoded, base64 in the envelope,
over
PAE(payloadType, payload). It is made by a KMS forensic-signer key published in the JWKS, resolved bykeyid, and there is exactly one signature. PAE binds thepayloadType, so a manifest signature cannot be replayed as a checkpoint or attestation signature, even from the same key. - Verify the bytes received. The signature covers the base64-decoded
payloadbytes exactly as shipped. A verifier MUST NOT re-canonicalize before verifying, and MUST NOT act on any payload field before the signature verifies. - Versions. A verifier MUST reject
schema_versionother than1and anyformatit does not understand. It must fail loudly, never accept silently. - Inventory.
files[].sha256is the lowercase hex SHA-256 of the file's uncompressed bytes. Exactly one file has rolereportand exactly one has roleverifier. Rolescheckpoints,proofsandtombstonesare present iff their folders are. - Cross-binding. The manifest's
org_id,scopeandrangeMUST equal the case file's. The case file'srangeis[min(id), max(id)]over its events, ornullwhen there are none. Every checkpoint payload'sorg_idMUST equal the manifest's.retention/tombstones.json'sorg_idandrangeMUST equal the manifest's. Any mismatch is a REJECT. - Counts.
counts.eventsis the number of exported events.counts.anchoredandcounts.pendingare the lengths ofproofsandpending, and they sum tocounts.events. Withoutevidence-anchor/,counts.anchoredis 0 and every event is pending.counts.tombstonedis the number of tombstones shipped, 0 withoutretention/. Each MUST equal what the verifier counts. - Scope.
scope.typeis one oforg,agentorincident. Any other value is a REJECT: a verifier that guesses the scope guesses the completeness rule (section 7.1 step 6).
What the signature establishes: every file in the bundle is byte-identical
to what the platform issued at generated_at. That includes the case file's
events, the proofs, the tombstones, and the embedded verifier. It does not
establish that the content was correct when issued (section 9).
The platform's forensic report for the export scope. It holds the audit rows
(events), the scope, the chain-verification result the platform computed at
export time, and the report signature.
- Events. Each row carries:
id: the audit identry_hashandprev_hash: platform chain;entry_hashis the Merkle leafentry_hash_organdprev_hash_org: per-org chainorg_chain_seqkey_fingerprint: the first 16 hex characters of SHA-256 of the HMAC key that hashed the row; absent on legacy rows- the payload fields listed in section 6.
- Scope. The
scopeobject:type,org_id, andagent_idor the incident id as applicable. - Chain verification block.
chain_verification(or the same fields flat):chain_valid(orvalid),total_entries,entries_verified. This is the platform's claim about its own chain at export time. A verifier does not trust it; it recomputes (section 7.2). - Report signature (Tier K). HMAC-SHA256 under the org verify key, in the
report_signaturefield, over the canonical payload{chain_valid, entries_verified, generated_at, report_id, total_entries}(section 6). This mirrorscommon.audit.writer.generate_report_signature, and the CLI recomputes it in_compute_report_signature. It covers only those five summary fields. It does not cover the events, the scope, the org or the range; the manifest (Tier P) and the chain walk (Tier K) bind those. - Reliability statement.
reliability_statementis a plain-English, FRE 702 / Daubert-oriented account of the integrity method and its limits. Its wording MUST stay consistent with section 9.
[{"merkle_root": "<hex>", "envelope": {<DSSE>}}, ...]: one entry per
distinct checkpoint covering at least one exported event. The format is
MerkleCheckpointV1, specified in evidence-anchor-reference-notes.md
section 3:
payloadTypeisapplication/vnd.ai-identity.anchor-checkpoint+json, with exactly one signature: ECDSA P-256 / SHA-256 by the KMS forensic-signer key, resolved bykeyidagainst the JWKS.- The payload is JCS-canonical (RFC 8785) on the producer side. Its fields are
schema_version(1),org_id,tree_size,merkle_root,first_audit_id,last_audit_id,signed_atandsigner_key_id. - The key is non-extractable (KMS HSM) and asymmetric: the public key verifies but cannot forge. That is what makes Tier P publicly verifiable where Tier K is not.
{"proofs": [...], "pending": [...]}, as specified in
evidence-anchor-reference-notes.md section 4.
- Each proof is
{audit_id, entry_hash, index, tree_size, merkle_root, proof: [<hex>, ...]}: the RFC 6962 audit path for the leafentry_hashatindexin a tree oftree_sizeleaves undermerkle_root. It is O(log N). pendinglists theaudit_ids of exported events not yet covered by any checkpoint. Together withproofsit accounts for every exported event exactly once (section 3).audit_id,indexandtree_sizeare unsigned labels. Section 7.1 step 5 binds them to signed data where possible.
The format is ai-identity-retention-tombstones/v1, fully specified in
retention-tombstones-in-exports.md. This spec incorporates it by reference
and adds four v1 rules:
- The checkpoint signature check on cited checkpoints is mandatory for a
Tier P result. In 1.5.0,
chain --tombstonesmakes it optional (--jwks). The cited checkpoint also passes the section 7.1 step 4 checks (schema_version,org_id). - The frozen
leavesandaudit_log_idshave the same length, and that length equals the checkpoint's signedtree_size. Each tombstone'saudit_idlies within the checkpoint's signed[first_audit_id, last_audit_id]. The frozen lists are unsigned; these bind them to what was signed. org_idandrangeMUST equal the manifest's.- A tombstone whose
org_chain_seqis also a surviving exported row is a contradiction, and a REJECT.
Know what the offline checks reach:
- Receipt check. It confirms a receipt with
event_type == "tombstone"and a non-nullaudit_log_idis present. It does not confirm that the anchor row exists in the chain, because that row usually falls after the export's range. mirror_commit. It is informational offline, and checkable only at Tier W (section 7.3).
The standalone verifier, open source under MIT in this repository. report
and chain (without --tombstones --jwks) are stdlib-only. attestation,
inclusion-proof, chain --tombstones --jwks and the v1 bundle command
also need cryptography.
The bundle embeds the exact version that built it: its hash is in the signed
inventory and its version is manifest.verifier_version.
The embedded copy is a convenience, not a trust anchor. A bundle's own verifier judging that same bundle is circular. A verifier SHOULD run an independently obtained copy of the CLI, at the same version or later, from a tagged release of this repository. The signed inventory lets anyone confirm the embedded copy is the one that was issued, which is useful for reproducing a result. It does not make that copy trustworthy.
What the CLI accepts or rejects is relied on by third parties. Changes to it
need maintainer sign-off (AGENTS.md, "Verifier semantics") and a CHANGELOG
entry, and they are versioned per section 8.
verify.command is a turnkey macOS runner (mode 0755). It locates the case
file, runs the verifier, and prompts for the org key, the one input the bundle
cannot ship, by design. README.md is the human-readable guide. Both are
convenience, not trust anchors: the procedures in section 7 are normative and
the README is their plain-English rendering.
Each layer keeps the scheme it shipped with, because changing a scheme breaks every existing signature. This spec locks the schemes; it does not unify them.
| Layer | Scheme | Used by |
|---|---|---|
| Audit entry payload (both chains) | Python json.dumps(payload, sort_keys=True, separators=(",", ":")), then UTF-8 |
Tier K chain walk; platform chain |
| Report signature payload | Same | Tier K report check |
| Checkpoint, attestation and manifest DSSE payloads | JCS, RFC 8785 (producer side) | Tier P signature checks |
| DSSE signing input | PAE = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body |
All DSSE verification |
| Merkle tree | RFC 6962 | Tier P inclusion and tombstone checks |
| File digests | SHA-256, lowercase hex, over uncompressed file bytes | Manifest inventory |
Audit entry payload. These are the exact fields. The key named prev_hash
carries the predecessor on whichever chain is being computed.
| Key | Value |
|---|---|
agent_id |
str() of the exported value |
cost_estimate_usd |
str() of the exported value, or null |
created_at |
the exported string with a trailing Z replaced by +00:00; no other normalization (precision, offset form and separators pass through unchanged) |
decision, endpoint, method |
as exported |
latency_ms |
integer or null |
prev_hash |
prev_hash for the platform chain; prev_hash_org for the per-org chain |
request_metadata |
object; {} when absent |
entry_hash = hex(HMAC-SHA256(AUDIT_HMAC_KEY, canonical)), with the platform predecessor.entry_hash_org = hex(HMAC-SHA256(org key for the row's epoch, canonical)), with the per-org predecessor.
A non-Python implementation must reproduce Python's json.dumps defaults:
- keys sorted by code point
- non-ASCII characters escaped as
\uXXXX, with surrogate pairs above U+FFFF - floats in Python
reprform - no whitespace
"Close enough" parsing is a verification failure, not a compatibility feature.
Report signature payload.
{chain_valid, entries_verified, generated_at, report_id, total_entries}. It
is serialized the same way, with the same Z rule applied to generated_at.
Merkle tree (RFC 6962 section 2.1).
leaf_hash(d) = SHA-256(0x00 || d) d = bytes.fromhex(entry_hash), 32 bytes
node_hash(l, r) = SHA-256(0x01 || l || r)
MTH(D[0:n]) = node_hash(MTH(D[0:k]), MTH(D[k:n])) k = largest power of 2 < n
Inclusion is the RFC 6962 section 2.1.1 audit-path check.
DSSE. Verify over PAE(payloadType, payload bytes as decoded), with no
re-canonicalization (section 4).
All procedures are fail-closed. Any mismatch is a REJECT, and the verifier reports the first failing check together with the artifact and field that failed. It MAY continue past that point to collect diagnostics, but the result stays REJECT.
Each tier ends in exactly one of these outcomes:
| Outcome | Meaning |
|---|---|
VERIFIED |
Every applicable check passed |
REJECTED |
A check failed |
UNAVAILABLE |
An input the tier needs is absent: no key for Tier K, no network for Tier W. Never shown as VERIFIED, never counted as REJECTED |
PRE-V1 |
Tier P only: the bundle has no manifest (section 8.2) |
Given only the bundle and the JWKS:
-
Container. Apply the ZIP hygiene rules (section 3). If
manifest.jsonis absent, the bundle is pre-v1: continue per section 8.2. -
Manifest. Confirm
payloadType == "application/vnd.ai-identity.case-file-manifest+json"and that there is exactly one signature. Resolvekeyidin the JWKS to an EC P-256 key and verify the signature (section 4). Then checkschema_versionandformat. Finally, confirm the inventory exactly matches the ZIP contents and everysha256matches. -
Cross-binding. Check the manifest's
org_id,scope,rangeandcounts.eventsagainst the case file (section 4). Confirmscope.typeis known. -
Checkpoints. For each entry in
evidence-anchor/checkpoints.json:payloadTypeis the checkpoint type, with exactly one signaturekeyidresolves in the JWKS- the signature verifies
- the payload's
schema_version == 1 - the payload's
merkle_rootequals the entry'smerkle_root - the payload's
org_idequals the manifest'sorg_id
-
Inclusion proofs. For each proof:
- Its
merkle_rootis a checkpoint that passed step 4. tree_sizeequals that checkpoint's signedtree_size.audit_idlies within the checkpoint's signed[first_audit_id, last_audit_id].- The RFC 6962 audit path from
leaf_hash(entry_hash)atindexreproduces the root. - An exported event with
id == audit_idexists, and itsentry_hashequals the proof'sentry_hash. The manifest signature makes this comparison meaningful: the event'sentry_hashis the one issued.
Then check accounting: every exported event id appears in exactly one of
proofsorpending, and the lengths matchcounts.anchoredandcounts.pending. Pending events are reported as unanchored, not as failures. - Its
-
Org-slice structure. The rule depends on
scope.type:orgis the only completeness scope. Sort events byorg_chain_seqand check two things over stored values. First,org_chain_seqis contiguous from first to last, except for gaps accounted for in step 7. Second, each row'sprev_hash_orgequals its predecessor'sentry_hash_org. This proves structure as issued; recomputing the hashes is Tier K.agentandincidentscopes are sparse slices: other agents and unrelated activity own the intervening sequence numbers. A gap re-anchors the linkage check and is not evidence of deletion; retention is not consulted. These scopes make no completeness claim, and the verifier says so.
-
Tombstone accounting (
orgscope, at each gap). Run the algorithm inretention-tombstones-in-exports.md, "Verifier algorithm", with the v1 rules of section 5.4. That includes the mandatory signature check on every cited checkpoint, applying the step 4 rules. Also check that the file'sorg_idandrangematch the manifest and that the number of tombstones equalscounts.tombstoned. A gap with no covering tombstone, or a tombstone that fails any check, is an unaccounted deletion: REJECT, report it as tampering evidence, and keep the bundle.
If every step passes, the bundle is verified at Tier P (section 9.1).
Tier K runs after Tier P steps 1 to 3 have passed, or on a pre-v1 bundle.
-
Org chain recompute. For each event, pick the supplied key whose fingerprint matches
key_fingerprint. For legacy rows without a fingerprint, accept a match under any supplied key. Recomputeentry_hash_org(section 6) and compare. A row whose fingerprint matches no supplied key is not covered, which is not tampering. The structural checks of Tier P step 6 still apply to it. If no row is covered, Tier K is UNAVAILABLE, never VERIFIED. -
Report signature. Recompute the section 6 report payload HMAC under the supplied keys and compare it with
report_signature. -
Claim consistency. Two checks against the platform's
chain_verificationblock:total_entriesMUST equal the number of exported events.- A
chain_valid: falseclaim is reported as a REJECT whatever the local result.
entries_verifiedis reported, not compared, because the platform verifies under keys the holder may not have.
If all steps pass, the bundle is verified at Tier K (section 9.2). The count of rows not covered travels with the result.
For each checkpoint root the bundle relies on (step 4, plus the checkpoints tombstones cite), query the public record. Ask the live feed first, and fall back to the mirror only when the feed gives no usable answer (unreachable, a 5xx or other non-404 status, or an unreadable body). The fallback is what keeps Tier W working in the disappearance case (section 2):
- the live feed:
GET https://api.ai-identity.co/evidence-anchor/checkpoints/<merkle_root> - the
evidence-anchor-mirrorbranch of this repository:checkpoints.ndjsonfor the entries andmirror-state.json(mirrored_at) to date the snapshot
| Response | Result |
|---|---|
| Same root, byte-identical envelope | WITNESSED |
| Same root, different envelope | REJECT: split view. Report to security@ai-identity.co and keep the bundle |
| Live feed 404 for a root that verified offline | REJECT: split view (evidence-anchor-public-feed.md section 2) |
Mirror lacks the root, and its signed_at is within one mirror interval (6 hours) of the mirror's newest snapshot |
NOT YET WITNESSED: not a failure; re-check later |
Mirror lacks the root, and its signed_at is older than that |
REJECT |
| Source unreachable | UNAVAILABLE |
The tier's outcome sums these up: REJECTED if any checkpoint is a split view, VERIFIED if every checkpoint is WITNESSED, and otherwise UNAVAILABLE, with the counts of checkpoints not yet in the mirror and with no reachable source.
With a clone of the mirror, a verifier MAY also confirm that each tombstone
checkpoint is present at its mirror_commit, and that the commit predates the
tombstone's pruned_at. Mirror commit timestamps are set by a workflow in AI
Identity's GitHub organization, so this check raises the bar; it is not
independent proof (evidence-anchor-public-feed.md, limits).
See section 8.2.
The verifier reports the tiers separately and never as a single blended "valid":
Bundle: ai-identity-case-file/v1 manifest signature VALID
Tier P: VERIFIED 4800 anchored, 190 pending, 10 tombstoned
Tier K: VERIFIED 4990/4990 rows under supplied keys
Tier W: UNAVAILABLE offline
-
Counts.
valid with N tombstoned rowsMUST be distinguishable fromvalid, andvalid with N rows not coveredfrom full coverage. Counts travel in both human and JSON output. The JSON result carries atiersobject with each tier's outcome and counts. -
Exit codes for the
bundlecommand:0: Tier P VERIFIED and no tier REJECTED1: any tier REJECTED2: usage error3: nothing REJECTED, but Tier P is not VERIFIED (PRE-V1)
Existing subcommands keep their 0/1/2 codes.
"Zero network calls" is measured at verification time, not collection time. Whoever collects a bundle for long-term holding SHOULD, at collection:
- Save a copy of the JWKS (
/.well-known/ai-identity-public-keys.json) with the bundle. Key versions are never destroyed (key-rotation.md), but a saved copy removes even that dependency. - Save an independently obtained copy of the verifier CLI (section 5.5).
- Run Tier W and keep its JSON output with the bundle, as a dated record that the checkpoints were in the public history when collected.
- Save the org verify key and retired epoch keys, if Tier K must survive on its own. Anyone holding them can also forge Tier K, so where they are stored is the holder's custody decision, not the vendor's.
- Format identifier. The bundle format identifier is
ai-identity-case-file/v1, in the manifest'sformatfield. A major version bump is a breaking change. A verifier MUST fail loudly on an unknown version and never accept silently. - Signed envelopes. The manifest, checkpoints and attestations each carry
their own
schema_version, and verifiers reject unknown values. The bundle format does not re-version them. - Unsigned artifacts. They MAY add fields within v1, and readers MUST ignore fields they do not understand.
- Verifier semantics. A change to what the CLI accepts or rejects needs maintainer sign-off and a CHANGELOG entry. A change that would turn a conforming v1 bundle's VERIFIED into REJECTED, or the reverse, travels with a format version bump.
Bundles issued before the manifest shipped have no manifest.json. They are
not backfilled or re-issued.
A v1 verifier reports them as PRE-V1: bundle integrity unsigned. It runs
Tier P steps 4 to 7 over the unsigned files, locating the case file by glob.
It reports Tier P as PRE-V1, never VERIFIED: without the manifest, nothing
binds the exported rows to the anchored hashes. Tier K runs as usual.
Two older shapes are handled the way the 1.5.0 chain command handles them:
- No
scopein the report: treated asorgscope, so a gap still needs a tombstone. - No per-org chain fields: there is no org-slice structure to check. The
result says so, and Tier K is UNAVAILABLE with a pointer to
chain --global.
The existing subcommands (report, chain, inclusion-proof, attestation)
keep working on pre-v1 bundles exactly as in 1.5.0.
Stripping manifest.json from a v1 bundle makes it read as PRE-V1, never as
VERIFIED, so there is no downgrade to a passing result.
Proves:
- Every file in the bundle is byte-identical to what the holder of the
manifest signing key issued at
generated_at. - Each anchored event's
entry_hashwas committed under a Merkle root that the checkpoint's KMS key signed. - For
orgscope, the issued slice is structurally complete: contiguous inorg_chain_seq, linked by stored hashes, with every gap accounted for by a tombstone whose hash sits under a signed root.
Does not prove:
- that row content matches its anchored
entry_hash(section 9.3) - that the events are complete beyond the slice (see
pendingand the scope rules) - that a tombstone's receipt is itself chained
- that the events are correct, or about what they claim to be about
Proves: every covered row's content is exactly what the org key hashed at write time, in chained order, with no insertions, deletions or modifications since then.
Does not prove:
- anything about rows outside the supplied keys' epochs
- that the agent did anything in particular: rows record gateway decisions
- that a decision was correct
- timing within a session
The table in trust-model.md applies unchanged.
Nobody outside AI Identity can check that a row's content hashes to its
anchored entry_hash, because that takes AUDIT_HMAC_KEY. The three tiers
compose around the gap without closing it:
- Tier K fixes content at write time, under the org key.
- Tier P fixes the anchored hash at anchoring time, and the whole bundle at issuance.
What this means in practice:
- After issuance. Anyone editing a bundle breaks the manifest signature. That includes an org admin who holds the org key and re-forges the org chain. This is the case the signed manifest exists for.
- Before issuance. A party that can write rows and also holds the platform
key is answered by the two-key custody split in
trust-model.md, not by this format. - After disappearance. Platform-chain verification is gone permanently (section 2).
Section 11 item 1 is the v2 candidate that would close the gap.
- anything about events outside the bundle's id range
- anything about the vendor's current state
- that AI Identity is trustworthy: only that the evidence is intact as described
Everything below is required before any bundle is v1. The layout, checkpoint format, tombstone format and canonicalization schemes are specified as they exist today; the changes are these.
- Signed
manifest.jsonper section 4, emitted by the bundle endpoint: one KMS sign per export, failing loudly if signing fails. - Coverage accounting.
proofsandpendingtogether account for every exported event exactly once, and every proof'saudit_idlies within its checkpoint's signed id range. Confirm whether both already hold; if not, enforce them. - README,
verify.commandandreliability_statementinvoke thebundlecommand and describe the tiers in section 9's terms.
All of items 1 to 7 shipped in CLI 1.6.0, with tests for each REJECT path
they add. The optional mirror_commit check in section 7.3 is not
implemented: it needs a clone of the mirror rather than its published files.
-
bundlesubcommand. It accepts a ZIP or an extracted directory and implements sections 7.1 to 7.5. All new strictness lives here: the existing subcommands keep their 1.5.0 verdicts except items 3 and 4 below. -
Manifest verification and cross-binding (section 4, section 7.1 steps 1 to 3).
-
inclusion-proof.- Reject a checkpoint payload whose
schema_versionis not 1.evidence-anchor-reference-notes.mdalready states this rule. From 1.6.0 the CLI enforces it wherever it verifies a checkpoint, which includeschain --tombstoneswith--jwksor--pubkey. - Bind
tree_sizeto the signed payload.
These change verdicts only for malformed inputs.
- Reject a checkpoint payload whose
-
chain. Treatscope.type == "incident"as a sparse slice, likeagent. Incident exports are sparse, and today a gap in one reads as a deletion. -
Key-free tombstone accounting inside
bundle, with the checkpoint signature mandatory. Today it exists only insidechain, which exits withoutAI_IDENTITY_HMAC_KEY. -
Tier W behind an explicit
--onlineflag. This is the only network code in the verifier, and it is off by default. -
Tests for every REJECT path above, plus a CHANGELOG entry.
- Anchor a public content digest (v2 candidate). Committing a public, unkeyed content digest per row, alongside or inside the leaf, would close section 9.3 for everyone. An unsalted digest of low-entropy rows invites dictionary attacks wherever leaves are published. Leaves are not served on the public feed, but they do ship in bundles. So the digest needs a per-row salt carried in the row. This is a checkpoint-format change and needs its own spec.
- Manifest signing key. Reusing the checkpoint signer's key version is
safe, because PAE domain-separates by
payloadType. A dedicated key in the same key ring isolates compromise and rotation. Recommendation: a dedicated key, published in the same JWKS. - Builder facts to confirm before implementation.
- Does the section 10.1 item 2 accounting hold today?
- What exact
scopeobject does an incident export carry (typevalue and id field name)?