The exhibit format
Version 1 (draft). Describes bundle_schema_version 1. (0.1.0 is a different field: record.schema_version, which versions the 14-field record rather than the exhibit around it.)
*** DRAFT-PENDING-COUNSEL — not legal advice and not a compliance certification. This document describes a file format and the arithmetic over it. What a court makes of that arithmetic is a question for the forum, not for this file. ***
What this document is
An exhibit produced by this system is a JSON file. This document says exactly what is in it and how
to check it, so that a third party can verify one without our software and without our
cooperation — with tools they already have: sha256sum, openssl, and any RFC 8785 (JCS)
implementation.
A verification program is also available, and it is free. This document exists so that program is a convenience rather than a dependency. If this company ceases to exist, an exhibit sealed today is still checkable from this document alone.
License. This document and its test vectors (vectors/) are licensed under the
Apache License, Version 2.0, as is the reference verifier. Implement the format, quote this document,
and copy the vectors into your own tests under that license.
What this document is NOT. It is not the source code of the reference verifier, which is a program of its own. Every primitive below is a published standard implemented by other people; nothing here is an invention of ours that a verifier would have to trust.
What an exhibit proves, and what it does not
Checking an exhibit establishes four things and no more:
| It establishes | It does not establish |
|---|---|
| This record has not been altered since it was sealed | That the record is true. A recorder seals a mistaken transcription as faithfully as a correct one |
| It sat at this position in a log of this size | That the log is complete. Nothing here shows that everything which happened was recorded |
| A named organization other than the log's operator cosigned that log's root | That the cosigning organization is trustworthy. Who counts as separated is the reader's judgement, from their own register |
| A timestamp was made over that root | That the timestamp is qualified under eIDAS, unless the token says so and the reader trusts its issuer |
Those limits are not caveats added at the end. They are the reason the format states what it states and no more.
1. The primitives
| Hash | SHA-256 |
| Multihash | uvarint(0x12) ‖ uvarint(32) ‖ digest — i.e. bytes 12 20 followed by the 32-byte digest |
| Content address | the multibase prefix f (base16, lower) followed by the multihash in lower-case hex |
| Canonical JSON | RFC 8785 (JCS), narrowed by the number rule in §1a |
| Signatures | Ed25519 (RFC 8032) or ML-DSA-44 (FIPS 204), over the exact bytes named below |
| Merkle tree | RFC 6962 §2 — leaf SHA-256(0x00 ‖ leaf_bytes), node SHA-256(0x01 ‖ left ‖ right) |
| Timestamps | RFC 3161 CMS (rfc3161-cms), or a local signed token (local-ed25519-ts), as algorithm says |
Signatures. Every signature in an exhibit is an object {"algorithm", "public_key", "signature"},
the key and the signature as standard base64 of their raw bytes. algorithm is one of two:
algorithm |
Raw public key | Raw signature | |
|---|---|---|---|
ed25519 |
32 bytes | 64 bytes | RFC 8032 |
ml-dsa-44 |
1312 bytes | 2420 bytes | FIPS 204, the pure form, with an empty context string |
The same two apply wherever a signature appears: the log's on the checkpoint (§4.2), a witness's on
its attestation (§4.3), and a local timestamp token's (§4.4). A signature under any other algorithm
does not verify. An installation of the reference verifier whose cryptography library cannot check
ML-DSA-44 says so and exits 2, the code for a file it could not check, never 1.
1a. Numbers
RFC 8785 defines number serialisation in terms of ECMAScript, which is exact and reproducible — and it is also the single place where two honest, independent implementations most often disagree. A disagreement there does not read in court as an interop bug. It reads as one side's exhibit being wrong.
So the format narrows it, and the narrowing is normative:
- An integer MUST lie within ±(2⁵³ − 1), i.e.
-9007199254740991to9007199254740991. Outside that range a value is not exactly representable as an IEEE-754 double, and two implementations following ECMAScript can legitimately produce different digits. A producer MUST refuse such a value rather than seal it; a verifier MUST refuse to canonicalise one rather than guess. NaNand the infinities MUST be refused. RFC 8785 has no representation for them, so a record containing one has no canonical form and therefore no reproducible address.- Non-integer numbers are permitted and are serialised exactly as ECMAScript prints them:
0.1is0.1, and0.1 + 0.2is0.30000000000000004— those digits, not a rounding of them. An implementation that shortens or reformats them computes a different leaf. - An integral float and the same integer are one value:
4200.0serialises as4200.
The reference implementation refuses an out-of-range integer, a NaN and an infinity BEFORE
serialising, and raises a typed error — so those become a refusal at capture time rather than an
exhibit nobody can reproduce. The two rules about how a permitted number PRINTS are not enforced
by a pre-check and cannot be: they are properties of the serialiser itself, and the only way to be
wrong about them is to write a different serialiser.
And the same rules bind the READER. A bundle carrying an integer outside the range parses and
then cannot be canonicalised, so evidence-verify refuses it as unreadable — exit 2, this is not
a bundle — rather than exit 1, this evidence was tampered with. That distinction was worth
splitting the exit codes for: for a while the reader raised an uncaught error and exited 1, which
told a court the wrong thing about a file that had merely been written badly.
Nor does the reader parse a document nested deeper than 128 levels, counting the outermost object or array as one. An exhibit nests 6 levels, a policy 4 and a disclosure 1, and no object the reference implementation canonicalises nests deeper than 65, so 128 leaves room for the deepest of them inside an envelope as deep again. Past that, a parser recurses toward the end of its stack: CPython raises an error near three thousand levels, and a runtime with a smaller stack, such as WebAssembly in a browser, fails before that and takes the program with it. So the depth is counted before anything is parsed, and a deeper file is refused as unreadable, exit 2, like any other file the verifier cannot read.
2. The file
The exported file has two halves, and the split is the most important thing about it:
{
"bundle": { /* the evidence itself — §2.1 */ },
"producer_asserted_trust": { "operator_org": "acme-gmbh", "registry": [ /* witness keys */ ] }
}
producer_asserted_trust is the producer's PROPOSAL, and you must replace it. It is what the
party under examination says the trust inputs are: which organization operates the log
(operator_org), and which witness keys to check the cosignatures against (registry, in rows of
the same shape as a policy's trusted_registry, §5.1). Every value in it is one you can obtain
elsewhere, and every value in it is one they would have chosen if they were lying. Read it to know
what to go and confirm; do not verify against it.
The verification program takes those inputs from a separate policy file for exactly this reason, and refuses to read them out of the exhibit.
2.1 The bundle
{
"bundle_schema_version": "1",
"record": { /* the sealed record, see §3 */ },
"leaf_index": 2,
"inclusion_proof": ["<base64>", "..."],
"checkpoint": { "origin": "acme/prod", "tree_size": 3, "root_hash": "<base64>" },
"signature": { "algorithm": "ed25519", "public_key": "<base64>", "signature": "<base64>" },
"anchor": {
"timestamp_token": { "algorithm": "...", "tsa_id": "...", "gen_time": "...", "digest": "<base64>",
"cms_token": "<base64>" /* or "signature": {...} for a local token */ },
"cosignatures": [ { "witness_id": "...", "org": "...", "signature": {...},
"last_seen_size": 2, "prev_link_hash": "<base64>",
"attested_at": "2026-08-24T17:33:36.415664+00:00" } ],
"operator_org": "acme-gmbh",
"threshold": 1
}
}
All paths below are relative to bundle.
anchor is absent from an exhibit that was never anchored. Such an exhibit proves only that a
record sits in a log the operator signed — which is the operator's own word, and worth little.
The reference verifier does not say that in its verdict. Under a reader policy that sets
require_anchor to false it prints bundle: VALID over such an exhibit and exits 0, and the
words cosignature and separation appear nowhere in the output — those checks are not failed,
they are absent. Read that VALID as "the checks this policy asked for passed", never as "this
record was anchored". require_anchor defaults to true, and a policy that leaves it there prints
bundle: INVALID and FAIL external_anchor over the same exhibit.
3. The record, and its hash
record is a JSON object. Its fields:
| Field | Meaning |
|---|---|
schema_version, hash_algorithm, canonicalization_method |
how to read the rest |
record_id |
the sender's own identifier. Unique within one archive; not globally unique, and not a hash |
prev_record_hash |
the content address of the previous record, or null for the first |
payload_hash |
content address of salt ‖ plaintext. The exhibit carries no plaintext |
timestamp |
the OPERATOR's clock, written by their recorder. Not independent |
agent_system_id, agent_version, deployer_operator_id |
which agent, which build, whose deployment. The deployer is stated by the producer and verified by nothing; no policy counts it toward separation |
event_type |
CONTEXT_RECEIVED (the agent was told something) or ACTION_TRIGGERED (it did something) |
action_type, actor_authority |
what kind of action, and on whose authority. Both may be null |
retention_class, legal_hold |
producer-asserted. Nothing in the format enforces them |
gap_manifest |
which capture tiers were active, and notes. This is where the format admits what it did not capture. Every tier is FALSE unless the producer set it: a tier that is not asserted was not claimed, never assumed |
anchor_receipt, certification_pointer |
attached AFTER sealing, and therefore excluded from the hash. Covered by no signature and verified by nothing (below) |
The leaf. Take record, remove anchor_receipt and certification_pointer, canonicalize with
RFC 8785, and multihash it:
leaf = 12 20 ‖ SHA-256( JCS(record minus the two post-seal fields) )
Note the leaf is the multihash (34 bytes), not the bare digest. prev_record_hash is the same
value written as a content address: "f" + hex(leaf).
The two fields outside the hash are not evidence. The leaf leaves them out, and so does every signature over the root, so anyone holding an exhibit can write into them and a valid exhibit stays valid. A verifier MUST NOT present either as checked, and MUST NOT let either change its verdict.
- The reference verifier names each one that is not
null. It does so in a block of its own after the checks, headedoutside the record hash, as covered by no signature. With--jsonit lists them underunsigned_claims, each withfield,value,contradicts_checkpointanddetail. An exhibit that sets neither field gets neither the block nor the key, so its report is unchanged. - One thing in them can be held against the evidence.
anchor_receipt.merkle_root(the standard base64 of a root, ascheckpoint.root_hashis written) andanchor_receipt.tree_sizename a tree. Where either is not the signed checkpoint's, the report says the receipt contradicts the checkpoint. Where both are, it says that and no more, because anyone holding the file can copy them in. - The verdict and the exit code are the same either way. The page that
evidence-verify --htmlwrites marks both rows "outside the hash, not checked".
4. Checking it — four steps
The steps give two answers, and the reference verifier reports both, integrity on its own line:
- Integrity — 4.1, 4.2 and the signature in 4.3: the record is in the log, the operator's key signed that root, and a witness's key signed exactly that checkpoint. It needs the keys you hold for the log and the witness (§5, items 1 and 2) and no reference time: whether a key was valid at a given moment is not part of it. A disclosure (§6), when you have one, belongs here too.
- The verdict — what only your policy can decide: the moment at which key validity is judged, whether you trust the timestamp (4.4), how many witnesses you require, and which organizations count as separated (§5, items 3 to 5).
Integrity can hold while the verdict waits for an input only you can supply; the reference verifier then says NOT VERIFIED and exits 3. When integrity fails, no input of yours changes that, and the verdict is INVALID.
4.1 The record is in the log
Recompute the leaf (§3). Then run the RFC 6962 audit path with leaf_index and
checkpoint.tree_size, hashing leaves as SHA-256(0x00 ‖ leaf) and nodes as
SHA-256(0x01 ‖ left ‖ right). The result must equal checkpoint.root_hash.
If this fails, nothing else matters.
4.2 The operator signed that root
The signed bytes are three lines, in this order, each terminated by \n:
<origin>\n<tree_size>\n<base64(root_hash)>\n
Verify signature.signature over exactly those bytes with signature.public_key (Ed25519).
Obtain the operator's public key from your own records, not from the exhibit. The key in the file is the producer's claim about which key to use. Checking a signature against the key that accompanies it proves only that whoever made the file had a key.
4.3 Somebody else cosigned the same root
For each entry in anchor.cosignatures, rebuild the body the witness signed. It is RFC 8785 JCS
over this object — attested_at present only if the cosignature carries it:
{
"blackbox_witness_attestation": 1,
"origin": "<checkpoint.origin>",
"tree_size": <checkpoint.tree_size>,
"root": "<base64(checkpoint.root_hash)>",
"witness_id": "<witness_id>",
"org": "<org>",
"last_seen_size": <last_seen_size>,
"prev_link_hash": "<base64(prev_link_hash)>",
"attested_at": "<attested_at>"
}
Verify the cosignature over those bytes with the witness's key — again, the one you hold, not the one in the file.
Three facts come out of this, and they are the reason the format exists:
orgis inside the signature. The organization is not a label attached to the file; the witness signed the claim that it is that organization.anchor.operator_orgis NOT signed by anybody. It is a producer-asserted field in the bundle, and a producer can put anything in it. So separation is judged by comparing the signedorgagainst the operator organization YOU registered for this log, out of band — never against the one in the file. An implementation that reads the operator's organization out of the exhibit hands the producer the naming of their own counterparty, which is the entire property this check exists to establish. The reference verifier takes it from the reader's policy (trusted_logs[].operator_org) and ignores the bundle's copy; changing that field in an exhibit provably does not move the verdict.last_seen_sizeis inside the signature. It is the head this witness had already attested before this one.0means a cold start — the witness checked nothing for consistency, and you can see that. A witness whose statements all say0has no memory and is worth accordingly less.attested_atis inside the signature. It is that organization's statement of when it saw this root. It is not an accredited timestamp and carries no legal presumption; it is the statement of a party with no interest in the dispute, which is more than the operator's own clock inrecord.timestamp. A cosignature made before witnesses stated a time omits the field entirely, and the body omits the key with it.
Older forms. A cosignature with neither last_seen_size nor prev_link_hash is the legacy
form: the signature covers the checkpoint bytes of §4.2 alone. It is strictly weaker — the identity
was unsigned — and a reader may refuse it. The three forms are distinguished by which fields are
present, and each verifies against the body it was made over. No form is ever rewritten.
Counting separation by legal entity (policy schema 2). The signed org names a service
identity, and whoever runs a service chooses its label. One company can sign under two labels, and a
company's legal name can change while every earlier signature keeps the label it had. A reader's
policy of schema 2 therefore counts legal entities, not labels:
- It binds each role to a legal entity for an interval and in an environment:
- the witness (
witness_idandorg); - the log host (
host_id); - the log host identity. That is the identity the witness admitted to enrol the log, named by the
label the log is enrolled under:
operator_orgintrusted_logs, and a binding of kindoperatorin the file.
- the witness (
- A hosted log's log host identity is its log host's own. A log that its customer runs itself has the customer's.
- The bindings are read at the reference time the witness keys are judged at (§5). An exhibit made before a service changed hands is counted against the binding that held then.
- A cosignature counts toward separation only if its witness resolves to a legal entity that is neither the log host identity's nor the log host's. A witness whose binding is missing, belongs to another environment, has not started or has ended counts for nobody, and the report says which it was.
- The deployer is carried as stated and bound to nothing, so it never counts toward separation.
- Two bindings for one identity whose intervals overlap are refused as ambiguous.
The reference verifier's report lists the roles in a block of their own: the log host, the log host
identity, the deployer as stated, and each witness. Each role comes with the legal entity it
resolves to, or the reason it resolves to none. The file keeps the words operator_org and
operator, and the report does not use "operator" for this role, because the word also names the
party that runs the witness.
The count stands in one line, organizational_separation. Under schema 2 with a floor of 0, the
report's summary of the anchor claims no separation, whatever was counted. A legal entity that a
policy requires to be present is not thereby required to be separated. A policy may also require no
cosignature at all; under schema 2, where none was counted, the summary claims no witnessed quorum
either, and cosignature_quorum states the count.
Under schema 2, just above the roles, the report names every key this exhibit carries a signature
under: the log's key on the checkpoint, and each witness's key on its cosignature. Each comes with
its algorithm and a fingerprint, sha256: followed by the 64 lower-case hex digits of SHA-256 over
the raw public key, the bytes whose base64 is signature.public_key. The fingerprint is never
shortened: two keys can be made to share a prefix, and sharing the whole digest would take a SHA-256
collision. To recompute it from a key published in base64, decode the base64 and take SHA-256 of
the bytes. The keys are read from the exhibit as it carries them. Compare each with the
key you obtained for that role (§5, items 1 and 2), not with the policy you were handed. Whether a
signature held under its key is said by the checks.
The exhibit itself carries no legal entity and needs none. Nothing in §2–§4 changes, and every exhibit verifies under either schema.
4.4 A timestamp was made over that root
anchor.timestamp_token.digest must equal checkpoint.root_hash — the raw 32-byte SHA-256
Merkle root, base64 in the file. Not its multihash, and not a hash of the checkpoint bytes: the
timestamp is made over the ROOT, so that a token issued for one tree cannot be presented for
another. Then:
cms_tokenpresent: an RFC 3161 token. Verify it withopenssl ts -verify, against a CA certificate you obtain yourself. If the only certificate available is the one inside the token, you are checking the producer's value against the producer's value and the check is empty.signaturepresent instead: a local token,algorithmlocal-ed25519-ts. Itstsa_idsays what it is. It carries no qualified presumption and, unless you registered its key out of band, no presumption at all. The bytes it signs are RFC 8785 JCS over this object, and the signature is checked as in §1, under a key you registered — the reference verifier takes those keys fromtrusted_tsa_keys(§5.1):{ "blackbox_timestamp_token": 1, "tsa_id": "<tsa_id>", "gen_time": "<gen_time>", "digest": "<base64(digest)>" }
gen_time beside the token is metadata. Compare it to the time inside the token; two spellings of
one instant (+00:00, Z, an offset) are the same instant, and a difference is a discrepancy.
5. What a reader must bring
The exhibit deliberately does not carry these, because a file supplied by the party under examination cannot supply its own trust inputs:
- The operator's public key, and which log name they are entitled to write under.
- The witness's key, organization, and the interval that key was valid for, from a source
the producer does not control. The
registryin an exhibit'sproducer_asserted_trust(§2) only tells you what to go and confirm. - A reference time at which key validity is judged.
- A trusted CA certificate for the timestamp, if one is claimed.
- A judgement about which organizations count as separated from the operator. Under policy schema 2 this is a directory: the legal entities, the intervals for which the witnesses, the log host and the log host identity were bound to them, and how each log is run (§4.3).
An exhibit checked without these still tells you the record was not altered and sat where it says. It does not tell you whom to believe.
5.1 The policy file
The reference verifier takes those inputs as one JSON object, the policy. It is the reader's file, not the producer's, and it is read strictly: a key the verifier does not know is refused, never ignored, because an ignored demand verifies as though it had been met. Times are RFC 3339. Keys are standard base64 of the raw public key.
Schema 1. A file without policy_schema is schema 1 ("policy_schema": 1 may also be written).
| Key | Value | If absent | What it decides |
|---|---|---|---|
trusted_registry |
rows of witness_id, org, public_key, not_before, not_after |
required | the witness keys you hold, each for its interval: not_before included, not_after excluded, an empty not_after open-ended. A row whose interval cannot be read, or whose not_after is no later than its not_before, makes the file unusable (exit 2), because such a key would be valid at no instant and the verdict would blame the evidence |
trusted_logs |
rows of origin, public_keys (a list), operator_org |
required, at least one | which logs you examine, under which checkpoint keys, and which organization label each is enrolled under |
as_of |
a time, or null |
null |
the reference time at which key validity (and, under schema 2, every binding) is judged. Without it only an eIDAS-qualified token's time may stand in, and otherwise the verdict waits |
tsa_anchor |
base64 DER of a certificate, or null |
null |
the RFC 3161 authority you trust. A CMS token verifies only if it is signed under exactly this certificate, the certificate carries id-kp-timeStamping, and the token's time falls inside its validity |
trusted_tsa_keys |
a list of keys | [] |
the local timestamp authorities you accept (§4.4). Naming nobody trusts nobody |
require_qualified |
true or false |
false |
refuse every timestamp but a CMS token whose certificate you assert, with tsa_is_accredited_qtsp, to be an accredited QTSP's |
tsa_is_accredited_qtsp |
true or false |
false |
your statement that tsa_anchor belongs to an accredited QTSP. It is never read off a token |
require_anchor |
true or false |
true |
demand an external anchor at all (§2.1) |
min_quorum |
a whole number ≥ 0 | 1 |
how many distinct registered cosignatures. An exhibit's threshold can raise it, never lower it |
min_separated |
a whole number ≥ 0 | 1 |
how many organizations (schema 1) or legal entities (schema 2) other than the log's own side must have cosigned |
min_witness_last_seen |
a whole number ≥ 0, or null |
null |
the head a witness must state it had already attested: 0 refuses the legacy form, 1 or more also a cold start |
also_disqualified_orgs |
a list of labels | [] |
organizations that never count toward separation |
required_orgs |
a list of labels | [] |
organizations that must be among the counted cosignatures |
Schema 2. "policy_schema": 2 counts separation over legal entities (§4.3). It reads every key
above except also_disqualified_orgs and required_orgs, which it refuses, and adds:
| Key | Value | If absent | What it decides |
|---|---|---|---|
legal_entities |
rows of id, name |
required, may be empty | the legal persons a role can be bound to |
service_bindings |
rows of kind, legal_entity, environment, not_before, and optionally not_after |
required, may be empty | which legal entity stands behind a service identity, in an environment, for an interval. The identity fields depend on kind: witness takes witness_id and org, log_host takes host_id, operator takes org (the log host identity, the label in trusted_logs[].operator_org), and a binding carries no identity field of another kind |
log_roles |
rows of origin, mode, environment, and optionally host_id and deployer_as_stated |
required, one for every log in trusted_logs |
how each log is run: mode hosted names the host_id it runs on, own names none, and environment is where its bindings are read. deployer_as_stated is reported as stated and verified by nothing |
also_disqualified_legal_entities |
a list of ids |
[] |
legal entities that never count toward separation |
required_legal_entities |
a list of ids |
[] |
legal entities that must be among those counted. Present is not the same as separated |
The rows of the three lists are read as strictly as the file: a field the verifier does not read is
refused. Two bindings for one identity in one environment whose intervals overlap are refused as
ambiguous, a binding naming a legal entity the file does not declare is refused, and so is a log role
for a log trusted_logs does not name.
docs/vectors/policy.json is a complete schema-2 file and docs/vectors/policy-schema1.json the
same inputs as schema 1.
6. Opening a payload
The exhibit carries no plaintext. To show what a record says, the holder discloses the plaintext and
the per-record salt separately, as a JSON object of two keys: salt, the salt, and payload_b64, the
plaintext, both standard base64 of the raw bytes. Other keys are ignored. Check it by recomputing:
"f" + hex( 12 20 ‖ SHA-256(salt ‖ plaintext) ) == record.payload_hash
The salt is exactly 32 bytes. That is normative, not a convention: SHA-256(salt ‖ plaintext)
commits to the concatenation and not to the cut, so a free length would give every address a second
valid reading — a holder could move the first k bytes of the payload into what they call the salt
and disclose the remainder, and the recomputation above would still match. Fixing the length leaves
exactly one way to cut, which is what makes this a commitment to the PAIR. An implementation MUST
reject a salt of any other length, and MUST report that rejection as an unreadable disclosure rather
than as a disclosure the record contradicts — the two say entirely different things about the
holder.
The salt is random per record, drawn from the operating system's CSPRNG, and is not derivable. A record whose salt has been destroyed can never be opened again — that is how erasure works here, and it is irreversible by design.
7. Test vectors
docs/vectors/ holds two constructed exhibits, the policies to read them under, and the cases an
implementation has to get right. They are the executable half of this document: if your reading of
the prose and the vectors disagree, the vectors are right and the prose is a defect.
Constructed, not taken from a deployment: every name in them is fictional, and every key is an
example key whose private half is derived from a published seed, so they show what an implementation
must compute and nothing about the world. A program writes them from fixed inputs, byte for byte the
same each time; the one exception is the ML-DSA-44 signature, which FIPS 204 randomizes and which is
kept as long as it verifies. docs/vectors/README.md says how each was made and quotes the reference
verifier's report on each.
| Vector | What a correct implementation does |
|---|---|
evidence.json + policy.json |
accepts the record as leaf 2 of 3 under a signed root, cosigned by a witness bound to a separate legal entity, timestamped by an authority whose certificate the policy names: VALID |
evidence.json + policy-schema1.json |
the same answer to every check, counted over organization labels |
evidence-ml-dsa-44.json |
the same, for a log whose checkpoint signature is ML-DSA-44 |
evidence-tampered.json |
rejects it on INCLUSION, while the checkpoint signature still passes — the root was not touched |
disclosure.json |
opens the address: salt + plaintext hashes to record.payload_hash |
disclosure-truncated.json |
refuses it as malformed, because the salt is not 32 bytes — and reports that as NOT CHECKABLE, never as a contradiction. Concatenated, these bytes DO hash to the address; that is exactly why the length is normative |
evidence-wrong-org.json |
fails the witness signature, the quorum and separation while the log's signature and the inclusion still pass, because the organization is inside the witness's signature and trust comes from the reader's registry, not from the file |
Remove tsa_anchor from policy.json and evidence.json is no longer VALID but NOT VERIFIED: the
timestamp is then rooted in nothing the reader named, which is a gap in the reader's file and not a
failure of the evidence.
8. Stability
Fields are added, never renamed or repurposed, and a body is never rewritten. A cosignature made
before a field existed still verifies against the body it was made over, because the key is omitted
from the canonical object rather than set to null. An implementation should ignore fields it does
not know and refuse a bundle_schema_version it does not recognise rather than guess.
The reference verifier refuses a version it does not know. It reads 1, and 0, which is how an
exhibit sealed before the field existed reads back: such an exhibit carries no field at all.
EvidenceBundle.from_dict (packages/tallwright-verifier/src/tallwright_verifier/bundle.py)
refuses any other value. The command line reports that file as unreadable and exits 2, never with a
verdict: set the field to 99-from-the-future in a copy of docs/vectors/evidence.json and it
answers bundle_schema_version '99-from-the-future' is not one this build reads (0, 1). This
document is the yardstick implementations are built against, ours included, and where the two
disagree the document is right.
Policy schemas. A reader's policy is not part of the exhibit, and its schema moves on its own:
- A policy without
policy_schemais schema 1. Every version of the reference verifier reads it, and this one reads it with exactly the semantics and the report it had before schema 2 existed. - A policy with
policy_schema: 2counts separation over legal entities (§4.3). A verifier from before schema 2 refuses it, because it refuses every top-level key it does not know. It does not read such a file as schema 1. - Directory rows are read strictly. A field this verifier does not read is refused, never ignored, because an older reader would drop it without a word.
Bundles, records, checkpoints and cosignatures are the same under both schemas.
Fixed identifiers
Five strings begin with blackbox, the working name this project had when the formats they belong to
were fixed. Each is part of bytes that are signed or hashed, or of a protocol already spoken, so
renaming one would change every signature or link made over it. They stay as they are, permanently,
and mean nothing beyond naming the structure they head:
blackbox_witness_attestationheads the body a witness signs (§4.3).blackbox_witness_genesisheads the object whose SHA-256 is theprev_link_hashof a witness's first statement about a log: RFC 8785 JCS over{"blackbox_witness_genesis": 1, "origin", "witness_id", "org"}, so a chain cannot be moved to another log or identity. In every later statement,prev_link_hashis SHA-256 of the witness's previous signed body.blackbox_timestamp_tokenheads the body a local timestamp authority signs (§4.4).blackbox_witness_refusalheads the body a witness signs when it refuses a checkpoint. A refusal is not part of an exhibit.blackbox-witness/2names the protocol a log speaks to a witness over the network. It is not part of an exhibit.