Repository navigation
A feature release following 6.4.0, with an offline Sigstore bundle verifier and stricter in-toto statement checks. It adds proofbundle verify-sigstore, which verifies GitHub Artifact Attestations and npm provenance offline against the trusted root, the artifact digest and the identity the relying party supplies. The verifiers for eval-result v0.1, test-result v0.1 and SVR v0.1 statements now refuse statements that lack the members their predicates require or that carry them in another form, and every rfc3339z time field must name a real calendar instant. Statements that a released version accepted with such members missing or malformed no longer verify. That includes statements that earlier exporters of this package emitted from caller-supplied time, claim timestamp or test-result url values in another form; the CHANGELOG names each case.
python -m pip install --upgrade proofbundle==6.5.0
Highlights
proofbundle verify-sigstoreverifies GitHub Artifact Attestations and npm provenance offline. It checks one Sigstore bundle under the profilesgithub-artifact-attestation,npm-provenanceandnpm-publish, from the Fulcio leaf and its embedded SCT to the inclusion proof and the checkpoint, and an in-toto Statement with a SLSA Provenance v1 predicate. It imports no Sigstore client.safeForAutomationis false in every verdict, since revocation and log consistency over time are not checked.- Stricter checks for eval-result v0.1, test-result v0.1 and SVR v0.1 statements. An eval-result v0.1 statement must carry its eight required members, at least one claim, and the required forms of
verifier,suiteandsubjectProfile. A test-result v0.1 statement must carryresultandconfigurationwith the required types. An SVR v0.1 statement must carryverifier,verifier.id,timeCreatedandproperties. The affected URI and timestamp fields are now validated as described in the entries below.proofbundle intoto --verifyandproofbundle svr --verifyreport the verifier's failure reasons; the meanings of their exit codes are unchanged. - Time fields name a real instant. Every
rfc3339ztime field checks month, day in month, hour, minute and second after its form, sodecision verify --strictrefuses adecidedAtof2026-13-15T09:05:00Z. The Rust verifier applies the same rule to the one time field it checks. - Stricter input and binding checks, and lower memory use in five JSON traversals. The Bitcoin header map in the trust policy,
--bitcoin-header,--bundled-header, the agent-review policy file and the SD-JWT bundle-binding check now reject inputs or bindings they previously accepted. Five JSON traversals carry paths as links and render path text only when needed for a message, keeping memory use proportional to the input and any supplied path declarations.
Verify this release yourself
Use the checksums and build provenance to check the downloaded files. To check the signed pre-tag audit receipt, use a clone at the source commit identified by the build attestation. The commands, prerequisites and limits are in Verifying a published release.
What a receipt still does not prove
A valid signature binds the signed content to the signing key. It does not make a reported result true.
decision verify and outcome verify do not implement the complete offline verification procedure in draft-gruszka-audit-decision-receipts-05. They do not perform all the procedure's checks for approval records, action bytes, executor identity, separation of keys and identifiers, effects and the execution-time window. decision verify also exits 0 when the evaluation time is before decidedAt, although Section 3.2 step 9 of revision 05 requires decidedAt <= T.
A Statement with two subject entries, which Section 3.2 step 6 does not allow, exits 0 without --require-derived-subject and 2 with that switch. Both measurements use --strict, the gate-1 public key, --aud observer-1, --nonce dec-0002 and --verification-time 2026-10-15T09:06:00Z. The valid V2 control still exits 0 with the switch. These command results are not verdicts of full conformance to revision 05.
Without --policy, decision verify does not establish that decisionMaker.id is the identity trusted for the signing key. In vector F3 of Appendix A.5 of draft-gruszka-audit-decision-receipts-05, the receipt names gate-2 under the gate-1 key. With --strict, --aud observer-1, --nonce dec-0002 and --verification-time 2026-10-15T09:06:00Z, it exits 0 without a policy and reports POLICY: NOT_EVALUATED. The same invocation exits 3 when --policy supplies a trusted_decision_makers mapping from that key to gate-1. These results depend on the stated verification time; they do not mean that an expired receipt passes without a policy. The command does not read or hash the evidence bytes referenced by evidenceRefs, with or without that test policy.
Measured on the reference machine
The budget cost axis was measured on the reference machine on 2026-11-06 using the code at 14fe339. The record identifies the run as a reference measurement (ist_referenzmessung=true). All 12 expected axes and all 7 expected combinations passed, and no axis was skipped. The recorded calibration factor maschinenfaktor_schnellstes_ende is 1.036, and the timing-limit setting is latte_aus_der_klammer=schnellstes_ende. These results apply to the listed axes and combinations, not to every reader or input. The measurement record lists every axis and combination.
Details
All 20 entries, one line each, as in the CHANGELOG
Added
proofbundle verify-sigstoreverifies GitHub Artifact Attestations and npm provenance offline.proofbundle.adapters.sigstore_bundle.verify_sigstore_bundlechecks one Sigstore bundle against the trusted root, the artifact digest and the identity the relying party supplies. It reads media types 0.1 to 0.3, a DSSE envelope and Rekor v1 entries of kinddsseandintoto, under three profiles:github-artifact-attestation,npm-provenanceandnpm-publish. It verifies the Fulcio leaf, its chain at every verified time and its embedded SCT, the signed entry timestamp, the inclusion proof and the checkpoint, and an in-toto Statement with a SLSA Provenance v1 predicate under the rules of the two GitHub workflow buildTypes, their four allowed events included. For npm-publish, supplying the version document also checks itsdist.signaturesagainst the registry's keys. For npm-provenance, supplying package.json also checks its package identity and repository. The reader does not open the tarball; the relying party must supply package.json from the artifact whose digest it provides. Whether the predicate agrees with the certificate is reported as a note, because those rules come from the producers' code and not from a specification. It was written from the specifications pinned inSPEC_PINand imports no Sigstore client. Unknown members of the DSSE envelope are refused, a deviation from DSSE thatdocs/SIGSTORE_BUNDLE_VERIFIER.mdnames. The exit codes follow the TRACE reader: 0 verified, 1 a cryptographic or structural failure, 2 unreadable input or a feature not implemented, 3 an unmet requirement of the relying party.safeForAutomationis false in every verdict, since revocation and log consistency over time are not checked. Each vector oftests/fixtures/sigstore_bundle/fails at the step it names, and public bundles of the npm registry and of the cli/cli test data verify offline against the vendored public-good root. It is Python only, and its parity register row is PENDING.
Changed
- docs/predicates/action-outcome.md no longer lists the attestation of
executor.idas closed, and docs/NON_CLAIMS.md names the outcome checks proofbundle does not perform. The trust-pack check bindsexecutor.keyIdto the signing key and to theoutcomeExecutorsrole; the pack names no identifier per key, so an anchored executor key can write anyexecutor.idand the outcome verifies. NON_CLAIMS.md now says thatexecutor.idis the signer's own statement, and names the inputs of draft-gruszka-audit-decision-receipts-05 that the decision and outcome verifiers do not take: the expectedexecutor.idand the approval record of Case 1, which the draft requires, and the effect and action bytes, which it calls optional. The binding, the two required inputs and the effect bytes are scheduled for 6.6.0. No verdict changes. - The texts name the two base64 alphabets the DSSE verifiers read, and say that
passedis optional in eval-result v0.1. SPEC.md section 7b said the test-resultpayloadis standard base64 and not base64url, while the verifiers of this version read both alphabets, as the DSSE envelope allows, for SVR, test-result and eval-result alike; the text now names both, padded, and docs/IN_TOTO_PROFILE.md says the same for all three verifiers. The v0.1 claim row says that a claim withoutpassedverifies and carries no verdict, and that a consumer who needs one reads v0.2. No verdict changes. - The
[scitt]extra and the dev pins take cbor2 6.1.5, measured as the pin asks.tools/scitt_ccf_external/reader_crosscheck.jsoncarries the run cbor2-6.1.5 of 2026-10-29, made withreader_crosscheck.pyin the environment of the 6.1.4 run. Of the 70 values the run of 6.1.4 recorded, each is equal in the run of 6.1.5 but the cbor2 version, and the data-hash recomputed with the strict options equals the receipt's on 4 of 4 real statements.reader_crosscheck.pynow holds each of the five files it reads against the size and sha256 pin offetch_external.pybefore it measures, stops on a difference without writing, and records the five underinputs; earlier runs carry noinputs.tests/test_scitt_pin_has_a_measured_run.pyholds the scitt and dev pins to one exact version with a run of that version made from the pinned files, and names a cbor2 requirement anywhere else inpyproject.toml, so a bump without the measurement fails a test; the rule was only a comment, and the automated bump passed every check without it. - The trust-policy schema declares the relation pins of SPEC.md §7j and §7k (slices 01 and 02 of the agent package).
schemas/trust_policy_v0_1.schema.jsonlistsrelation_signer,require_relation_targetandreject_retracted, and SPEC.md, CONFORMANCE.md, EVAL_CLAIM.md, conformance/README.md, docs/AGENT_REVIEW_PREDICATE.md, docs/RECEIPT_ENVELOPE_PROFILE.md, docs/adr/0002-universal-content-root.md and the predicate pages for action-outcome, decision-receipt and relation state what the conformance vectors read. No module of the package changes. - The conformance corpus has three successor cases, and the readiness pack carries a self-receipt under a new key (slice 03 of the agent package). Two cases under
conformance/action_chain_content_roots/and one underconformance/provenance/are new, andconformance/manifest.jsonlists them. The self-receipt underdocs/readiness_pack/was regenerated with a new key, whichreadiness_pack.pub.b64holds. docs/ANCHORS.md states the OpenTimestamps proof format, docs/predicates/decision-receipt.md the findings of a decision receipt in the default mode, conformance/README.md the axes of decision_crossimpl, and CROSS_IMPLEMENTATION_REPORT.md the corpus of 148 cases, 71 of them reproduced in Rust. No module of the package changes. - The CAP-1 draft and its schema are vendored, and the trust-policy schema declares the pins the policy loader reads (slices 04 to 13 of the agent package).
conformance/cap1/holds the CAP-1 draft, its JSON schema and their licence files.schemas/trust_policy_v0_1.schema.jsondeclaressd_jwt.issuer_key_pinand nine keys of the policy loader:require_audience,require_nonce,require_not_checked,require_decision_change_conditions,require_trace_context,allow_raw_inputs,trusted_tsa_roots,trusted_tsa_policy_oidsandbitcoin_block_headers. The texts of SPEC.md, EVAL_CLAIM.md, conformance/README.md, docs/AGENT_REVIEW_PREDICATE.md, docs/ANCHORS.md, docs/RECEIPT_ENVELOPE_PROFILE.md, docs/SD_JWT_VC_PROFILE.md, docs/VERIFIER_BLOCK.md, ADR 0002 and the three predicate pages state what the verifier, the predicates and the conformance vectors do. No module of the package changes.
Fixed
- Every
rfc3339ztime field must name a real instant, not only have its form.decision verify --strictexited 0 on a decision receipt whosedecidedAtwas2026-13-15T09:05:00Zor2026-10-32T09:05:00Z, although docs/predicates/decision-receipt.md section 5 states time fields are RFC 3339 under its fail-closed rules, and the same value invalidity.expiresAtalready failed closed._schema_shapes.RFC3339_Znow checks month, day in month and year, hour, minute and second (to 60, the leap second of RFC 3339 5.7) after the form, the ruleintoto._is_rfc3339already applied, and the decision, outcome, relation, run ledger, subject binding, trust pack and verification summary checks read it. agent-review reads the same check instead of a private copy of the form; by maintainer decision this applies to v0.1, v0.2 and v0.3 rather than introducing a new version, as for the ASCII digit fix, and no receipt or vector in the repository holds a time value of the form that is not a real instant. The Rust verifier in tools/pb_verify_rs applies the same rule to thedeclaredAtof a relationship edge, its one time check, so both verifiers give a receipt with such an edge the same verdict and exit code. The JSON Schema patterns stay the form; a validator refuses more than its schema here. - A copy of an owner's release with a later answer no longer extends the window of its identity. The plugin's hook, and the signer service with the same code, ran the window of a release from the answer of the copy it was reading, so for a card not used yet a copy answered later opened a later window. The window of an identity (card, sha256, session) now runs from the earliest answer the gate has read for it, every copy in the folder
freigabenand the recordfreigaben-erste.jsonin the plugin data directory included, which the gate never moves later; a record that does not read, or that has to change and cannot be written, releases nothing. A copy the gate never read stays unknown to it, asplugins/proofbundle/DECISIONS.mdD119 names (tests/test_plugin_release_window_first_answer_d119.py). This entry concerns the plugin underplugins/, which the package does not carry; it ships with plugin 0.4.0. proofbundle intoto --verifyandproofbundle svr --verifyname the reason of every check that does not hold. Both printed a reason under the FAIL line only for the Statement layer and, for eval-result v0.2, the predicate shape. An envelope with emptysignatures, a payload in base64 without its padding, a signature under another key, anotherpayloadType, a payload that is not canonical for its declaredcontentRootAlgand a claim field the claim rule refuses printed[FAIL]and=> FAILEDand nothing between, andsvr --verifydid so for anotherpredicateTypeas well. The commands now print the part of the verifier's merged reason they had not printed, and the signature on its own line wherever the DSSE check ran;intoto --verifyalso names apredicateTypethat is neither eval-result version. The exit codes do not change.- An SVR, eval-result v0.1 or test-result statement verifies only when its URI and timestamp strings have that form. svr v0.1 types
verifier.idas TypeURI andtimeCreatedas Timestamp, docs/IN_TOTO_PROFILE.md types theverifier.idof eval-result v0.1 as TypeURI, and the test-result predicate typesurlas ResourceURI. For theevaluatedAtof eval-result v0.1 the profile named no form, only the time the eval ran; RFC 3339 was the form of the v0.2 time fields, and the profile now states it for v0.1 as well. The verifiers judged these members by their JSON type alone, so a validly signed statement verified with "not a uri" in each URI member and "yesterday" in each timestamp member, andproofbundle svr --verifyandproofbundle intoto --verifyanswered PASS with exit 0, as measured on main b55d736. By the owner's choice of 2026-10-31 they are now read with the readers eval-result v0.2 uses, a URI with a scheme and an RFC 3339 date-time with an offset;urlis read in URI form only and never fetched. Such an SVR is malformed, exit 2, and such an eval-result or test-result statement does not verify, withpredicate_claim_okFalse and a reason naming the member.export_svr_dsserefuses atime_created,to_test_result_statementaurlandto_eval_result_predicatea claim timestamp in another form, so no producer signs what its verifier refuses. The 14 entries of tests/fixtures/eval_result_v0_1/corpus.json verify as before. Earlier released exporters accepted caller-suppliedtime_created, claim timestamps and test-resulturlvalues without validating these forms; a statement emitted with nonconforming values no longer verifies. Two cases oftests/test_cli_text_surface_rt06.pyused "x" and "t" as a well-formed SVR; they now carry a URI and an RFC 3339 time. - An eval-result v0.1 statement verifies only when
claimscarries at least one claim. docs/IN_TOTO_PROFILE.md listedclaims[]without a minimum, and the v0.2 draft requires one or more. A validly signed v0.1 statement whoseclaimswas an empty array verified, andproofbundle intoto --verifyprinted PASS with exit 0, as measured on main b55d736 and with the other fixes of this release. By the owner's choice of 2026-10-31 such a statement now does not verify, withpredicate_claim_okFalse and a reason namingclaims, and the command answers FAIL with exit 1. Aclaimsthat is no array stays the claim rule's reason. The 14 statements of tests/fixtures/eval_result_v0_1/corpus.json, five from the published 6.1.0 wheel and nine rebuilt from the source of nine tags between v2.0.0 and v6.0.0, carry exactly one claim each and verify as before, andto_eval_result_predicatealways writes one; a predicate a caller built is not covered. - An eval-result v0.1 statement verifies only when
verifier,suiteandsubjectProfilehave the form v0.1 gives them. docs/IN_TOTO_PROFILE.md listsverifier.id,suiteas{name, version}andsubjectProfileas one of the three subject profiles, none marked optional. Pull request 445 made the eight top-level members required and left the value of a present member to the claim rule, which never readsverifierorsubjectProfileand judgessuiteonly through the parts it carries, so a validly signed v0.1 statement verified, andproofbundle intoto --verifyprinted PASS with exit 0, withverifier{}or a string, withverifier.id5, withsuitelackingnameorversion, and withsubjectProfile7 or "bogus_profile", as measured on main b55d736. Such a statement now does not verify, withpredicate_claim_okFalse and a reason naming the member, and the command answers FAIL with exit 1. The 14 statements of tests/fixtures/eval_result_v0_1/corpus.json verify as before. - An SVR v0.1 statement verifies only when its predicate carries
verifier,verifier.id,timeCreatedandproperties. The in-toto SVR predicate in its v0.1 version marks all four required, butclassify_svr_predicate_shapejudgedverifierandtimeCreatedonly when present and never looked atverifier.id, so a validly signed statement of that type withoutverifier, withoutverifier.idor withouttimeCreated, or with a number forverifier.id, verified withverify_svr_dsseandproofbundle svr --verifyprinted PASS with exit 0, as measured on main b55d736. Such a statement now does not verify, withpredicate_shape_okFalse and a reason naming the member, and the command answers it as malformed with exit 2. The extension fieldverifier.policystays optional, and every statementexport_svr_dssewrites carries all four. - A test-result v0.1 statement verifies only when the members it carries have the types the predicate gives them. The in-toto test-result predicate makes
resultone ofPASSED,WARNEDandFAILED,urla string, the case lists arrays of strings andconfigurationa list of ResourceDescriptors, each of which must specify one ofuri,digestorcontent, and SPEC.md section 7b calls a barename-only descriptor invalid.verify_intoto_dssejudged these types only beside a descriptor of ours that annotates a verdict, so a validly signed generic statement verified withresult1, "BOGUS" or "passed", with a configuration entry{}or{"name": "only-a-name"}, withurl5 or with a case list that was no array of strings, as measured on main b55d736. Such a statement now does not verify, withpredicate_claim_okFalse and a reason naming the member. A configuration entry withoutdigeststill makes no claim when it names its resource byuriorcontent, and every statementto_test_result_statementwrites has these types. - A test-result v0.1 statement verifies only when its predicate carries
resultandconfiguration. The in-toto test-result predicate marks both required, and SPEC.md section 7b says the same ofconfiguration, butverify_intoto_dssejudged only the members a statement happened to carry, so a validly signed statement of that type could verify when its predicate omitted both required members, as measured on main 647ed0a. A statement without one of the two now does not verify, withpredicate_claim_okFalse and a reason naming the first missing member.urland the case lists stay optional, a generic statement that carries both members verifies as before, and every statementto_test_result_statementwrites carries both. - An eval-result v0.1 statement verifies only when its predicate carries the members v0.1 requires.
verify_eval_result_dsseandproofbundle intoto --verifychecked a v0.1 predicate for its signature, content root and type and judged the claim fields it happened to carry, so a statement of that type whose predicate carried none of these members verified, and the command printed PASS with exit 0, measured on main 647ed0a. docs/IN_TOTO_PROFILE.md lists them, and the same page applies the Monotonic Principle, a verifier denies unless a valid attestation exists.verifier,evaluatedAt,suite,claims,sampleSize,commitmentswithmodelanddataset,assuranceLevelandsubjectProfileare now required; a missing one is a reason withpredicate_claim_okFalse, and the command prints FAIL and exits 1, as for a v0.2 statement of the wrong shape.preRegistration,receiptandharnessstay optional, an unknown member stays unread, and no v0.2 shape applies to v0.1. The 14 statements oftests/fixtures/eval_result_v0_1/corpus.json, five from the published 6.1.0 wheel and nine rebuilt from the source of nine tags between v2.0.0 and v6.0.0, carry all of them and verify as before; a predicate a caller built is not covered.tests/test_a_v01_statement_carries_its_required_members.pyholds the eight members, the two parts ofcommitments, the optional members and the corpus; the C2 case oftests/test_intoto_content_root_migration.py, the dispatch case oftests/test_intoto_eval_result_v02.pyand the eval-result predicate oftests/test_the_intoto_verifiers_read_the_statement_layer.pysigned a predicate without them and now carry them. - Five walks over a JSON value keep their memory in proportion to the input and to the declaration they are given.
trace_record._canonicalizableandtrace_record._canonicalizable_value,_plain_value.plain_json,subject_binding.nested_closure_violationsandsubject_binding.nested_type_violationsgave every child a path string that copied the text of all its ancestors, so a deep input with long member names cost memory in proportion to its children times the length of the path above them. A trace record of 64301 bytes needed 120 MB of traced memory, and under a 600 MiB address-space limitverify_trace_recordraised a rawMemoryErrorbefore any signature was checked, in the check that the record has an RFC 8785 form, which runs before the signature; the structure check runs after it. Each walk now carries the path as a link and renders it only for a message, throughbudget._chain_text, at most 256 characters with a cut marked…. The closure and the type walk ofsubject_bindingalso kept every prefix of every declared path as a string of its own and named a declared path whole, so a declaration of 400 names of 100 characters, passed by a direct caller, needed 8.1 MB of traced memory and a message on it ran past the limit. They now keep a position in the sorted declared paths and build no path text, and every message goes throughbudget._chain_text. A dict subclass takes the same way: the reading at the call (canonical._ein_stand) copies it into a plain dict before either walk sees it, so its owngetis never asked. A message about a short path is the message it was before. The open member walk of a trace record names at most 64 values without an RFC 8785 form and counts the rest in one finding.tests/test_walk_paths_are_links_until_a_message_needs_them.pyholds all five walks under 50 times the input, compares 800 generated inputs with a recursive reference walk, 400 for the closure walk and 400 for the type walk, checks 400 refusals ofplain_jsonagainst the path where the generator planted the refused value, andtests/test_a_declared_path_costs_no_text_per_prefix.pyholds the long declaration under 50 times its size and checks the position against the prefix set on 400 generated declarations. - Four readers refuse what they let through: the Bitcoin header map of the trust policy,
--bitcoin-header, the agent-review policy file and the SD-JWT bundle binding. The trust-policy loader takesanchors.bitcoin_block_headersonly with heights of ASCII digits and roots of exactly 64 hexadecimal digits; it askedstr.isdigit(), which also holds for digits of other scripts, and measured only the length of a root.schemas/trust_policy_v0_1.schema.jsonstates both patterns.--bitcoin-headertakes heights of ASCII digits only, so such a height is malformed input (exit 2) instead of an anchor requirement that stays unmet (exit 3).agent_review.load_policyreads the policy file with the strict JSON reader, so NaN, Infinity, 1e400, an integer outside ±(2^53 − 1), nesting past the depth bound, more members than the node budget and a lone surrogate are refused asAgentReviewError, as a duplicate key was before.sdjwt_issue.check_binds_bundlebinds each ofpassed,threshold,comparator,suiteandissueronly when both sides carry it and both values write the same RFC 8785 bytes:1no longer stands fortruenor0forfalse, a member absent from either side no longer matches anull, and1still equals1.0. A value the canonicalizer refuses does not bind. The mutation operator WP-C1 targets the new comparison, and four new test files hold the four readers. --bitcoin-headerand--bundled-headerread a header as the trust-policy loader does. Both took a merkle root throughbytes.fromhex, which accepts ASCII white space between the byte pairs, and--bitcoin-headerput such a root, white space and all, into the relying party's header map, which a policy file refuses.--bundled-headerofanchor upgradealso askedstr.isdigit()of a height, which holds for digits of other scripts. Both now readHEIGHT:MERKLEROOT_HEXthrough one function with the loader's two patterns: a height of ASCII digits and a root of 64 hex digits, each after the white space around it is stripped, and anything else is malformed input (exit 2).tests/test_both_header_options_build_what_the_policy_takes.pycompares both readers with a hand-written oracle on 600 generated specs.
Full changelog · Known limitations for 6.5.0 · Earlier risk record · Release scope · proofbundle.dev
The plugin under plugins/ is not part of this package. It is released on its own as plugin 0.4.0, with release notes of its own.
Assisted by little helpers.