Repository navigation
v6.2.0
Stricter input checks at verification boundaries, hardened Ed25519 trust-anchor handling, and offline AGT receipt verification. Beta. Locations of the closing records are listed under Audit status.
Changelog · Known limitations · Release scope
Highlights
- Agent Governance Toolkit receipts verify offline, without AGT installed and without network access.
- Low-order and non-canonical Ed25519 keys are refused wherever the verifier relies on a key.
- The Rust and Python verifiers refuse the same inputs in more places than in 6.1.0.
If you use 6.0.0 or 6.1.0, please upgrade. 6.2.0 also contains security fixes for those releases.
pip install proofbundle==6.2.0
Findings addressed in 6.2.0
6.2.0 fixes the issues described below, including findings in the released 6.0.0 and 6.1.0 and a pre-release regression in pull request 313. The rows summarize affected surfaces, effects, fixes and measurement baselines. Fixes are linked to their public pull requests, including 311, 312 and 313. The last five rows describe additional classes addressed in pull request 311 and state where each was measured.
| Finding | Affected | Effect | Fixed by |
|---|---|---|---|
| Truthy non-boolean callback results promoted verification evidence | 6.0.0, 6.1.0; Python API resolver and registered-anchor-verifier paths | For example, an evidence resolver returning "false" lifted a digest from REFERENCE_WELL_FORMED to CONTENT_RESOLVED |
Boolean success decisions now require exact True; documented receiver-key results remain supported (#293, which carries #291) |
| A low-order Ed25519 key as a trusted key | 6.0.0, 6.1.0; verify_ed25519, dsse.verify_envelope |
With the identity point supplied as the key, R = identity, S = 0 verifies without a private key |
Trust-anchor paths now reject weak keys, including dsse.verify_envelope; bare verify_ed25519 and the bundle's in-band key retain the §4a profile (#280, #293) |
| A low-order Ed25519 key as the holder key of an SD-JWT key binding | 6.0.0, 6.1.0; CLI included | an SD-JWT bound to the identity point and a Key Binding JWT signed by nobody (R = identity, S = 0) gave "key binding valid" | 6.2.0: issue_sd_jwt raises ValueError, its documented refusal for a bad holder key, and the verifier refuses the presentation (#280, #293) |
| A related map that says it is empty hid an attached retraction | 6.0.0, 6.1.0; Python API | With reject_superseded enabled, a dict subclass reporting length 0 hid a verified attached retraction. verify_decision_receipt and verify_outcome_receipt returned ok=True; the same entries in a plain dict returned ok=False |
6.2.0 reads the map by what it stores (#300) |
An edge's declaredAt accepted non-ASCII decimal digits |
6.0.0, 6.1.0 in validate_relationships; signed CLI comparison measured on main 86671552 |
On that main head, the package emitted such a signed edge; decision verify returned exit 0 with ok=True, while pb_verify_rs verify-relation refused the same bytes with exit 2 |
The edge's declaredAt now accepts ASCII digits only; both verifiers refuse the tested non-ASCII cases (#300) |
| Eight findings involved caller-controlled inputs | 6.0.0, 6.1.0; Python API | For example, verify_decision_receipt reported signer_trusted, ok and safeForAutomation True for a receipt signed by an untrusted key |
6.2.0 uses a fixed reading of the affected inputs, or refuses their types (#312) |
| Wrong-typed policy fields, callback mutation and malformed containers affected verification | 6.0.0, 6.1.0 for these cases; Python API, plus malformed falsy --anchors input on the CLI |
Python API example: a wrong-typed trusted_decision_makers let an untrusted signer pass. On the CLI, malformed falsy anchors were read as absent |
6.2.0 shares policy-field validation, rejects the affected malformed containers, and snapshots affected inputs before callbacks (#313) |
| An attached target's subject state outside the four words of its resolver was read as present | 6.0.0, 6.1.0; Python API | A target labelled AMBIGUOUS or multiple bound a declared targetSubjectDigest to its first subject, and verify_decision_receipt, verify_outcome_receipt and verify_relation_statement returned lineage VERIFIED and ok=True |
When binding a declared targetSubjectDigest, states other than None must be present, absent, ambiguous or malformed; other values fail on direct edges and attached hops. Missing or None states remain inferred from the digest (#311) |
| A restricting CLI option given an empty value was read as absent | 6.0.0, 6.1.0; CLI | Nine command/option combinations, including verify --policy '', verify --anchor-type '' and decision verify --anchors '', exited 0 when their empty values were ignored |
These options are read with is not None; empty paths and an empty nonce are refused with exit 2, while verify --anchor-type '' exits 3 (#311) |
| A verifier read a caller's related map twice, and an anchors file holding null read as no option | 6.0.0, 6.1.0; Python API for the related map, CLI for --anchors |
Under a policy that refuses both the full and the empty map, a gc callback of the caller that emptied its map between the two readings let verify_decision_receipt and verify_outcome_receipt return ok=True. decision verify --anchors FILE with the content null or [] exited 0 like no --anchors |
The verifiers read the map once, and every public function now reads all of its arguments in one reading at its call; such an anchors file is refused with exit 2, and so is a policy file with nothing in it the command evaluates (#311) |
| A policy rule the command does not apply was dropped in silence | 6.0.0, 6.1.0; CLI and Python API, outcome verify and decision verify |
Under a policy whose only rule was reject_retracted, which only relation-statement verify applies, outcome verify printed POLICY: OK over an attached, verified retraction of its receipt, and decision verify ended like a verify without the policy; the same held beside a rule the command applies |
Every rule a policy sets is applied by the command it is given to, or the policy is refused with exit 2 (policy_ok False in the library); validity, purpose and the raw-template flag apply at every receipt command (#311) |
evaluate_policy did not apply a policy's sd_jwt.expected_aud |
6.0.0, 6.1.0; Python API evaluate_policy (the CLI binds the audience itself) |
A caller who verified with verify_bundle(bundle) and handed the policy to evaluate_policy got policy_ok True for a KB-JWT bound to another audience than the policy's expected_aud |
evaluate_policy applies sd_jwt.expected_aud from a verified key binding, and a test checks that every rule a check path lists as applied turns its verdict (#311) |
sd_jwt.expected_vct, the key binding rules and verify trusted values under an issuer key the policy never pinned |
6.0.0, 6.1.0; CLI verify and verify --policy, Python API verify_bundle and evaluate_policy |
The issuer key of an SD-JWT comes from sd_jwt_vc.issuer_public_key_b64, outside the bundle's signed payload, and the rules asked only whether the issuer signature or the key binding verified under that key |
expected_vct, expected_aud, require_nonce and require_key_binding_when_cnf_present pass only when the verifying key matches the new, algorithm-bound sd_jwt.issuer_key_pin or the SD-JWT is bound to the signed eval claim, and fail otherwise; verify_bundle and verify report a Key Binding JWT positive only under the same anchor (check sd-jwt-issuer-trust) (#311) |
anchor verify-pack did not bind the timestamp to a target |
6.0.0, 6.1.0; CLI | The command checked the proof against the pack's own canonicalRoot and read no target, so a valid proof of another root could confirm |
verify-pack requires exactly one of --target-file or --expected-root and refuses a pack whose root differs before the proof is read, exit 1; without a target it exits 2 (#311) |
| A trust pack conferred role trust without an anchor of the relying party | 6.0.0, 6.1.0; Python API verify_outcome_receipt |
A v2 trust-pack predicate passed as trust_pack without any anchor of the relying party made executor_role_trusted True |
Role trust from a trust pack needs a relying-party anchor bound to its content and its full key identity, and the receiver role is never positive from a resolver answer alone (#311) |
| A judge adopted a verification result for other data than that result verified | Python API evaluate_policy, evaluate_decision_policy, evaluate_relations_policy, svr_properties; not measured against 6.0.0 and 6.1.0 |
A result built by hand, or verified for another bundle, statement or claim, was judged as if it had verified the data passed beside it | A result carries an origin over what its verifier verified, and each judge adopts only a result of this process for exactly its input (#311) |
| Expired material produced a positive verdict | 6.0.0, 6.1.0; CLI decision verify, Python API verify_status_snapshot, verify_enclave_attestation |
decision verify exited 0 for a receipt whose validity.expiresAt had passed, and a status-list snapshot or an enclave attestation past its expiry at the given now was ok |
An expired or unreadable receipt expiry fails closed, and decision verify exits 2 (#311) |
| A JWS whose protected header names a critical extension verified | 6.0.0, 6.1.0; Python API verify_key_binding, verify_sd_jwt, verify_status_snapshot, verify_enclave_attestation, and the sd-jwt-key-binding check of verify_bundle |
A token signed with crit: ["future"] in its protected header, an extension proofbundle does not understand, reached ok=True; RFC 7515 §4.1.11 makes such a JWS invalid |
Any crit in a protected header fails closed, in the Python verifiers and in the Rust verify-bundle (#311) |
| The eval adapters read the last of two duplicate JSON keys | 6.1.0 (6.0.0 not measured); Python API from_eee_dataset, from_promptfoo_results, from_lm_eval_results, the two sample readers, measure_vector_set |
A results file with a duplicated score, successes, metric value or cases list was read with its last value, where another JSON reader reads the first or refuses |
These readers refuse a duplicate key at any depth (#311) |
The measurements behind the rows before the last five are in RESTRISIKO_620.md; the last five are measured by the tests their CHANGELOG entries name. A security advisory is a separate outward act.
What changed
| Area | Change | Evidence |
|---|---|---|
| Verify boundary | The affected verify paths reject malformed inputs or read fixed copies of their stored values. | #300 · #312 · #313 · Detail |
| Keys | Ed25519 trust-anchor paths reject low-order and non-canonical keys; the core §4a verification profile remains unchanged. | #280 · #293 · Detail |
| Callbacks | Truthy non-boolean callback results no longer grant success; documented key-returning resolver results remain supported. | #293 · Detail |
| Relation timestamps | An edge's declaredAt takes ASCII digits only, as the Rust verifier does. |
#300 · Detail |
| AGT receipts | Offline verification of Agent Governance Toolkit (AGT) governance receipts. | #255 · Detail |
| Build timestamp | The release build's default timestamp is the commit time of the first commit git log finds from HEAD back that changes a path outside audit_artifacts/ and release_notes/. It skips commits confined to those two paths and a merge whose tree outside them equals one of its parents. 6.1.0 used HEAD's commit time. |
#311 · Detail |
| Verification boundaries | A trust pack confers role trust only under a relying-party anchor, a judge refuses a result built by hand or verified for another signer, payload, SD-JWT presentation or Merkle root, expired receipt validity is rejected, the four JWS verifiers refuse a crit header, and the eval adapters refuse duplicate JSON keys. |
#311 · Detail |
Before upgrading
- Stricter verify boundary. Some inputs accepted by 6.1.0 are now refused. Review the affected API and input rules before upgrading. Details.
- Security-driven breaking changes. A trust policy that sets
sd_jwt.expected_vct,expected_aud,require_nonceorrequire_key_binding_when_cnf_presentfor a stand-alone SD-JWT needssd_jwt.issuer_key_pin,ed25519:ores256:followed by the standard base64 of the issuer's raw public key; without itverify --policyexits 3.verifyof a bundle whose stand-alone SD-JWT carries a Key Binding JWT exits 1 unless a policy pins the issuer key or the SD-JWT is bound to the signed eval claim, with or without--aud/--nonce;verify_bundletakes the pin assd_jwt_issuer_key_pin.anchor verify-packneeds--target-fileor--expected-root; without one it exits 2, and with a root that differs from the pack's it exits 1. The library functionverify_evidence_packtakes no target, so bind the root yourself before trusting its result. Details. - Python API and CLI. The CLI passes no callbacks and builds plain values; of the findings of pull request 313 it reaches only a falsy
anchorsfrom--anchors <file>. A restricting option given an empty value, such as--policy '', is refused or applied where 6.1.0 exited 0. A file given todecision verify --anchorsthat holdsnullor an empty list is refused with exit 2, a policy given todecision verify,outcome verifyorrelation-statement verifythat holds nothing the command evaluates is refused with exit 2, and so is an empty--keyor--new-keybeside the other. A policy that sets a rule the command does not apply is refused with exit 2 at every verify command, a decision policy instantiated from the 6.1.0 template among them: it carriesallowed_schema_versionsandsignature, two rules of the eval bundle path thatdecision verifynever applied, and the 6.2.0 template no longer carries them. A permission or anchor trust given without the requirement it serves (allow_pending, the anchor trust material,--trusted-tsa-rootor--bitcoin-headerwithout an anchor requirement or without--anchors) is refused with exit 2, andevaluate_policywithsd_jwt.expected_audfails a bundle whose key binding did not verify. The AGT verifiers refuse an iterator or a generator astrusted_authorizer_keys(exit 2); a list, a tuple or a numpy or ctypes array of keys is read as before. From Python, an argument reaches a public function as a private copy of one reading: a subclass of a built-in container is read as what it stores, a deque, an array or a view of a dict as a copy of the same type, an object of a dataclass of the package as a new object of that class, and a key of astrorbytessubclass as what it stores; a value of the caller's own class reaches it as a stand-in that holds nothing of the caller, and an iterator or a generator, a memoryview no copy can take and a container the copy cannot hold are refused with aProofBundleError, except where an argument's contract takes the caller's object (a callback, a signer, a path, a clock, the class of a classmethod, a path or a loaded log); the reading does not yet prove a joint state of mutable inputs, since a change made and undone between its two reads is not seen (named inRESTRISIKO_620.md); an argument that changes during each of three readings raises aProofBundleError, and so does a Mapping read through its own methods whose two answers are no one value, such as a value of a type the comparison does not read built anew on each read (named inRESTRISIKO_620.md); and apart fromverifier_block.attach, which fills the caller's predicate by contract, no function changes the caller's own mutable object or returns it. The package's own classes and functions are not part of that reading, and code in the same process that rebinds one of them, such as a property ofVerificationResult, is outside the threat model of this release. Details. - Keys a producer writes. Ed25519 holder-key, log-vkey and witness-vkey producers reject the weak encodings their verifiers reject. Details.
- Release-candidate fixes.
decision verifyexits 2 for a receipt whosevalidity.expiresAthas passed, where 6.1.0 exited 0, and for one whosevalidity.expiresAtcannot be read;--verification-timepins the instant. A trust pack conferssafeForAutomationand role trust only under an anchor of the relying party, and a verification result built by hand or verified for other data fails the policy. A token whose protected JWS header carriescritno longer verifies, and the eval adapters refuse a results file with a duplicate key. Details.
Audit status and known limitations
These notes describe the package-source commit linked above; the closing record identifies the head actually checked. In the v6.2.0 tagged tree, consult gate_zeile.head and gate_zeile.verdict in audit_artifacts/360/fuzz_soak_latest.json and audit_artifacts/360/rust_differential_matrix.json; the separate audit_artifacts/620/pre_tag_receipt_v6.2.0.json records its own audit command and result. A short soak does not satisfy C6.3's full 24-hour requirement; a full run after the tag is planned.
Residual risks · README status
The published package and a passed closing audit are separate facts.
All changes
46 pull requests, grouped by area. Shortened descriptions link to the original discussions.
Verifier and receipt formats · 16 pull requests
- Apply the loader's field rules, reject malformed containers, and protect affected inputs from callback mutation. #313.
- Use one stored reading for the affected verify inputs. #312.
- Parse the bytes a signature covers, and hold commitment patterns at the verify boundary. #300.
- Extend small-order key refusal to the affected carrier and producer paths. #293.
- Refuse low-order and non-canonical Ed25519 keys that a verifier relies on. #280.
- Give an ES256 signature one identity and never rewrite a foreign signer's bytes. #288.
- Make the Rust policy reader judge the relations section like load_policy. #284.
- Treat an empty container as malformed in both verifiers, with a named reason. #272.
- Require each zlib field to contain one complete stream with no trailing bytes. #283.
- Require the in-toto Statement type, fail on missing promised sdist paths, and cap OTS proofs. #286.
- Refuse a bundle that names another format, instead of judging it invalid. #268.
- Require a verdict in the verdict field at five public exporters. #257.
- Split the SD-JWT refusals into three disjoint forms. #260.
- Tell an absent contentRootAlg from a present but unusable one. #254.
- Count distinct CAP-1 units, validate each element and run the alias check on both branches. #252.
- Verify AGT governance receipts offline. #255.
Build, CI and test infrastructure · 14 pull requests
- Prepare 6.2.0: version, release block, notes renderer and register producer. #311.
- Render the release body from a versioned source. #256.
- Run each mutant against the test files that reach it, and count only confirmed kills. #285.
- Cap the mutation gate's test child at 6 GiB of address space. #289.
- Degrade a bare install to clean skips, and run the gate that claims it. #253.
- Choose a test file list by its project, not by its sort order. #264.
- Treat a fallen high-water mark in the tests as a refuted premise. #259.
- Test that a fail-closed verdict is False, not merely not True. #273.
- Add three negative cases on a real receipt for a Codex answer. #276.
- Add an advisory check that measures the Codex review loop. #275.
- Read each file once per run in the English gate. #281.
- Cover every curve and the signing call in the ECDSA inventory test. #295.
- Generate the house form for pull requests and issues from data. #261.
- Judge the shape of a cost by counted work, not by a ratio of run times. #320.
Audit and evidence · 6 pull requests
- Guard every caller of the pre-tag cleanliness gate, with the Rust dependency audit. #249.
- Let a pre-tag verifier judge a tree without installing it. #274.
- Pin only the current release in the README, and run the gate on prose. #266.
- Bind the PR 259 receipt to the comment as GitHub stores it. #265.
- Check a declared error marker against both implementations. #270.
- State in the parity registry what the verifier does when no policy is named. #271.
Documentation and interoperability · 8 pull requests
- Cut 6.2.0 to main and its frozen fixes, and move the rest to 6.3.0. #294.
- Recast the 6.2.0 scope against its four outcomes and open 6.3.0. #251.
- Rewrite the README from the current-release block on, for 6.1.0. #245.
- Add two readings of the SCITT CCF receipt draft and check the checker with planted defects. #258.
- Compare SCITT vector bytes rather than lengths, and add a digest-checked fallback for the unavailable pinned source. #263.
- Record the ToBeSigned inputs per SCITT vector case. #267.
- Correct the SCITT vectors' attribution to Nicholas Templeman. #269.
- Give every site value its source and every gap its reason. #262.
Dependencies · 2 pull requests
Contributors
Thanks to @b7n0de and @dependabot.