The verifier block
September 18, 2026 · View on GitHub
Status: 6.1.0, self-declared. Implemented in src/proofbundle/verifier_block.py, carried by
agent-review/v0.3 receipts under producer.verifier, produced for a conformance run by
conformance/run_conformance.py --test-result-out. The module is the enforced validator; this
page explains it. Conformance cases live under conformance/agent_review/ (rule P19).
The question it answers
A relying party reading a receipt at T+n asked, on the SCITT list on 2026-09-10: how do I establish that the verifier instance which signed this receipt was an implementation that met the cited conformance floor, and that code and configuration at T were the tested ones? A configuration hash prevents reconfiguration after the receipt; it does not show the configuration was the tested one. An issuer URI says which verifier, not which build.
Measured against proofbundle 6.0.0 on 2026-09-12, the question applied to this package word for
word. proofbundle bound what decided (the policy, by a digest over its bytes, recomputed at
verify time rather than read from the receipt) and what was decided about (subject, diff,
visible block, findings root). It did not bind what it was decided with: no receipt and no
verify result carried the version, the wheel digest or a build digest of the verifier that produced
it. The only place a tool version reached a receipt was a CLI template writing {"proofbundle": "6.0.0"} into a field the verifier never reads. Two wheels of the same version were
indistinguishable from the receipt.
What the block carries
"producer": {
"id": "b7n0de-release-runner",
"verifier": {
"implementation": "proofbundle",
"version": "6.1.0",
"build": {"digest": {"sha256": "<64 hex>"}, "source": "installed-record", "files": 77},
"vectorSet": {"name": "proofbundle.conformance.manifest.v1",
"digest": {"sha256": "<64 hex>"}, "cases": 115},
"testResult": {"predicateType": "https://in-toto.io/attestation/test-result/v0.1",
"result": "PASSED", "statementDigest": {"sha256": "<64 hex>"}},
"assurance": "selfDeclared"
}
}
| Field | Measured from | Required |
|---|---|---|
implementation | the package name | yes |
version | proofbundle.__version__ — a name, deliberately not the identity | yes |
build.digest | a digest over the package's own files; see build.source | yes |
build.source | installed-record: the sha256 of every proofbundle/ row the installer wrote into RECORD (path and per-file sha256), identical for every install of the same wheel · source-tree: a digest over the package directory's files, for an editable install or a checkout on PYTHONPATH | yes |
build.files | how many files went into the digest | no |
vectorSet | the conformance corpus the build was held against: the manifest's schema name, the case count, and a digest over the manifest and every file of every case directory it names — not the manifest alone, because a manifest lists directories and a directory is not bytes | no |
testResult | a reference to a separate signed object (below): its predicate type, its result, and the sha256 over its RFC 8785 canonical bytes | no |
assurance | always selfDeclared | yes |
Every key set is closed. An unknown field is refused, not ignored: a field nobody validates is a field a producer can put anything into. The two build sources are different measurements and are compared only with themselves; neither is the sha256 of the wheel file on PyPI, and the block says which one it is.
The separate object: an in-toto test-result statement
python conformance/run_conformance.py --test-result-out statement.json writes the run as an
in-toto statement of predicate type https://in-toto.io/attestation/test-result/v0.1:
- subject: the build digest, measured the same way the block measures it;
- configuration: one resource descriptor for the vector set, with its digest and case count;
- result: the corpus rule that a skipped check is never a passed one — any failed case is
FAILED; no failure but any case that ran partially or not at all isWARNED;PASSEDonly when every case ran in full; - passedTests / warnedTests / failedTests: the case ids, so a reduction of scope is in the statement and not only in a headline.
The statement is written unsigned. Signing is the producer's step
(verifier_block.sign_test_result_statement, the same DSSE primitive every receipt of this package
uses); a runner signing with a key of its own would be one more identity nobody can look up. The
receipt cites the statement by the digest of its canonical bytes, and a relying party joins the
two by equality: join_test_result(block, statement) reports four equalities separately —
subject equals the block's build digest, canonical digest equals the cited one, result equals the
cited one, and the statement's configuration names the vector set the block declares — and
ok only when all four hold. The issuer is not trusted for the join. A block that cites a run
must declare its vector set; without one the join would have nothing to hold the statement's
configuration against, and a statement over one corpus could be cited beside a block declaring
another (found by review on 2026-09-18, before this landed).
What the verifier reports
verify_agent_review_v02 carries a verifier_block axis:
| Field | Meaning |
|---|---|
present, valid | whether the receipt carries a block, and whether its form holds |
build_digest, build_source, vector_set_digest, vector_set_cases, test_result | what the block names |
matches_this_verifier | MATCH / MISMATCH / NOT_EVALUATED — whether the build running this verification is the build the block names. Three states, and the third is not a pass: no block, no comparable measurement, or builds measured from different sources. |
The axis is reported, never folded into ok. A malformed block is a structural error and
fails the receipt, like any malformed field. A well-formed block naming another build does not
invalidate the receipt: it is a receipt whose producer you can now name.
Version rule, stated rather than glossed
The block lives in a new predicate version, agent-review/v0.3 (6.1.0): v0.2 plus exactly one
optional field, producer.verifier. Nothing else changes — time semantics, observation rules,
the closed field sets, the policy handling are v0.2's, and verify_agent_review_v03 is the v0.2
verifier with the block admitted and reported.
Why a new version and not v0.2 with the block: the first draft of 6.1.0 did exactly that, and two
reviewers measured the consequence on 2026-09-18 with the tagged 6.0.0 validator. The same bytes —
predicateType …/v0.2 carrying producer.verifier — were rejected by 6.0.0
(producer.verifier is not an allowed field) and accepted by 6.1.0. Two verdicts over one
receipt, distinguished by nothing a reader can see in the receipt. The predicate's own version rule
says a change to what a verifier must reject is a new version, so the reader can tell from the
predicateType alone which validator judged it. Hence v0.3.
What follows from that, stated rather than glossed: v0.2 stays exactly what 6.0.0 shipped, and a
v0.2 receipt carrying the block is refused by every verifier, 6.0.0 and 6.1.0 alike. v0.1
refuses it as it always refused unknown fields. verify_agent_review_any routes by predicateType
and reports both v0.2 and v0.3 as current — v0.3 does not deprecate v0.2. A producer that needs
6.0.0 readability keeps issuing v0.2 without the block. The emitter picks v0.3 exactly when the
predicate carries the block; there is no parameter that chooses it, the version follows the object.
Which version to issue, stated plainly: issue v0.3 with the block whenever your readers run 6.1.0 or later — it is the version that answers which build produced this. Issue v0.2 only when a reader on 6.0.0 must verify the receipt; v0.2 is not deprecated, but it cannot carry the block.
Honest limits
- Self-declared. The producing build measures itself. A producer that lies about its build digest signs a lie; the signature makes the lie forgery-resistant, not correct. A block observed by a runner or witnessed independently needs a witness outside the producer, which this version does not provide.
- A digest over files, not over a wheel.
installed-recordidentifies the installed file set of a wheel;source-treeidentifies a checkout. Neither is the artifact digest on PyPI. Joining the block to a published wheel is a further step and is not claimed here. - A
source-treedigest is local. It names the files of one checkout at one moment; another checkout of the same commit reproduces it only if the same files are present, and an editable install moves it with every edit. It serves a local audit trail. For a receipt that is verified elsewhere, produce it from an installed distribution so the block carriesinstalled-record. - The vector set digest pins what the manifest names. A case directory not listed in the manifest is not part of the corpus and not part of the digest — which is the corpus's own rule.
- It does not make the conformance claim true. What a relying party gains is a joinable, digest-bound statement instead of a version string.
Every rule brings its counter-proof
conformance/agent_review/ carries a positive control (a v0.3 predicate with a block is valid) and
three counter-proofs (a block that raises its own assurance, a build digest that is not a sha256,
an unknown field) plus one v0.1 counter-proof (the old version refuses the block). Each
counter-proof has a flip test in tests/test_agent_review_conformance_runner.py that removes
exactly its defect and expects the verdict to turn. The measured behaviour — a real build, the real
corpus, the join, the DSSE round trip — is held by tests/test_verifier_block.py.