Contents

This document specifies what a verifier does with one b1 record bundle (Bundle): the four outcomes a check can have, the checks and the order they are reported in, the report, and the exit codes of a command-line verifier. Two conforming verifiers given the same bundle and the same inputs produce the same report, apart from the instant it records, and the same exit code.

The record itself is specified in Record, the anchor artifact and its checks in Anchor, and the tree proofs in Tree. Requirement keywords are used as Specs states.

1. Principles

  • Nothing the bundle says about itself is an input. A verifier never reads the bundle's exporter member, the envelope's attestationStatus, or nonRepudiation.rfc3161Metadata, and never copies a summary or verdict the bundle carries. Every fact in a report is derived again from the artifacts.
  • Every input may be hostile. Verification MUST complete, and report, on any input. An unforeseen failure inside the verifier is reported as the single row bundle, not-checked, bundle_verifier_error (§4.2): a fault to report in the verifier, never a verdict on the bundle.
  • There is no overall verdict. A report lists what is decided and what is not. The auditor's regime (§3.2) and the exit code (§6.6) say what the rows add up to.
  • Nothing present passes in silence. Material a verifier of this version does not verify, a relation it does not know included, is reported by a row that abstains or is not-checked.

2. Outcomes

2.1 The four outcomes

Every check, called a row below, has exactly one outcome:

OutcomeMeaning
checked-correctDecided, and it holds
checked-wrongDecided, and it fails: a finding against the artifact
not-checkedThis verifier could not decide it; the reason says why
abstainThe artifact names a construction or version this verifier does not implement

2.2 Rules

  1. An abstention is neither a failure nor a pass. An unknown version tag makes the verifier abstain on the part of the record that the tag governs, and only on that part. An unknown bundle version or profile makes it abstain on the whole bundle.
  2. A failure is reported once, by the row that decided it. A row whose prerequisite is not decided (the prerequisite is checked-wrong, not-checked or abstain) is not-checked, with the reason blocked_by:<id of the prerequisite>.
  3. Only a missing artifact is absent. An artifact that is present and cannot be read is a finding or an abstention, as its row states. It is never absent, and never pending.
  4. Facts are derived, never copied (§1).
  5. Reasons. A checked-correct row carries no reason; every other row carries exactly one.

2.3 Reasons

The reasons are closed lists. A verifier MUST NOT report a reason that neither this section nor Anchor §10.1 lists. The rows of §4 use:

OutcomeReasons
checked-wrongmalformed_bundle, subject_mismatch, digest_mismatch, payload_unserializable, chain_link_mismatch, chain_link_missing, predecessor_mismatch, malformed_token, imprint_mismatch, verification_failed, malformed_signoff_expectation, session_signoff_not_expected, signoff_unbound, signoff_stripped, statement_digest_mismatch, session_seq_mismatch, mandate_not_expected, mandate_stripped, malformed_mandate_statement, mandate_assertion_mismatch, mandate_unbound, mandate_document_mismatch, malformed_decider, decider_inconsistent, decider_kind_contradicted, freshness_block_mismatch, malformed_contributions, step_chain_broken, decider_not_deciding_actor, deciding_output_mismatch, input_undeclared, review_unbound, decision_unbound, decided_output_differs, malformed_links, link_rule_violated, link_attrs_invalid, link_target_mismatch, issuance_not_expected, issuance_material_stripped, issuance_material_mismatch, malformed_mandate_document, agent_not_in_mandate, outside_mandate_scope, confirmation_evaded, mandate_totals_malformed, mandate_limit_exceeded
abstainunsupported_bundle_version, unsupported_bundle_profile, unsupported_envelope_version, unsupported_schema_version, unsupported_signoff, unsupported_extension, unsupported_slot, unsupported_member, unsupported_artifact, legacy_blockchain_anchor, unsupported_decider_kind, unsupported_identifier_scheme, unsupported_freshness_chain, unsupported_actor_kind, unsupported_pipeline, unsupported_link_relation, unsupported_link_rule, unsupported_link_edge, unsupported_mandate_document
not-checkedabsent, not_applicable, not_yet_published, blocked_by:<id>, not_in_bundle:predecessor, not_in_bundle:credential, not_in_bundle:mandate_document, not_in_bundle:link_target, signature_missing, confirmation_missing, material_unverified, scope_undeterminable, link_rule_undetermined, no_matching_trust_anchor, missing_dependency, bundle_verifier_error

The anchor rows use the lists of Anchor §10.1, and the rows anchor and anchor.newer also use publication_not_checked (Anchor §10.6). The row decider.freshness also uses input_not_supplied:evm_header_source and the reasons of the header source's states listed in Anchor §10.1 (§4.9). bundle_verifier_error and missing_dependency report a fault in the verifier or a library it lacks, never in the artifact.

2.4 Rungs

Sign-off verification reports the strength of a signature as rungs: facts on a checked-correct row, never outcomes. This version of the specification defines no row that reports a rung. A report's summary.rungs is empty, and a rung that the regime requires is never met (§3.2).

3. Inputs and the regime

3.1 Inputs

InputUsed by
The bundleEvery row
One publisher manifest (Anchor §9), or none. A verifier of this version reads no supplementary manifest.EVM publications (Anchor §10.4)
Header sources, each for one CAIP-2 chain or for any chainanchor.evm.canonical[i] (Anchor §10.4), decider.freshness (§4.9)
RFC 3161 trust roots, and any intermediate certificatests.record.trust, anchor.tsa.trust[i]
A timeout, 30,000 ms unless the auditor sets anotherOne deadline shared by every header-source call of one verification (Anchor §10.4)
The verifier's network registryanchorNetwork (Anchor §9)

The auditor supplies these, and the bundle never does. Only roots the auditor supplies count: a verifier MUST NOT fall back on a trust store of its platform or its libraries. A supplied root or intermediate that the verifier cannot read, including one whose key algorithm or curve it does not implement, is an auditor input that cannot be read (§6.6): a verifier MUST NOT drop it and go on. A verifier MAY ship a default publisher manifest, and MUST let the auditor replace it or trust none.

The chain id that Anchor §10.2 takes as an input is the bundle's chain.chainId when it is a string, and is not supplied otherwise (Bundle §5.3).

3.2 The regime

The auditor's regime has three parts:

  • require: a list of row ids. An entry names the row with that id; an entry that ends in [*] names every row whose id is the entry's stem followed by [, one or more decimal digits, and ] (so anchor.evm.canonical[*] names anchor.evm.canonical[0]). The requirement is met only when every entry names at least one row of the report and every row it names is checked-correct.
  • requireRung: a list of rungs, met only when each is reported (§2.4).
  • allowUnchecked: when set, rows left undecided do not lead to exit code 4 (§6.6).

Informative. tzun-verify takes these as --require <id>[,<id>…] and --require-rung <rung>, both repeatable, and the switch --allow-unchecked.

4. The check catalogue

4.1 Report order

Rows are reported in this order. Consumers read a row by its id, but the order is normative, because reports are compared whole (§6.5).

  1. bundle (§4.2). When it is not checked-correct, it is the only row.
  2. subject (§4.2)
  3. record.digest, record.chainLink, custody.ledger (§4.3)
  4. ts.record.token, ts.record.trust (§4.4)
  5. signoff.expectation, signoff.key (§4.5)
  6. decider, decider.freshness (§4.9)
  7. contributions, contributions.binding, then contributions.pipeline when reported (§4.10)
  8. links, then, for each link in list order, links[<i>] and links[<i>].edge, when reported (§4.11)
  9. mandate.issuance, mandate.scope, mandate.limits, mandate.key and mandate.assertionTimestamp, each when reported (§4.12)
  10. anchor, the six step rows from anchor.spec to anchor.anchorInclusion, then the rows of each publication in array order (§4.6; Anchor §10.7)
  11. anchor.newer, when reported (§4.6)
  12. anchor.consistency[<origin>], one per origin that has a row, in byte order of origin (§4.6)
  13. anchor.pending, when the slot is filled (§4.6)
  14. extension[<key>], in byte order of key (§4.7)
  15. relatedRecords, then policy, each when filled (§4.7)
  16. payload.<name>, in byte order of name (§4.7)
  17. member[<key>], in byte order of key (§4.7)
  18. rows for the members of nonRepudiation that no row above reads, in byte order of member name (§4.7)

"Byte order" compares the UTF-8 encodings of two strings byte by byte. A member name that would name a row never holds a lone surrogate: a bundle with one is malformed_bundle (§4.2).

4.2 The bundle

bundle decides, in this order:

  1. The bundle is a JSON text (Record §3.1) whose value is an object. Otherwise checked-wrong, malformed_bundle.
  2. bundleVersion is the string "b1". Otherwise, absent or null included, abstain, unsupported_bundle_version.
  3. profile is the string "record". Otherwise, absent included, abstain, unsupported_bundle_profile. This version does not verify a bundle of any other profile.
  4. The bundle has the frame of a record bundle, steps 4 to 9 of Bundle §4: all eleven slots of Bundle §3.1 present, each an object or null; the envelope, its payload and its integrity block objects; the members of chain of the right types; the anchoring slot in the shape of Bundle §7.1, every checkpoint body filed under its own origin and at most two of them per origin; and no lone surrogate in a member name that a row id carries. Otherwise checked-wrong, malformed_bundle.
  5. Otherwise checked-correct.

An unforeseen failure of the verifier makes bundle not-checked, bundle_verifier_error, as the only row (§1).

subject (Bundle §5.1) is checked-correct when subject.evidenceId is a string equal to envelope.payload.evidenceId, and subject.canonicalDigest and envelope.integrity.canonicalDigest are hashes with the same 32 bytes. Otherwise it is checked-wrong, subject_mismatch. It compares the stored digest, not a recomputed one, so that a changed payload is reported once, by record.digest.

4.3 The record

record.digest decides, in this order:

  1. envelope.envelopeVersion is not the string "e1": abstain, unsupported_envelope_version.
  2. payload.schemaVersion is not the string "v1.0": abstain, unsupported_schema_version.
  3. The canonical serialization of the hashed object (Record §4.2) is refused (Record §3.6): checked-wrong, payload_unserializable.
  4. integrity.canonicalDigest is not a hash, or its 32 bytes differ from the digest recomputed by Record §4.2: checked-wrong, digest_mismatch. When the stored digest is a hash whose 32 bytes equal the digest recomputed over the payload under one of the earlier field sets of Record §4.1, the row carries the fact fieldSet, the name of that set: "0.7" or "before-0.7". The fact says which earlier draft the record's digest follows. It never changes the outcome: whoever can rewrite a stored record can also rewrite it into an earlier set.
  5. Otherwise checked-correct.

The recomputed digest is the input of ts.record.token, ts.record.trust, signoff.expectation, decider, contributions, links, mandate.issuance, anchor and its step rows, and anchor.newer. Each of them is not-checked, blocked_by:record.digest, unless record.digest is checked-correct, except where §4.4 and §4.6 report an artifact absent first.

record.chainLink checks Record §5 over the stored digests. It runs whatever record.digest says. With n the payload's sequenceNumber, D the stored integrity.canonicalDigest, L the stored integrity.chainLink and p the bundle's chain.predecessor (Bundle §5.3):

  1. n is not an integer of at least 1, or D is not a hash: checked-wrong, chain_link_mismatch.
  2. When n is 1: L present gives checked-wrong, chain_link_mismatch; else p present gives checked-wrong, predecessor_mismatch; else checked-correct.
  3. When n is more than 1:
    • L absent: checked-wrong, chain_link_missing;
    • p absent: not-checked, not_in_bundle:predecessor;
    • p.sequenceNumber is not the integer n − 1, or p.canonicalDigest is not a hash: checked-wrong, predecessor_mismatch;
    • L is not a string equal, ignoring case, to the link computed from p.canonicalDigest and D: checked-wrong, chain_link_mismatch;
    • otherwise checked-correct.

custody.ledger reports on the bundle's custody slot (Bundle §6). This version does not verify custody material, and the row is never checked-correct:

  1. The slot is null, or an object whose entries member is an empty array: not-checked, not_applicable.
  2. Otherwise: abstain, unsupported_artifact.

4.4 The record's timestamp

ts.record.token and ts.record.trust check nonRepudiation.rfc3161Token (Record §10):

  1. No token: both not-checked, absent.
  2. record.digest is not checked-correct: both blocked_by:record.digest.
  3. The token is not a string, or is empty: ts.record.token reads checked-wrong, malformed_token.
  4. Otherwise the steps of Anchor §10.5 run over the token, with the 32 bytes of the recomputed digest in place of P. Steps 1 to 3 decide ts.record.token, with the reason malformed_token where Anchor §10.5 step 1 gives malformed_publication. Step 4 decides ts.record.trust: checked-correct with the facts genTime and authority, checked-wrong, verification_failed, or not-checked, no_matching_trust_anchor. Step 5 gives not-checked, missing_dependency, on the check whose step could not run.

Whenever ts.record.token is not checked-correct, ts.record.trust is blocked_by:ts.record.token, unless step 1 or 2 above applies. A verifier MAY bound its search for a certificate path; a search stopped by its bounds has not shown a path to a supplied root, and reads no_matching_trust_anchor.

4.5 Sign-off

signoff.expectation compares what the record carries under nonRepudiation with its hashed signoffExpectation (§5).

signoff.key says whether a key is available to check the record's WebAuthn signature. This version verifies no WebAuthn signature. With "assertion" as Record §9.3 defines it, the row decides, in this order:

  1. signoff.expectation is checked-wrong: not-checked, blocked_by:signoff.expectation.
  2. signoff.expectation is not-checked, signature_missing, and the record has no assertion: not-checked, signature_missing.
  3. The bundle's credentials slot is not null: abstain, unsupported_signoff.
  4. The record has no assertion: not-checked, absent.
  5. Otherwise: not-checked, not_in_bundle:credential.

4.6 The anchor

anchor and its step and publication rows are the checks of Anchor §10.3 to §10.7, over:

  • the artifact nonRepudiation.anchor;
  • the digest recomputed by record.digest;
  • the payload's sequenceNumber, and the chain id of §3.1;
  • the auditor's inputs (§3.1);
  • each evm-calldata/1 entry of the artifact, joined with the material the bundle carries for the same transaction (Bundle §7.4; Anchor §8.2).

When the record has neither anchor nor blockchainAnchor, anchor and the six step rows are not-checked, absent, whatever record.digest says, and there are no publication rows (Anchor §10.9). Otherwise, when record.digest is not checked-correct, anchor and the six step rows are blocked_by:record.digest, with no publication rows. A blockchainAnchor without an anchor is step 1's legacy abstention (Anchor §10.3); one beside an anchor has a row of its own (§4.7).

anchor.newer checks the anchor that ties the subject to the latest anchor checkpoint in the bundle. It is reported when an evidence-log checkpoint entry of the anchoring slot has any of the members logPath, anchorLeafIndex, anchorSize and anchorPath; Bundle §7.5 states which entry is the target, how the verifier composes an anchor artifact from it, and how that artifact is verified. Because the composed artifact carries the subject's own path, every step of Anchor §10.3 applies to it, the inclusion of the subject included. The row takes the outcome and reason that the composed artifact's anchor row would have; a member the bundle cannot supply leaves the artifact short of step 2's shape (malformed_anchor). When it is checked-correct its facts are treeSize and anchorSize, the sizes of the two bodies it authenticated. It is blocked_by:record.digest when the digest does not hold. With no target there is no row.

anchor.consistency[<origin>] is Anchor §10.8, read from the bundle as Bundle §7.6 states: one row for each origin whose checkpoint bodies the bundle carries or an authenticated anchor names. The bodies authenticated are the log and anchor checkpoints of the record's anchor when anchor is checked-correct, and those of the composed anchor when anchor.newer is checked-correct. For each origin:

  • a carried body that neither authenticated makes the row blocked_by:anchor while anchor is not checked-correct, and blocked_by:anchor.newer once it is, whether or not the report has an anchor.newer row: with no target (Bundle §7.5), nothing can authenticate that body;
  • otherwise, with two authenticated bodies, the row compares them by Anchor §10.8, using the first entry of the origin's consistency list whose fromSize and toSize are the smaller and the larger of the two sizes (Bundle §7.6), and not-checked, not_in_bundle:consistency_proof, when there is no such entry;
  • otherwise the origin has no row.

anchor.pending is reported when the bundle's anchorPending slot is not null (Bundle §7.7): not-checked, not_yet_published. It is never an abstention and never absent.

4.7 Material this version does not read

Each of these rows reports material that is present and that this version does not verify, so that it is never passed over in silence:

  • extension[<key>]: each member of extensions whose value is not null: abstain, unsupported_extension (Bundle §9). This version implements no extension.
  • relatedRecords, policy: the reserved slot of that name, when it is not null: abstain, unsupported_slot (Bundle §8).
  • payload.<name>: each member of the payload, whatever its value, whose name is not one of the twelve of Record §2.2: abstain, unsupported_member. Such a member is not hashed (Record §4.1), so nothing vouches for its content, and a consumer that showed it beside the verdict on the record would show unverified content as if it were verified. A payload that still carries aiBom, humanAction or humanReason, which an earlier draft hashed, is reported this way.
  • member[<key>]: each top-level member of the bundle other than bundleVersion, profile and the eleven slots of Bundle §3.1: abstain, unsupported_member (Bundle §3.3).
  • For each member of nonRepudiation whose value is not null, other than anchor, rfc3161Token, rfc3161Metadata, webauthnAssertion, signoff, mandate and mandateIssuance, which the rows of §4.4 to §4.6, §4.12 and §5 read, and other than confirmation when the payload's signoffExpectation is an object with a mandateAssertion member and a confirm member that is true, which §5 reads:
    • assertionTimestamp: the row ts.assertion.token, abstain, unsupported_artifact;
    • blockchainAnchor, when anchor is also present: the row nonRepudiation.blockchainAnchor, abstain, legacy_blockchain_anchor;
    • any other member: the row nonRepudiation.<name>, abstain, unsupported_artifact. This includes the reserved members agentSignature, contributionSignatures and runtimeQuote (Record §2.4), and a confirmation that the record does not owe.

The credentials slot has no row of its own: when filled it makes signoff.key abstain (§4.5). The signatures of a checkpoint entry have no row in this version, whatever they hold: a verifier does not read them (Bundle §7.2). exporter is never read.

4.8 What this version does not verify

This version verifies no WebAuthn signature, credential, sign-off statement beyond what §5 reads, sign-off policy, related record, assertion timestamp, custody material or extension, and no bundle of a profile other than record. Nor does it verify:

  • who a decider is beyond what the record states: the level it reports is asserted (§4.9), and a human signature bound to the record is reported as bound, not as proven;
  • material that would contradict a decider's kind, when that material carries a signature this version cannot check (material_unverified, §4.9);
  • agent descriptors (Record §6.9), and the credential registration a decider names;
  • pipeline definitions, signatures over individual steps, and whether the contributors were independent of one another;
  • the target rules of links, since a b1 bundle carries no target, the rules of relations it does not know, and link edges: that a target was on record before the record that names it, or sat at the position its locator names (§4.11);
  • who signed a mandate, whether the mandate was in force when a record was captured (its validity window, its revocation or its replacement), and the consumption of a mandate across records (Record §9.10);
  • a payload member outside the twelve of Record §2.2, which is not hashed (§4.7).

Whatever of these a bundle carries is reported by a row that abstains or is not-checked, which keeps the exit code at 4 or above unless the auditor allows unchecked rows (§6.6).

4.9 The decider

These rows read payload.decider (Record §6).

decider decides, in this order:

  1. record.digest is not checked-correct: not-checked, blocked_by:record.digest.
  2. signoff.expectation is checked-wrong, malformed_signoff_expectation: not-checked, blocked_by:signoff.expectation, since the agreement of step 7 needs the expectation's form.
  3. The decider is null: not-checked, absent. The decider is not recorded, and the verifier infers none (Record §6.7).
  4. The decider is not well formed under Record §6.2: checked-wrong, malformed_decider. The value rules of the schemes of Record §6.3 bind writers, and are not applied here.
  5. kind is not one of the kinds of Record §6.1: abstain, unsupported_decider_kind. An unknown kind is not malformed, so that the vocabulary can grow.
  6. The kind, the agent reference and the scheme do not agree, or a desc value is not agent (Record §6.4): checked-wrong, decider_inconsistent.
  7. The expectation's form contradicts the kind (Record §6.5): checked-wrong, decider_kind_contradicted.
  8. The scheme of id is not one of Record §6.3: abstain, unsupported_identifier_scheme.
  9. The kind is agent, agent-mandated or policy-engine, and the record carries human decision material that binds it (Record §6.5): not-checked, material_unverified, with the fact kindContradiction "undetermined". A signature this version cannot check is never taken as settling the contradiction either way.
  10. Otherwise checked-correct, with these facts:
    • kind, the decider's kind, and scheme, the scheme of its id;
    • level: "human-asserted" for the kind human, "agent-asserted" for the others. The decider is what the writer stated: this version verifies no signature that would prove it;
    • levelUndetermined: "human-proven", only for the kind human when the record carries human decision material that binds it. A human signature is then bound to the record, and who made it is not verified by this version;
    • committed: true, only for the scheme cmt. The row then checks the member's structure and agreement; who the decider is stays committed and unopened.

decider.freshness checks the finalized-block reference (Record §6.6) against the auditor's header source (Anchor §10.4). It decides, in this order:

  1. The decider is null, or is an object whose freshness is null: not-checked, absent.
  2. decider is not checked-correct: not-checked, blocked_by:decider.
  3. The reference names a chain outside the CAIP-2 namespace eip155: abstain, unsupported_freshness_chain.
  4. The auditor supplied no header source for that chain (§3.1): not-checked, input_not_supplied:evm_header_source.
  5. The header source is asked as Anchor §10.4 items 8 to 11 ask it, under the deadline shared by every header-source call of the verification: a call still waiting at the deadline, a call that fails, a source whose chain id is not the reference's, a block number above the source's final height, and a source with no canonical block at that height give not-checked with evm_header_source_timeout, evm_header_source_unavailable, evm_header_source_chain_mismatch, evm_block_not_finalized and evm_header_source_inconsistent.
  6. The source's canonical block at that height has a hash other than the reference's: checked-wrong, freshness_block_mismatch.
  7. Otherwise checked-correct, with the facts blockTime, the canonical block's time, and captureLag, the payload's capturedAt less blockTime, in milliseconds (a JSON number, negative when capturedAt is the earlier). capturedAt is read only when it is a string matching /[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?Z/ whose fields name a real instant, as Record §9.7 states for validity; its fraction is truncated to whole milliseconds. Otherwise captureLag is left out. captureLag is informative.

From the point where the source's chain id has matched, the row carries the fact finality, the mode the source declares, whatever its outcome, as Anchor §10.4 item 14 states for an EVM publication. blockTime and finality are written as Anchor §10.7 writes them.

4.10 Contributions

These rows read payload.contributions (Record §7).

contributions checks the structure of the contributions. Its steps are ordered so that a failure that does not depend on an actor's kind or scheme is never hidden behind an abstention. It decides, in this order:

  1. record.digest is not checked-correct: not-checked, blocked_by:record.digest.
  2. contributions is null: not-checked, absent.
  3. The contributions are not well formed under Record §7.1, apart from the vocabulary of an actor's kind and scheme; or they break the rules of Record §7.3 on sets, verdicts and the deciding step; or an actor whose kind is one of Record §6.1 is not consistent under Record §6.4: checked-wrong, malformed_contributions. A step's aiBom is read only as null or an object (Record §7.4).
  4. The step chain is broken: checked-wrong, step_chain_broken, with the fact step, the index of the first step whose prev is wrong.
  5. There is a deciding step, and the decider and the deciding step's actor differ: checked-wrong, decider_not_deciding_actor.
  6. The output of a step of the class decide is not the output digest of the record's action and reason: checked-wrong, deciding_output_mismatch.
  7. An actor's kind is not one of Record §6.1: abstain, unsupported_actor_kind. Otherwise, an actor's scheme is not one of Record §6.3: abstain, unsupported_identifier_scheme.
  8. Otherwise checked-correct, with the facts steps, the number of steps; models, the distinct modelDescriptorDigest values of the steps' bills of materials that are strings, in byte order, to which a step whose aiBom is null, or whose modelDescriptorDigest is absent or not a string, adds nothing; and decidingStep, the index of the deciding step, or "record" when there is none and the record's decider took the decision after the last step.

contributions.binding checks the process the contributions state (Record §7.5). A writer may create a record that fails it, so its findings are about the decision's process, never about the record's form. It decides, in this order:

  1. contributions is not-checked, absent: not-checked, absent.
  2. contributions is not checked-correct: not-checked, blocked_by:contributions.
  3. An input is undeclared: checked-wrong, input_undeclared, with the facts step and digest, the first such input in order of step and then of input. Links declare inputs only when payload.links meets the grammar of Record §8.2, whatever the links row says of its rules.
  4. A review does not have the output it covers among its inputs: checked-wrong, review_unbound, with the fact step, the first such review.
  5. There is a deciding step, and its inputs do not include the output of P*: checked-wrong, decision_unbound.
  6. outputDigest(aiOutput) is not the output of P*: checked-wrong, decided_output_differs.
  7. Otherwise checked-correct.

Whenever it is decided, the row carries these facts, each an array of step indexes in increasing order: proposals, the steps of the class propose; approvals, the current reviews whose verdict is "approve"; staleApprovals, the stale reviews whose verdict is "approve"; and uncoveredReviews, the reviews that cover no step. It also carries decisionBinding: "checked", or "not-applicable" when there is no P*, in which case steps 5 and 6 do not apply. A stale approval is never counted as an approval of the decision.

contributions.pipeline is reported when contributions is checked-correct and its pipeline is not null: abstain, unsupported_pipeline. This version defines no pipeline definition.

4.11 Links

These rows read payload.links (Record §8). A b1 bundle carries no target record (Bundle §8), so a verifier of a bundle holds none, and the steps below that need a target apply only to a verifier that holds one by other means, such as a store's own (Record §8.8).

links checks the member as a whole. The list-level rule mandate_link is not applied when the expectation is malformed (Record §8.3), that is, when signoff.expectation is checked-wrong, malformed_signoff_expectation. The row decides, in this order:

  1. record.digest is not checked-correct: not-checked, blocked_by:record.digest.
  2. links is null or absent, and no list-level rule of Record §8.3 that applies requires a link: not-checked, absent.
  3. links breaks the grammar of Record §8.2: checked-wrong, malformed_links. When the member is an array of 1 to 256 elements, the row carries the fact index: the index of the first link that breaks the grammar of a single link or, when each link meets it, the index of the first link that is not after the link before it in the order of Record §8.2.
  4. A list-level rule of Record §8.3 fails, a null member whose record requires a link included: checked-wrong, link_rule_violated, with the fact rule, the name of the first rule that fails in the order of that table. The conditions on a link record's form (Record §8.6) are part of subject_link, so a link record that breaks one fails on this row, and each of its links[<i>] rows is then not-checked, blocked_by:links.
  5. Otherwise checked-correct, with the facts count, the number of links; relations, the distinct values of rel, in byte order; and extensions, the number of links whose rel is an extension name.

links[<i>] and links[<i>].edge are reported for each element of links when it is an array of 1 to 256 elements, i being the element's index counting from 0: links[0], links[0].edge, links[1] and so on.

links[<i>] checks one link against its relation. In a link record (Record §8.6), a rule of a link other than the subject link that reads this record's members reads the subject's instead, and so needs the subject. It decides, in this order:

  1. links is not checked-correct: not-checked, blocked_by:links.
  2. The link's rel is mandate and signoff.expectation is checked-wrong, malformed_signoff_expectation: not-checked, blocked_by:signoff.expectation.
  3. rel is not a relation of the registry of Record §8.3, being an extension name or a name this version does not register: abstain, unsupported_link_relation, with the fact rel.
  4. attrs breaks the relation's attribute profile (Record §8.3): checked-wrong, link_attrs_invalid.
  5. A record-local rule of the relation fails: checked-wrong, link_rule_violated, with the fact rule, its name.
  6. The verifier holds the target, and a member of target that is not null disagrees with it (Record §8.8): checked-wrong, link_target_mismatch, with the fact member, "evidenceId" or "locator", the first that disagrees in that order.
  7. target.locator is not null, the verifier holds the log it names, and the position it names holds another record or none (Record §8.8), whether or not the verifier holds the target: checked-wrong, link_target_mismatch, with the fact member "locator".
  8. The verifier holds the records a target rule of the relation reads, and the rule fails: checked-wrong, link_rule_violated, with the fact rule, its name.
  9. A rule of the relation is one this version does not define (the target rules of delegatedBy, closes and fanoutOf), or the record-local rule of root is not decided (Record §8.3): abstain, unsupported_link_rule.
  10. The verifier holds the records a target rule of the relation reads, and the rule is undetermined (Record §8.3): not-checked, link_rule_undetermined.
  11. The relation has a target rule, or, in a link record, a rule that reads the subject, and the verifier does not hold a record it needs: not-checked, not_in_bundle:link_target.
  12. Otherwise checked-correct, with the fact targetHeld, true when the verifier holds the target and false otherwise, and, for a root link, the fact rootInconsistent true when Record §8.5 calls for it.

links[<i>].edge is not-checked, blocked_by:links, when links is not checked-correct. Otherwise it is abstain, unsupported_link_edge: this version verifies no edge (Record §8.8), whatever the relation.

4.12 Mandates

These rows read the mandate form of the expectation, the mandate block and the issuance block (Record §9.6).

mandate.issuance is reported when the record carries an issuance block, or when its aiSystemId is "tzun:mandate" and its action is "issue-mandate", which makes it an issuance record. It decides, in this order:

  1. record.digest is not checked-correct: not-checked, blocked_by:record.digest.
  2. The record carries an issuance block and is not an issuance record: checked-wrong, issuance_not_expected.
  3. An issuance record carries no issuance block: checked-wrong, issuance_material_stripped.
  4. A link of Record §9.6 does not hold: checked-wrong, issuance_material_mismatch, with the fact link, the first that fails of "document" (the document's digest is not aiOutput.mandateDigest and the statement's mandateDigest, or its principal.signer is not the statement's signer), "statement" (the statement's digest is not aiOutput.statementDigest), "challenge" (the assertion does not present the statement challenge), "assertion" (the assertion digest is not aiOutput.assertionDigest) and "decider" (the decider is not an object whose kind is "human" and whose id is acct: followed by the statement's signer). Material that is missing, or of the wrong type, fails the first link that needs it.
  5. Otherwise checked-correct. Who made the assertion is not verified by this version.

mandate.scope is reported for the mandate form only. It holds the record to the scope of the document in its mandate block (Record §9.9), and decides, in this order:

  1. signoff.expectation is neither checked-correct nor not-checked, confirmation_missing: not-checked, blocked_by:signoff.expectation.
  2. The mandate block carries no document, having no document or a document that is null (Record §9.6): not-checked, not_in_bundle:mandate_document.
  3. The document's v is not "tzun-mandate/1": abstain, unsupported_mandate_document.
  4. The document is not well formed under Record §9.7 and §9.8, its size included: checked-wrong, malformed_mandate_document.
  5. The decider is not one of its agents: checked-wrong, agent_not_in_mandate.
  6. Another constraint fails: checked-wrong, outside_mandate_scope, with the fact failed, the names of the constraints that fail, in this order: schemaVersion, aiSystemIds, actions, models, then predicate:<name> for each predicate of scope.predicates in byte order of name.
  7. confirm is false while a predicate of the document's confirm is true: checked-wrong, confirmation_evaded, with the fact confirm, the names of those predicates in byte order.
  8. A constraint is undeterminable, or confirm is false while no predicate of the document's confirm is true and one is undeterminable: not-checked, scope_undeterminable, with the fact undeterminable, the names of each such constraint (models, predicate:<name>, confirm:<name>) in that order and then in byte order of name.
  9. Otherwise checked-correct.

mandate.limits is reported for the mandate form only. It holds the record to the document's limits (Record §9.9), and decides, in this order:

  1. signoff.expectation is neither checked-correct nor not-checked, confirmation_missing: not-checked, blocked_by:signoff.expectation.
  2. The mandate block carries no document, as in mandate.scope step 2: not-checked, not_in_bundle:mandate_document.
  3. mandate.scope reports the document unsupported_mandate_document or malformed_mandate_document: not-checked, blocked_by:mandate.scope.
  4. totals does not have exactly one member for each cap, or the value at a cap's path is not an integer: checked-wrong, mandate_totals_malformed.
  5. mandateSeq is above limits.maxRecords, or a running total is below the record's own value at its cap's path or above the cap's max: checked-wrong, mandate_limit_exceeded.
  6. Otherwise checked-correct, with the facts slot, the expectation's mandateSeq, and totals, its totals as it stands.

mandate.key is reported for the mandate form, and for a record that carries an issuance block. It says whether a key is available to check the principal's signature, as signoff.key does for a record's own (§4.5):

  1. For the mandate form, signoff.expectation is checked-wrong: not-checked, blocked_by:signoff.expectation. For a record that is not of the mandate form, mandate.issuance is checked-wrong: not-checked, blocked_by:mandate.issuance.
  2. The bundle's credentials slot is not null: abstain, unsupported_signoff.
  3. Otherwise: not-checked, not_in_bundle:credential.

mandate.assertionTimestamp is reported when the mandate block, for the mandate form, or the issuance block, for an issuance record, is an object whose assertionTimestamp is not null: abstain, unsupported_artifact. This version does not verify a timestamp over an assertion.

5. The signature expectation check

signoff.expectation compares the sign-off material under a record's nonRepudiation with the record's payload.signoffExpectation, in the terms of Record §9. It reads no key and no signature, and so it answers the same whatever a signature check would say.

The expectation is authenticated by the record digest and by nothing else. The row is therefore not-checked, blocked_by:record.digest, unless record.digest is checked-correct. Under a schema whose hashed field set does not include signoffExpectation it is not-checked, not_applicable; no such schema is defined by this version.

Otherwise the rows of this table are tried in order, and the first that matches decides:

#signoffExpectationThe record carriesOutcome, reason
1Malformed (Record §9.2)anythingchecked-wrong, malformed_signoff_expectation
1anull, the policy form or the session forma mandate blockchecked-wrong, mandate_not_expected
2nulla session sign-offchecked-wrong, session_signoff_not_expected
3nulla sign-off block and an assertion that do not bind as a batch sign-offchecked-wrong, signoff_unbound
4nullanything elsechecked-correct
5the policy forma session sign-offchecked-wrong, session_signoff_not_expected
6the policy formno assertion, with or without a sign-off blocknot-checked, signature_missing
7the policy forma sign-off block and an assertion that bind as a batch sign-offchecked-correct
8the policy forma sign-off block and an assertion that do notchecked-wrong, signoff_unbound
9the policy formno sign-off block, and an assertion that binds as a per-record assertionchecked-correct
10the policy formno sign-off block, and an assertion that does notnot-checked, signature_missing
11the session formno sign-off blockchecked-wrong, signoff_stripped
12the session forma sign-off block whose statement has no digest (it is missing, not an object, or outside the value domain) or a digest other than statementDigestchecked-wrong, statement_digest_mismatch
13the session forma sign-off block whose sessionSeq is not an integer equal to the expectation'schecked-wrong, session_seq_mismatch
14the session formthat sign-off block, and no assertionchecked-wrong, signoff_stripped
15the session formthat sign-off block, and an assertion that does not present the statement challenge for statementDigestchecked-wrong, signoff_unbound
16the session formthat sign-off block, and an assertion that presents itchecked-correct
17the mandate forma session sign-offchecked-wrong, session_signoff_not_expected
17athe mandate forma sign-off block and an assertion that do not bind as a batch sign-offchecked-wrong, signoff_unbound
18the mandate formno mandate blockchecked-wrong, mandate_stripped
19the mandate forma mandate block whose statement is not a mandate statement of Record §9.6 in the value domainchecked-wrong, malformed_mandate_statement
20the mandate forma mandate block whose assertion has no assertion digest, or one other than mandateAssertionchecked-wrong, mandate_assertion_mismatch
21the mandate forma mandate block whose assertion does not present the statement challenge of its statementchecked-wrong, mandate_unbound
22the mandate forma mandate block with a document other than null whose digest (Record §9.7) is not the statement's mandateDigest, that has no digest, or whose principal.signer is not the statement's signer (a document whose principal is not an object with a signer member included)checked-wrong, mandate_document_mismatch
23the mandate form, confirm trueno confirmation that presents the record's confirmation challenge (Record §9.4)not-checked, confirmation_missing
24the mandate formanything elsechecked-correct

Row 1a reads only the mandate block. An issuance record carries its material as an issuance block, which the row mandate.issuance reads (§4.12).

What the outcomes say:

  • checked-correct says that the record carries the sign-off its expectation names, as far as that can be read without a key. It does not say that a signature verifies, and it does not compare a batch statement's policy with the record's policyDigest.
  • signature_missing is never final. A record under the policy form may receive its signature after capture, and a bundle alone cannot tell a signature still to come from one removed together with its assertion, or replaced by an assertion that binds nothing.
  • A session record without its sign-off is a finding, since a writer stores the two together (Record §9.1).
  • A sign-off that does not bind is a finding in every form: it has been moved from another record, relabelled or made up.
  • Under the mandate form, checked-correct says that the record carries the mandate block its expectation names: the principal's assertion has the digest the record committed to, it presents the challenge of the statement carried, and the document, when the block carries one, is the one that statement names, with the statement's signer as its principal. It does not say that the principal's signature verifies (mandate.key), nor that the record lies within the mandate (mandate.scope, mandate.limits, §4.12).
  • confirmation_missing is never final, as signature_missing is not: a confirmation is added after capture.

So a session sign-off copied onto another record fails on that record alone: session_seq_mismatch in another slot of the same session, statement_digest_mismatch in another session, and session_signoff_not_expected on a record that names no session. A mandate block copied onto another record fails the same way: mandate_assertion_mismatch on a record decided under another mandate, and mandate_not_expected on a record that names none. A record decided under the same mandate accepts the copy, and the copy then claims the slot that record committed to; that is seen only across the records of the mandate (Record §9.10).

The row carries no facts.

6. Report and exit codes

6.1 The report

A report is a JSON object with these members:

{
  "reportVersion": "tzun-verify-report/1",
  "bundleVersion": …,   // the bundle's bundleVersion value as it stands, or null
  "subject": …,         // the bundle's subject value as it stands, or null
  "inputs": { … },      // §6.2
  "checks": [ … ],      // §6.3
  "summary": { … },     // §6.4
  "checkedAt": "…"      // the instant the verification started; left out of comparisons
}

bundleVersion and subject are copied from a bundle that is a JSON object and has the member; otherwise they are null.

6.2 inputs

inputs names what the auditor supplied, by fingerprint only, so that a reader can tell which inputs produced the report:

MemberValue
trustAnchorsThe hex SHA-256 of the DER encoding of each supplied RFC 3161 root, sorted; [] when none. Intermediates are not listed.
evmHeaderSourcesOne { "chain", "finality" } per header source: chain is the CAIP-2 chain it serves, or null for a source that serves any chain; finality is the mode it declares (Anchor §10.4), written as the fact finality is (Anchor §10.7), null when it declares none. The source for any chain comes first, then the others in byte order of chain. A source's address never appears, since it can hold a credential.
publisherManifestThe hex SHA-256 of the publisher manifest's bytes as read, or null when no manifest is trusted. A verifier that is handed a parsed manifest rather than its bytes names it by the SHA-256 of its canonical serialization (Record §3); the two agree for a manifest file written in canonical form.
frameOrigins, vkeys, mdsRootsnull. Reserved for inputs this version does not take.

6.3 checks

checks is the list of rows in the order of §4.1. Each row is an object:

MemberValue
idThe row's id (§4)
outcomeOne of the four outcomes (§2.1)
reasonThe reason (§2.3). Left out on a checked-correct row.
factsAn object of the row's facts, left out when it has none. The facts of each row are named in §4 and in Anchor §10.7.

6.4 summary

The summary is derived from the rows and from the verifications behind them, never read from the bundle:

MemberValue
mode, coverage, manifestEnforced, keyKind, keyAssurance, uv, actorBoundnull. Reserved for sign-off verification, which this version does not define.
rungs[] (§2.4)
existedByThe earliest instant the report proves, as { "at", "source", "check" }, or null when it proves none. The bounds considered are: the record's anchor when anchor is checked-correct, with its existedBy and source ("evm-calldata" or "rfc3161", Anchor §10.6) and check "anchor"; the composed anchor when anchor.newer is checked-correct, likewise with check "anchor.newer"; and the record's own timestamp when ts.record.token and ts.record.trust are both checked-correct, with its genTime, source "record-timestamp" and check "ts.record.trust". Instants are compared to the millisecond, the digits beyond it dropped, as §6.5 writes them, so two that differ only below the millisecond are equal; of equal instants, the first in that order is named.
anchorNetworkTaken from anchorNetwork (Anchor §10.6) of the record's anchor and of the composed anchor, over those the verifier verified: their common value when both give the same one, the one value when only one gives a value, and null when they give different values or none gives one
anchorNamespaceThe same rule over anchorNamespace
anchorOriginAuthenticatedtrue when anchorOriginAuthenticated is true for the record's anchor or for the composed anchor, false otherwise
outcomesAn object keyed by outcome, with a member for each of the four (§2.1) even when its count is 0: how many rows have that outcome

The record's anchor counts as verified when the record has an anchor artifact and record.digest is checked-correct; the composed anchor, when anchor.newer is reported and not blocked.

6.5 Serialization and comparison

A report is written in canonical form (Record §3). Instants in a report (checkedAt, the genTime facts, existedBy.at) are UTC, written YYYY-MM-DDTHH:MM:SS.sssZ with exactly three fractional digits, finer precision truncated. Two reports are equal when their canonical forms, without checkedAt, are the same bytes. A verifier that cannot write its report in canonical form, for example because the bundle's subject holds a lone surrogate, exits 2.

6.6 Exit codes

CodeMeaning
0No row is checked-wrong, every row whose artifact is present is checked-correct, and the regime is met
1Some row is checked-wrong
2The bundle could not be read, the row bundle is not checked-correct, or the report could not be written
3The regime's require or requireRung is not met (§3.2)
4Without allowUnchecked: some row abstains, or is not-checked for any reason other than absent, not_applicable, or blocked_by a row that is checked-wrong

The first code of the order 2, 1, 3, 4, 0 that applies is the exit code. The exemption of a row blocked by a failed row cannot change it, since any failed row gives 1 first.

Code 4 keeps an undecided artifact from reading as success: a bundle whose artifacts all abstained, for example because a forged future version tag stands in place of a failing one, would otherwise exit 0.

Exit 0 says nothing about artifacts that are absent. A record with no anchor artifact, no anchoring and no anchorPending reads absent on every anchor row, and absent does not lead to exit code 4. An auditor whose regime needs an anchor MUST require it: anchor for a record that carries its own artifact, or anchor.newer for one exported before its own artifact is written. One who needs the record's own timestamp requires ts.record.trust.

Informative. A record therefore exits 4, unless the auditor allows unchecked rows, whenever it carries something this version does not verify: a decider kind or identifier scheme it does not know, a finalized-block reference without a header source for its chain, human decision material bound to a decider of an agent kind (material_unverified), a pipeline, any link (each links[<i>].edge abstains, and so does a link of a relation this version does not register), a mandate document of another version, a confirmation still owed, an assertion timestamp, a payload member outside the twelve of Record §2.2, every record decided under a mandate and every record that carries an issuance block, since mandate.key is never decided. The rows of §4.9 to §4.11 read absent where the record leaves their member null, which does not lead to exit code 4.

An error in the verifier's invocation, such as an unknown option or an auditor input that cannot be read, is not a verification result, and a command-line verifier MUST NOT report it with a code from 0 to 4. Informative: tzun-verify uses 64, and refuses a value given to a switch (--allow-unchecked=false), so that a wrapper cannot turn a switch on by accident.

6.7 Human-readable output

A verifier's human-readable output MUST print every not-checked and every abstain row with its reason, and every rung. A script reads the exit code; a person reads what is not checked.

7. Conformance

Specs §6 states what conformance requires, and Bundle §13 how a corpus case is replayed: for every case of the b1 corpus, the report compared as in §6.5, and the exit code, are those the case states. Where the corpus and this specification disagree, this specification governs (Specs §7).