Skip to content

v6.5.0

Latest

Choose a tag to compare

@github-actions github-actions released this 08 Nov 16:02
· 172 commits to main since this release

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-sigstore verifies GitHub Artifact Attestations and npm provenance offline. It checks one Sigstore bundle under the profiles github-artifact-attestation, npm-provenance and npm-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. safeForAutomation is 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, suite and subjectProfile. A test-result v0.1 statement must carry result and configuration with the required types. An SVR v0.1 statement must carry verifier, verifier.id, timeCreated and properties. The affected URI and timestamp fields are now validated as described in the entries below. proofbundle intoto --verify and proofbundle svr --verify report the verifier's failure reasons; the meanings of their exit codes are unchanged.
  • Time fields name a real instant. Every rfc3339z time field checks month, day in month, hour, minute and second after its form, so decision verify --strict refuses a decidedAt of 2026-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-sigstore verifies GitHub Artifact Attestations and npm provenance offline. proofbundle.adapters.sigstore_bundle.verify_sigstore_bundle checks 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 kind dsse and intoto, under three profiles: github-artifact-attestation, npm-provenance and npm-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 its dist.signatures against 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 in SPEC_PIN and imports no Sigstore client. Unknown members of the DSSE envelope are refused, a deviation from DSSE that docs/SIGSTORE_BUNDLE_VERIFIER.md names. 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. safeForAutomation is false in every verdict, since revocation and log consistency over time are not checked. Each vector of tests/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.id as closed, and docs/NON_CLAIMS.md names the outcome checks proofbundle does not perform. The trust-pack check binds executor.keyId to the signing key and to the outcomeExecutors role; the pack names no identifier per key, so an anchored executor key can write any executor.id and the outcome verifies. NON_CLAIMS.md now says that executor.id is 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 expected executor.id and 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 passed is optional in eval-result v0.1. SPEC.md section 7b said the test-result payload is 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 without passed verifies 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.json carries the run cbor2-6.1.5 of 2026-10-29, made with reader_crosscheck.py in 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.py now holds each of the five files it reads against the size and sha256 pin of fetch_external.py before it measures, stops on a difference without writing, and records the five under inputs; earlier runs carry no inputs. tests/test_scitt_pin_has_a_measured_run.py holds 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 in pyproject.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.json lists relation_signer, require_relation_target and reject_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 under conformance/provenance/ are new, and conformance/manifest.json lists them. The self-receipt under docs/readiness_pack/ was regenerated with a new key, which readiness_pack.pub.b64 holds. 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.json declares sd_jwt.issuer_key_pin and 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_oids and bitcoin_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 rfc3339z time field must name a real instant, not only have its form. decision verify --strict exited 0 on a decision receipt whose decidedAt was 2026-13-15T09:05:00Z or 2026-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 in validity.expiresAt already failed closed. _schema_shapes.RFC3339_Z now 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 rule intoto._is_rfc3339 already 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 the declaredAt of 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 freigaben and the record freigaben-erste.json in 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, as plugins/proofbundle/DECISIONS.md D119 names (tests/test_plugin_release_window_first_answer_d119.py). This entry concerns the plugin under plugins/, which the package does not carry; it ships with plugin 0.4.0.
  • proofbundle intoto --verify and proofbundle svr --verify name 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 empty signatures, a payload in base64 without its padding, a signature under another key, another payloadType, a payload that is not canonical for its declared contentRootAlg and a claim field the claim rule refuses printed [FAIL] and => FAILED and nothing between, and svr --verify did so for another predicateType as 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 --verify also names a predicateType that 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.id as TypeURI and timeCreated as Timestamp, docs/IN_TOTO_PROFILE.md types the verifier.id of eval-result v0.1 as TypeURI, and the test-result predicate types url as ResourceURI. For the evaluatedAt of 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, and proofbundle svr --verify and proofbundle intoto --verify answered 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; url is 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, with predicate_claim_ok False and a reason naming the member. export_svr_dsse refuses a time_created, to_test_result_statement a url and to_eval_result_predicate a 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-supplied time_created, claim timestamps and test-result url values without validating these forms; a statement emitted with nonconforming values no longer verifies. Two cases of tests/test_cli_text_surface_rt06.py used "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 claims carries at least one claim. docs/IN_TOTO_PROFILE.md listed claims[] without a minimum, and the v0.2 draft requires one or more. A validly signed v0.1 statement whose claims was an empty array verified, and proofbundle intoto --verify printed 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, with predicate_claim_ok False and a reason naming claims, and the command answers FAIL with exit 1. A claims that 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, and to_eval_result_predicate always writes one; a predicate a caller built is not covered.
  • An eval-result v0.1 statement verifies only when verifier, suite and subjectProfile have the form v0.1 gives them. docs/IN_TOTO_PROFILE.md lists verifier.id, suite as {name, version} and subjectProfile as 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 reads verifier or subjectProfile and judges suite only through the parts it carries, so a validly signed v0.1 statement verified, and proofbundle intoto --verify printed PASS with exit 0, with verifier {} or a string, with verifier.id 5, with suite lacking name or version, and with subjectProfile 7 or "bogus_profile", as measured on main b55d736. Such a statement now does not verify, with predicate_claim_ok False 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, timeCreated and properties. The in-toto SVR predicate in its v0.1 version marks all four required, but classify_svr_predicate_shape judged verifier and timeCreated only when present and never looked at verifier.id, so a validly signed statement of that type without verifier, without verifier.id or without timeCreated, or with a number for verifier.id, verified with verify_svr_dsse and proofbundle svr --verify printed PASS with exit 0, as measured on main b55d736. Such a statement now does not verify, with predicate_shape_ok False and a reason naming the member, and the command answers it as malformed with exit 2. The extension field verifier.policy stays optional, and every statement export_svr_dsse writes 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 result one of PASSED, WARNED and FAILED, url a string, the case lists arrays of strings and configuration a list of ResourceDescriptors, each of which must specify one of uri, digest or content, and SPEC.md section 7b calls a bare name-only descriptor invalid. verify_intoto_dsse judged these types only beside a descriptor of ours that annotates a verdict, so a validly signed generic statement verified with result 1, "BOGUS" or "passed", with a configuration entry {} or {"name": "only-a-name"}, with url 5 or with a case list that was no array of strings, as measured on main b55d736. Such a statement now does not verify, with predicate_claim_ok False and a reason naming the member. A configuration entry without digest still makes no claim when it names its resource by uri or content, and every statement to_test_result_statement writes has these types.
  • A test-result v0.1 statement verifies only when its predicate carries result and configuration. The in-toto test-result predicate marks both required, and SPEC.md section 7b says the same of configuration, but verify_intoto_dsse judged 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, with predicate_claim_ok False and a reason naming the first missing member. url and the case lists stay optional, a generic statement that carries both members verifies as before, and every statement to_test_result_statement writes carries both.
  • An eval-result v0.1 statement verifies only when its predicate carries the members v0.1 requires. verify_eval_result_dsse and proofbundle intoto --verify checked 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, commitments with model and dataset, assuranceLevel and subjectProfile are now required; a missing one is a reason with predicate_claim_ok False, and the command prints FAIL and exits 1, as for a v0.2 statement of the wrong shape. preRegistration, receipt and harness stay optional, an unknown member stays unread, and no v0.2 shape applies to v0.1. 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 all of them and verify as before; a predicate a caller built is not covered. tests/test_a_v01_statement_carries_its_required_members.py holds the eight members, the two parts of commitments, the optional members and the corpus; the C2 case of tests/test_intoto_content_root_migration.py, the dispatch case of tests/test_intoto_eval_result_v02.py and the eval-result predicate of tests/test_the_intoto_verifiers_read_the_statement_layer.py signed 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._canonicalizable and trace_record._canonicalizable_value, _plain_value.plain_json, subject_binding.nested_closure_violations and subject_binding.nested_type_violations gave 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 limit verify_trace_record raised a raw MemoryError before 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, through budget._chain_text, at most 256 characters with a cut marked …. The closure and the type walk of subject_binding also 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 through budget._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 own get is 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.py holds 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 of plain_json against the path where the generator planted the refused value, and tests/test_a_declared_path_costs_no_text_per_prefix.py holds 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 takes anchors.bitcoin_block_headers only with heights of ASCII digits and roots of exactly 64 hexadecimal digits; it asked str.isdigit(), which also holds for digits of other scripts, and measured only the length of a root. schemas/trust_policy_v0_1.schema.json states both patterns. --bitcoin-header takes 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_policy reads 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 as AgentReviewError, as a duplicate key was before. sdjwt_issue.check_binds_bundle binds each of passed, threshold, comparator, suite and issuer only when both sides carry it and both values write the same RFC 8785 bytes: 1 no longer stands for true nor 0 for false, a member absent from either side no longer matches a null, and 1 still equals 1.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-header and --bundled-header read a header as the trust-policy loader does. Both took a merkle root through bytes.fromhex, which accepts ASCII white space between the byte pairs, and --bitcoin-header put such a root, white space and all, into the relying party's header map, which a policy file refuses. --bundled-header of anchor upgrade also asked str.isdigit() of a height, which holds for digits of other scripts. Both now read HEIGHT:MERKLEROOT_HEX through 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.py compares 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.