proofbundle 3.0.1__tar.gz → 3.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. {proofbundle-3.0.1/src/proofbundle.egg-info → proofbundle-3.1.1}/PKG-INFO +33 -3
  2. {proofbundle-3.0.1 → proofbundle-3.1.1}/README.md +32 -2
  3. {proofbundle-3.0.1 → proofbundle-3.1.1}/pyproject.toml +2 -2
  4. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/__init__.py +1 -1
  5. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/bundle.py +198 -4
  6. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/cli.py +225 -12
  7. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/evalclaim.py +64 -0
  8. proofbundle-3.1.1/src/proofbundle/policies/decision-receipt-template-v1.json +24 -0
  9. proofbundle-3.1.1/src/proofbundle/policies/research-preview-v1.json +7 -0
  10. proofbundle-3.1.1/src/proofbundle/policies/strict-eval-authenticated-root-template-v1.json +25 -0
  11. proofbundle-3.1.1/src/proofbundle/policies/strict-eval-template-v1.json +24 -0
  12. proofbundle-3.1.1/src/proofbundle/policies/strict-prereg-template-v1.json +25 -0
  13. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/policy.py +150 -7
  14. proofbundle-3.1.1/src/proofbundle/policy_profiles.py +236 -0
  15. {proofbundle-3.0.1 → proofbundle-3.1.1/src/proofbundle.egg-info}/PKG-INFO +33 -3
  16. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle.egg-info/SOURCES.txt +10 -0
  17. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_claims_hygiene.py +86 -0
  18. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_policy.py +73 -0
  19. proofbundle-3.1.1/tests/test_eval_evidence_class.py +113 -0
  20. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_kbjwt.py +29 -8
  21. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_ok_semantics.py +13 -3
  22. proofbundle-3.1.1/tests/test_policy_profiles.py +211 -0
  23. proofbundle-3.1.1/tests/test_policy_templates.py +185 -0
  24. proofbundle-3.1.1/tests/test_root_authenticity.py +679 -0
  25. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_sdjwt_verify_binding.py +94 -1
  26. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_trust_policy.py +30 -4
  27. {proofbundle-3.0.1 → proofbundle-3.1.1}/LICENSE +0 -0
  28. {proofbundle-3.0.1 → proofbundle-3.1.1}/setup.cfg +0 -0
  29. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/_inspect_registry.py +0 -0
  30. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/_integration.py +0 -0
  31. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/_strict_json.py +0 -0
  32. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/__init__.py +0 -0
  33. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/_provenance.py +0 -0
  34. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/eee.py +0 -0
  35. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/inspect_ai.py +0 -0
  36. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/lm_eval.py +0 -0
  37. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/promptfoo.py +0 -0
  38. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/samples.py +0 -0
  39. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/anchors.py +0 -0
  40. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/anchors_chia.py +0 -0
  41. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/anchors_chia_add.py +0 -0
  42. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/anchors_markovian.py +0 -0
  43. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/anchors_ots.py +0 -0
  44. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/anchors_rfc3161.py +0 -0
  45. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/beacon.py +0 -0
  46. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/canonical.py +0 -0
  47. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/checkpoint.py +0 -0
  48. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/decision.py +0 -0
  49. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/demo.py +0 -0
  50. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/dsse.py +0 -0
  51. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/eee_eval_schema.json +0 -0
  52. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/emit.py +0 -0
  53. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/errors.py +0 -0
  54. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/experimental/__init__.py +0 -0
  55. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/experimental/enclave.py +0 -0
  56. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/hf_evals.py +0 -0
  57. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/inspect_hook.py +0 -0
  58. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/intoto.py +0 -0
  59. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/kbjwt.py +0 -0
  60. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/merkle.py +0 -0
  61. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/persample.py +0 -0
  62. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/prereg.py +0 -0
  63. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/py.typed +0 -0
  64. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/pytest_plugin.py +0 -0
  65. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/sdjwt.py +0 -0
  66. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/sdjwt_issue.py +0 -0
  67. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/signature.py +0 -0
  68. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/statuslist.py +0 -0
  69. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/tlogproof.py +0 -0
  70. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle.egg-info/dependency_links.txt +0 -0
  71. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle.egg-info/entry_points.txt +0 -0
  72. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle.egg-info/requires.txt +0 -0
  73. {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle.egg-info/top_level.txt +0 -0
  74. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_adapters.py +0 -0
  75. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_adversarial.py +0 -0
  76. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchor_target_trustedtime.py +0 -0
  77. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_chia.py +0 -0
  78. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_chia_add.py +0 -0
  79. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_chia_claims.py +0 -0
  80. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_generic.py +0 -0
  81. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_markovian.py +0 -0
  82. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_ots.py +0 -0
  83. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_rfc3161.py +0 -0
  84. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_beacon.py +0 -0
  85. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_branch_base_check.py +0 -0
  86. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_bundle.py +0 -0
  87. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_bundle_robustness.py +0 -0
  88. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_canonical.py +0 -0
  89. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_checkpoint.py +0 -0
  90. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_cli.py +0 -0
  91. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_cli_eval.py +0 -0
  92. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_cli_require_anchor.py +0 -0
  93. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_conformance.py +0 -0
  94. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_cosignature.py +0 -0
  95. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_cosignature_mldsa.py +0 -0
  96. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_anchors.py +0 -0
  97. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_cli.py +0 -0
  98. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_evidence.py +0 -0
  99. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_fuzz.py +0 -0
  100. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_hardening.py +0 -0
  101. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_schema.py +0 -0
  102. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_validator_api.py +0 -0
  103. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_verify.py +0 -0
  104. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_demo.py +0 -0
  105. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_docs_truth.py +0 -0
  106. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_dup_key_reject.py +0 -0
  107. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_ed25519_semantics.py +0 -0
  108. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_eee.py +0 -0
  109. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_emit.py +0 -0
  110. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_eval_claim_schema.py +0 -0
  111. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_evalclaim.py +0 -0
  112. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_examples.py +0 -0
  113. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_experimental_enclave.py +0 -0
  114. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_fork_pr_secret_isolation.py +0 -0
  115. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_fuzz_parsers.py +0 -0
  116. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_hf_entry_verify_eee_digest.py +0 -0
  117. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_hf_evals.py +0 -0
  118. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_inspect_hook.py +0 -0
  119. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto.py +0 -0
  120. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_claims_hygiene.py +0 -0
  121. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_content_root_migration.py +0 -0
  122. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_dsse.py +0 -0
  123. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_eval_result.py +0 -0
  124. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_examples.py +0 -0
  125. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_spec_diff.py +0 -0
  126. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_svr.py +0 -0
  127. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_merkle.py +0 -0
  128. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_merkle_property.py +0 -0
  129. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_oversized_int_guard.py +0 -0
  130. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_persample.py +0 -0
  131. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_pinned_key_validation.py +0 -0
  132. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_policy_explain_lint.py +0 -0
  133. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_predicate_type_enforcement.py +0 -0
  134. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_prereg.py +0 -0
  135. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_promptfoo.py +0 -0
  136. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_provenance.py +0 -0
  137. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_pytest_plugin.py +0 -0
  138. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_rekor_interop.py +0 -0
  139. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_release_integrity_gate.py +0 -0
  140. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_rfc6962_external_vectors.py +0 -0
  141. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_schema.py +0 -0
  142. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_sdjwt_duplicate_cnf.py +0 -0
  143. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_sdjwt_issue.py +0 -0
  144. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_sdjwt_reference.py +0 -0
  145. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_signature.py +0 -0
  146. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_statuslist.py +0 -0
  147. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_tlogproof.py +0 -0
  148. {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_verify_matrix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proofbundle
3
- Version: 3.0.1
3
+ Version: 3.1.1
4
4
  Summary: Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT.
5
5
  Author: Konrad Gruszka
6
6
  License: MIT
@@ -55,8 +55,8 @@ Dynamic: license-file
55
55
  <div align="center">
56
56
 
57
57
  <picture>
58
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-logo-dark.svg">
59
- <img alt="b7n0de, Verified AI Work" src="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-logo.svg" height="60">
58
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-hase-logo-dark.png">
59
+ <img alt="b7n0de, Verified AI Work, pink rabbit mascot over the B7N0DE wordmark" src="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-hase-logo.png" width="200">
60
60
  </picture>
61
61
 
62
62
  <h1>proofbundle</h1>
@@ -89,6 +89,7 @@ Merkle, one file, no server, no network.
89
89
  - [The problem](#the-problem)
90
90
  - [What a receipt proves, and what it doesn't](#what-a-receipt-proves-and-what-it-doesnt)
91
91
  - [How it fits together](#how-it-fits-together)
92
+ - [Cite this work](#cite-this-work)
92
93
  - [Post-quantum posture](#post-quantum-posture-honest-two-layers)
93
94
  - [What's in the box](#whats-in-the-box)
94
95
  - [Docs](#docs)
@@ -122,6 +123,13 @@ proofbundle verify receipt.json # exit 0 = crypto OK, 1 = crypto/verifica
122
123
 
123
124
  # apply YOUR trust decision — verify makes NO trust decision on its own:
124
125
  proofbundle verify receipt.json --policy trust_policy.json # POLICY: OK | FAIL (exit 3) | NOT_EVALUATED
126
+
127
+ # start from a shipped TEMPLATE and pin your own signer — offline, no network:
128
+ proofbundle policy instantiate strict-eval-template-v1 \
129
+ --issuer-key org-eval.pub --policy-id org/strict-eval-v1 --output org.json
130
+ proofbundle verify receipt.json --json --policy org.json --expected-root <b64>
131
+ # → root_authenticity.safeForAutomation: true only when the policy pins a trusted signer AND the
132
+ # root is authenticated (else false, with automationBlockers naming every reason)
125
133
  ```
126
134
 
127
135
  ## Inspect-native? (METR Task Standard, UK-AISI ecosystem)
@@ -183,6 +191,28 @@ flowchart LR
183
191
  style OK fill:#D6248A,stroke:#D6248A,color:#fff
184
192
  ```
185
193
 
194
+ ### Where it sits in the research neighbourhood
195
+
196
+ proofbundle is a **practical, released, offline verifier — complementary to TEE and zero-knowledge
197
+ approaches**, not a replacement for any of them. It is honest about the line each neighbour crosses
198
+ that a receipt does not. Maturity labels are stated so nothing reads as a settled standard when it is
199
+ a preprint.
200
+
201
+ | Neighbour | What it contributes that a receipt does not | Maturity | Where proofbundle draws the line |
202
+ |---|---|---|---|
203
+ | **K-Veritas** ([arXiv 2605.08586](https://arxiv.org/abs/2605.08586)) — nonrepudiable experimental results | the academic case for tamper-evident, execution-bound experiment reports | preprint | proofbundle is a released, offline, eval-shaped receipt for exactly this problem, not the only take on it |
204
+ | **Attestable Audits** ([arXiv 2506.23706](https://arxiv.org/abs/2506.23706)) — TEE-verified safety audits | that the computation actually ran, inside a trusted enclave | preprint (research prototype) | a receipt proves authorship + integrity, **not** that the computation was correct — that needs a TEE or independent reproduction |
205
+ | **BenchJack** ([arXiv 2605.12673](https://arxiv.org/abs/2605.12673)) — auditing agent benchmarks | whether the benchmark itself is gameable (reward-hacking) | preprint | a receipt over a gameable benchmark is honestly still just a receipt; it says nothing about whether the eval was well designed |
206
+ | **Evaluation Cards** ([arXiv 2606.09809](https://arxiv.org/abs/2606.09809)) — reporting / interpretation layer | a structured, human-facing account of what a result means | preprint | a receipt can bind a card's integrity, not its quality |
207
+ | in-toto / Sigstore, SCITT / Rekor v2, OpenSSF Model Signing (stable standards / production) | artifact-provenance, public transparency, model-artifact signing | — | see [INTEROP.md](https://github.com/b7n0de/proofbundle/blob/main/INTEROP.md) for the honest tool-by-tool comparison |
208
+
209
+ ## Cite this work
210
+
211
+ If proofbundle helped your evaluation pipeline, please cite it. Machine-readable metadata is in
212
+ [`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff); the archival record and
213
+ concept DOI are on Zenodo ([10.5281/zenodo.21230467](https://doi.org/10.5281/zenodo.21230467)), and the
214
+ current design write-up is the Technical Note linked from [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
215
+
186
216
  ## Post-quantum posture (honest, two layers)
187
217
 
188
218
  proofbundle is **not** "quantum-proof" or "quantum-safe" as a whole. It combines two cryptographic layers
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-logo-dark.svg">
5
- <img alt="b7n0de, Verified AI Work" src="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-logo.svg" height="60">
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-hase-logo-dark.png">
5
+ <img alt="b7n0de, Verified AI Work, pink rabbit mascot over the B7N0DE wordmark" src="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-hase-logo.png" width="200">
6
6
  </picture>
7
7
 
8
8
  <h1>proofbundle</h1>
@@ -35,6 +35,7 @@ Merkle, one file, no server, no network.
35
35
  - [The problem](#the-problem)
36
36
  - [What a receipt proves, and what it doesn't](#what-a-receipt-proves-and-what-it-doesnt)
37
37
  - [How it fits together](#how-it-fits-together)
38
+ - [Cite this work](#cite-this-work)
38
39
  - [Post-quantum posture](#post-quantum-posture-honest-two-layers)
39
40
  - [What's in the box](#whats-in-the-box)
40
41
  - [Docs](#docs)
@@ -68,6 +69,13 @@ proofbundle verify receipt.json # exit 0 = crypto OK, 1 = crypto/verifica
68
69
 
69
70
  # apply YOUR trust decision — verify makes NO trust decision on its own:
70
71
  proofbundle verify receipt.json --policy trust_policy.json # POLICY: OK | FAIL (exit 3) | NOT_EVALUATED
72
+
73
+ # start from a shipped TEMPLATE and pin your own signer — offline, no network:
74
+ proofbundle policy instantiate strict-eval-template-v1 \
75
+ --issuer-key org-eval.pub --policy-id org/strict-eval-v1 --output org.json
76
+ proofbundle verify receipt.json --json --policy org.json --expected-root <b64>
77
+ # → root_authenticity.safeForAutomation: true only when the policy pins a trusted signer AND the
78
+ # root is authenticated (else false, with automationBlockers naming every reason)
71
79
  ```
72
80
 
73
81
  ## Inspect-native? (METR Task Standard, UK-AISI ecosystem)
@@ -129,6 +137,28 @@ flowchart LR
129
137
  style OK fill:#D6248A,stroke:#D6248A,color:#fff
130
138
  ```
131
139
 
140
+ ### Where it sits in the research neighbourhood
141
+
142
+ proofbundle is a **practical, released, offline verifier — complementary to TEE and zero-knowledge
143
+ approaches**, not a replacement for any of them. It is honest about the line each neighbour crosses
144
+ that a receipt does not. Maturity labels are stated so nothing reads as a settled standard when it is
145
+ a preprint.
146
+
147
+ | Neighbour | What it contributes that a receipt does not | Maturity | Where proofbundle draws the line |
148
+ |---|---|---|---|
149
+ | **K-Veritas** ([arXiv 2605.08586](https://arxiv.org/abs/2605.08586)) — nonrepudiable experimental results | the academic case for tamper-evident, execution-bound experiment reports | preprint | proofbundle is a released, offline, eval-shaped receipt for exactly this problem, not the only take on it |
150
+ | **Attestable Audits** ([arXiv 2506.23706](https://arxiv.org/abs/2506.23706)) — TEE-verified safety audits | that the computation actually ran, inside a trusted enclave | preprint (research prototype) | a receipt proves authorship + integrity, **not** that the computation was correct — that needs a TEE or independent reproduction |
151
+ | **BenchJack** ([arXiv 2605.12673](https://arxiv.org/abs/2605.12673)) — auditing agent benchmarks | whether the benchmark itself is gameable (reward-hacking) | preprint | a receipt over a gameable benchmark is honestly still just a receipt; it says nothing about whether the eval was well designed |
152
+ | **Evaluation Cards** ([arXiv 2606.09809](https://arxiv.org/abs/2606.09809)) — reporting / interpretation layer | a structured, human-facing account of what a result means | preprint | a receipt can bind a card's integrity, not its quality |
153
+ | in-toto / Sigstore, SCITT / Rekor v2, OpenSSF Model Signing (stable standards / production) | artifact-provenance, public transparency, model-artifact signing | — | see [INTEROP.md](https://github.com/b7n0de/proofbundle/blob/main/INTEROP.md) for the honest tool-by-tool comparison |
154
+
155
+ ## Cite this work
156
+
157
+ If proofbundle helped your evaluation pipeline, please cite it. Machine-readable metadata is in
158
+ [`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff); the archival record and
159
+ concept DOI are on Zenodo ([10.5281/zenodo.21230467](https://doi.org/10.5281/zenodo.21230467)), and the
160
+ current design write-up is the Technical Note linked from [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
161
+
132
162
  ## Post-quantum posture (honest, two layers)
133
163
 
134
164
  proofbundle is **not** "quantum-proof" or "quantum-safe" as a whole. It combines two cryptographic layers
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proofbundle"
7
- version = "3.0.1"
7
+ version = "3.1.1"
8
8
  description = "Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -100,7 +100,7 @@ proofbundle = "proofbundle.pytest_plugin"
100
100
  where = ["src"]
101
101
 
102
102
  [tool.setuptools.package-data]
103
- proofbundle = ["py.typed", "eee_eval_schema.json"]
103
+ proofbundle = ["py.typed", "eee_eval_schema.json", "policies/*.json"]
104
104
 
105
105
  [tool.ruff]
106
106
  line-length = 100
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  from typing import TYPE_CHECKING
15
15
 
16
- __version__ = "3.0.1"
16
+ __version__ = "3.1.1"
17
17
 
18
18
  # The `proofbundle/v0.1` normative spec revision this build implements — kept in sync with the
19
19
  # `Revision:` line at the top of SPEC.md by tests/test_docs_truth.py (WP-B1, closes #28). Bump
@@ -22,7 +22,8 @@ malformed exit code, not a crash.
22
22
  from __future__ import annotations
23
23
 
24
24
  import base64
25
- from typing import Union
25
+ import hmac
26
+ from typing import Optional, Union
26
27
 
27
28
  from . import merkle
28
29
  from ._strict_json import loads_strict
@@ -31,7 +32,28 @@ from .kbjwt import holder_key_from_cnf, split_key_binding, verify_key_binding
31
32
  from .signature import verify_ed25519
32
33
  from .sdjwt import verify_sd_jwt
33
34
 
34
- __all__ = ["SCHEMA", "verify_bundle", "load_bundle", "recompute_merkle_root_b64"]
35
+ __all__ = ["SCHEMA", "verify_bundle", "load_bundle", "recompute_merkle_root_b64",
36
+ "root_authenticity_summary", "AUTOMATION_BLOCKER_REASONS"]
37
+
38
+ # AP-1 §5.3: the human-legible reason for each automationBlockers enum value. Kept HERE, next to the
39
+ # blocker logic in root_authenticity_summary, so the human `SAFE_FOR_AUTOMATION` line and the JSON flag
40
+ # can never drift apart (one source of truth — Iteration F). A blocker with no entry falls back to its
41
+ # raw enum string, so a future blocker is never silently unexplained.
42
+ AUTOMATION_BLOCKER_REASONS = {
43
+ "CRYPTO_FAILED": "Cryptographic verification did not pass",
44
+ "ROOT_NOT_AUTHENTICATED": "The Merkle root was not authenticated against a relying-party value "
45
+ "(--expected-root or a policy trusted_roots entry)",
46
+ "POLICY_NOT_EVALUATED": "No trust policy was evaluated (supply --policy to authorise a signer)",
47
+ "POLICY_FAILED": "The supplied trust policy was not satisfied",
48
+ "SIGNER_NOT_PINNED": "The trust policy pins no trusted signer identity (attributes to nobody)",
49
+ "TEMPLATE_NOT_INSTANTIATED": "The trust policy is a raw template (requiresIdentityOverlay:true) — "
50
+ "instantiate it with a signer identity before depending on it for automation",
51
+ "POLICY_EXPIRED": "The trust policy has expired (its valid_until is in the past)",
52
+ "POLICY_WARNINGS_PRESENT": "The trust policy carries a warning that blocks automation",
53
+ "ANCHOR_REQUIRED_FAILED": "A required external time anchor did not verify",
54
+ "PUBLIC_TRANSPARENCY_REQUIRED_FAILED": "A required public-transparency proof did not verify",
55
+ "REPLAY_BINDING_REQUIRED_FAILED": "A required replay/audience binding did not verify",
56
+ }
35
57
 
36
58
 
37
59
  def _issuer_requires_holder_binding(sd_part: str) -> bool:
@@ -68,6 +90,27 @@ _MERKLE_KEYS = {"hash_alg", "leaf_index", "tree_size", "inclusion_proof_b64", "r
68
90
  _SD_KEYS = {"compact", "issuer_public_key_b64"}
69
91
 
70
92
 
93
+ def _sd_jwt_carries_eval_root_commitment(sd_payload) -> bool:
94
+ """N1 (audit 2026-07-13; discriminator hardened after pre-land L1 review): True iff an SD-JWT issuer
95
+ payload carries the eval-binding ROOT COMMITMENT (``receipt.root_b64``, a non-empty base64 string)
96
+ that ``check_binds_bundle`` binds against — i.e. it CLAIMS to be anchored to a proofbundle merkle
97
+ root. That commitment is the exact cross-receipt substitution vector, so it (NOT a heuristic
98
+ word-match on generic keys like ``passed``/``suite``/``threshold``) is what marks an eval SD-JWT
99
+ grafted onto a non-eval payload. `issue_sd_jwt` always writes ``receipt.root_b64`` always-open, so a
100
+ genuine eval SD-JWT is caught even if its ``passed``/``threshold`` facts are moved into selective
101
+ disclosures; a generic SD-JWT-VC (``iss``/``vct``, no receipt commitment) is not an eval graft and
102
+ stays in scope (backward-compatible). Residual (documented, out of scope): a SELF-SIGNED credential
103
+ that hides even ``receipt.root_b64`` in a disclosure asserts no always-open anchoring claim and cannot
104
+ forge a trusted-issuer graft (the genuine emitter never does this)."""
105
+ if not isinstance(sd_payload, dict):
106
+ return False
107
+ receipt = sd_payload.get("receipt")
108
+ # Fire on the PRESENCE of a receipt.root_b64 string, INCLUDING "" (L1 pre-land audit F3): an empty root
109
+ # commits nothing concrete, but "an eval-shaped commitment present yet evading N1" should not exist. A
110
+ # generic SD-JWT-VC has no receipt object at all, so this never false-refuses one.
111
+ return isinstance(receipt, dict) and isinstance(receipt.get("root_b64"), str)
112
+
113
+
71
114
  def _b64d(value: str, field: str) -> bytes:
72
115
  try:
73
116
  return base64.b64decode(value, validate=True)
@@ -134,7 +177,9 @@ def load_bundle(path: str) -> dict:
134
177
  return loads_strict(handle.read())
135
178
 
136
179
 
137
- def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce=None) -> VerificationResult:
180
+ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce=None,
181
+ expected_root_b64: Optional[str] = None,
182
+ expected_tree_size: Optional[int] = None) -> VerificationResult:
138
183
  """Verify an evidence bundle (a dict or a path to a JSON file).
139
184
 
140
185
  ``expected_aud`` / ``expected_nonce`` (v1.3): when the bundle carries a Key Binding JWT, these enforce
@@ -142,6 +187,17 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
142
187
  ``nonce`` MUST match ``expected_nonce``. If omitted, the KB-JWT signature + disclosure binding are still
143
188
  checked, but the relying party has NOT bound the presentation to itself/this transaction — a stale or
144
189
  cross-audience replay would still verify. A relying party doing challenge-response MUST pass both.
190
+
191
+ ``expected_root_b64`` / ``expected_tree_size`` (P0-A, Hardening 3.0.1 §6.2): RELYING-PARTY root
192
+ authentication. The native Merkle root is NOT part of the signature input (SPEC §5), so the SAME
193
+ signed payload verifies under DIFFERENT roots — a *coherent one-leaf rewrap* re-anchors the payload
194
+ at index 0 of a 2-leaf tree with a foreign sibling, and inclusion still holds. Merkle inclusion alone
195
+ therefore proves CONSISTENCY under the stated root, NOT that the root is authentic. When the relying
196
+ party supplies an authenticated root / tree size (out of band: a pinned value, a signed checkpoint,
197
+ the trusted_roots of a policy), these are enforced bit-exactly and a mismatch FAILS (adds the
198
+ ``root-authenticity`` / ``tree-size`` checks). ``expected_root_b64`` is decoded and compared to the
199
+ stated root's BYTES (canonicalization-agnostic). Absent, root authenticity stays NOT_EVALUATED and
200
+ the crypto verdict is unchanged (backward-compatible) — see ``root_authenticity_summary``.
145
201
  """
146
202
  if isinstance(bundle, str):
147
203
  bundle = load_bundle(bundle)
@@ -198,9 +254,28 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
198
254
  result.add(
199
255
  "merkle-inclusion",
200
256
  incl_ok,
201
- f"anchored at index {leaf_index} of {tree_size}" if incl_ok else "inclusion proof failed",
257
+ f"anchored at index {leaf_index} of {tree_size} (Merkle-consistent under the STATED root)"
258
+ if incl_ok else "inclusion proof failed",
202
259
  )
203
260
 
261
+ # 2b. P0-A (§6.2): relying-party root authentication. The stated root is NOT signed, so inclusion
262
+ # alone does not authenticate it; only a bit-exact match against a root/size the relying party
263
+ # obtained out of band does. Adds a check ONLY when the RP supplies an expectation — absent, root
264
+ # authenticity is NOT_EVALUATED and the verdict is unchanged (backward-compatible).
265
+ if expected_root_b64 is not None:
266
+ exp_root = _b64d(expected_root_b64, "expected_root_b64")
267
+ root_ok = hmac.compare_digest(root, exp_root)
268
+ result.add("root-authenticity", root_ok,
269
+ "stated root matches the expected authenticated root" if root_ok
270
+ else "stated root does NOT match the expected root — possible root/rewrap substitution")
271
+ if expected_tree_size is not None:
272
+ # strict: a real int only — reject bool (1==True) and float (1==1.0), matching _require_int.
273
+ size_ok = (isinstance(expected_tree_size, int) and not isinstance(expected_tree_size, bool)
274
+ and tree_size == expected_tree_size)
275
+ result.add("tree-size", size_ok,
276
+ f"tree_size {tree_size} matches the expected size" if size_ok
277
+ else f"tree_size {tree_size} != expected {expected_tree_size} — possible tree-size substitution")
278
+
204
279
  # 3. optional SD-JWT selective disclosure credential
205
280
  sd = bundle.get("sd_jwt_vc")
206
281
  kb_binding_checked = False # F4: did a KB-JWT (the aud/nonce carrier) actually get verified?
@@ -316,6 +391,25 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
316
391
  "sd_jwt_vc disclosures do NOT bind this bundle (reason: unbound/mismatch — cross-receipt "
317
392
  "substitution; the SD-JWT's passed/threshold/comparator/suite/issuer/root differ from the "
318
393
  "signed claim)")
394
+ elif _sd_jwt_carries_eval_root_commitment(_sd_p):
395
+ # N1 (audit 2026-07-13, L1 live PoC; discriminator hardened after pre-land L1 review): an
396
+ # EVAL SD-JWT (issue_sd_jwt writes the always-open root commitment receipt.root_b64) MUST
397
+ # bind to a proofbundle/eval-claim/v0.1 payload — check_binds_bundle above is the only
398
+ # binding for it. Grafted onto a NON-eval payload it has nothing to bind against, so
399
+ # previously the exact same issuer-valid eval SD-JWT verified CRYPTO: OK on ANY
400
+ # opaque-payload bundle (cross-receipt substitution, sd_jwt_ok stayed true, zero operator
401
+ # signal). Fail-closed (Reifegradpolitik §0.6: never a silent PASS for insecure legacy
402
+ # behaviour). Keying on receipt.root_b64 (the real substitution vector) rather than a
403
+ # word-match on generic keys catches a graft even when passed/threshold are moved into
404
+ # disclosures, and never false-refuses a GENERIC SD-JWT-VC (iss/vct, no receipt commitment,
405
+ # e.g. examples/example_bundle.json), which carries no eval anchoring claim and is out of
406
+ # scope (backward-compatible).
407
+ result.add(
408
+ "sd-jwt-bundle-binding", False,
409
+ "sd_jwt_vc carries an eval-claim root commitment (receipt.root_b64) but the signed "
410
+ "payload is not a proofbundle/eval-claim/v0.1 claim, so that anchoring claim cannot be "
411
+ "bound to this bundle (reason: unbindable eval SD-JWT — a valid issuer signature does not "
412
+ "make an unbound eval anchoring claim belong to this receipt; refused fail-closed)")
319
413
 
320
414
  # F4 (v1.9.2, fail-closed): supplying expected_aud/expected_nonce asks for RFC 9901 §7.3
321
415
  # replay/audience binding. A bundle with no verifiable KB-JWT (no sd_jwt_vc at all, or an
@@ -332,6 +426,106 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
332
426
  return result
333
427
 
334
428
 
429
+ def root_authenticity_summary(result: VerificationResult, *,
430
+ policy_authenticated_root: Optional[bool] = None,
431
+ policy_ok: Optional[bool] = None,
432
+ anchor_ok: Optional[bool] = None,
433
+ signer_trusted: Optional[bool] = None,
434
+ policy_warnings: Optional[list] = None,
435
+ policy_expired: Optional[bool] = None,
436
+ requires_identity_overlay: Optional[bool] = None,
437
+ public_transparency_ok: Optional[bool] = None,
438
+ replay_ok: Optional[bool] = None) -> dict:
439
+ """Structured root-authenticity verdicts (P0-A §6.3), derived from a completed VerificationResult.
440
+
441
+ Separates what Merkle inclusion actually proves from what it does NOT, as three-state strings so a
442
+ consumer never mistakes 'not checked' for 'passed':
443
+
444
+ payloadSignature PASS/FAIL — the payload is signed by the stated key
445
+ merkleConsistency PASS/FAIL — the payload is Merkle-consistent under the STATED root
446
+ rootAuthenticity PASS/FAIL/NOT_EVALUATED — was the stated root authenticated against a
447
+ relying-party value (``expected_root``, or a policy's ``trusted_roots``)?
448
+ publicTransparency NOT_EVALUATED — a public-log receipt is the separate §10 profile
449
+ safeForAutomation bool — True ONLY if the whole crypto verdict passed, the root was
450
+ affirmatively authenticated, AND a supplied trust policy
451
+ PASSED with a real signer pin (P0-B, audit 2026-07-13)
452
+ automationBlockers list[str] — every reason safeForAutomation is false, so a consumer
453
+ keying off the flag sees exactly WHY (fail-closed)
454
+
455
+ ``policy_authenticated_root`` folds the policy layer's root verdict in when no explicit
456
+ ``root-authenticity`` check ran (e.g. the root matched a policy ``trusted_roots`` entry).
457
+ ``policy_ok`` / ``anchor_ok`` are the relying-party gate verdicts (True/False/None=not-evaluated).
458
+ P0-B: ``safeForAutomation`` is a GLOBAL trust verdict, so ``policy_ok`` must be True (a policy that
459
+ passed) — ``None`` (no policy evaluated) can never make it true. ``policy_warnings`` (the vacuous
460
+ 'attributes to nobody' lint) forces it false too: a policy that pins no signer authorises no
461
+ identity, so a crypto-valid, root-pinned receipt under it is NOT automation-safe.
462
+ """
463
+ by = {c.name: c.ok for c in result.checks}
464
+
465
+ def _tri(name: str) -> str:
466
+ return "PASS" if by.get(name) else ("FAIL" if name in by else "NOT_EVALUATED")
467
+
468
+ if "root-authenticity" in by:
469
+ root_auth = "PASS" if by["root-authenticity"] else "FAIL"
470
+ elif policy_authenticated_root is True:
471
+ root_auth = "PASS"
472
+ elif policy_authenticated_root is False:
473
+ root_auth = "FAIL"
474
+ else:
475
+ root_auth = "NOT_EVALUATED"
476
+ # P0-B (audit 2026-07-13): the former `policy_ok is not False` let policy_ok=None (no policy
477
+ # evaluated) through → a crypto-valid, root-pinned receipt looked automation-safe though NO trusted
478
+ # signer was ever authorised. safeForAutomation is now a GLOBAL trust verdict: policy_ok must be True
479
+ # AND carry no vacuous 'attributes to nobody' warning. Every failed condition is surfaced in
480
+ # automationBlockers (fail-closed, so the flag is never a silent yes).
481
+ # P0-B / AP-1 §5 (audit 2026-07-13): safeForAutomation is a GLOBAL trust verdict. Variant A (strict):
482
+ # true ONLY when crypto passed, the root was authenticated, a supplied policy PASSED (policy_ok is
483
+ # True — None/no-policy never qualifies), that policy actually PINS a trusted signer (signer_trusted),
484
+ # and no required anchor / public-transparency / replay gate FAILED. automationBlockers enumerates
485
+ # every reason it is false (fail-closed, never a silent yes). NOTE for 3.1.1: public_transparency_ok
486
+ # and replay_ok are relying-party gates that are None (not-requested) on the current core — the §10
487
+ # public-transparency policy section is 3.2.0, and replay (aud/nonce) already fails the crypto verdict
488
+ # (CRYPTO_FAILED) when a required KB-JWT is absent — so these two blockers are defined for forward
489
+ # compatibility and stay dormant unless a future policy layer supplies a False verdict.
490
+ blockers: list[str] = []
491
+ if not bool(result.ok):
492
+ blockers.append("CRYPTO_FAILED")
493
+ if root_auth != "PASS":
494
+ blockers.append("ROOT_NOT_AUTHENTICATED")
495
+ if policy_ok is None:
496
+ blockers.append("POLICY_NOT_EVALUATED")
497
+ elif policy_ok is False:
498
+ blockers.append("POLICY_FAILED")
499
+ elif requires_identity_overlay:
500
+ # AP-2 §6.2 (L2 pre-land audit): a RAW template (requiresIdentityOverlay:true) is never automation-safe
501
+ # — reported as its OWN blocker, not SIGNER_NOT_PINNED, which would be factually wrong when the template
502
+ # actually does match a signer (the real reason is the un-cleared template-lifecycle flag).
503
+ blockers.append("TEMPLATE_NOT_INSTANTIATED")
504
+ elif signer_trusted is not True:
505
+ blockers.append("SIGNER_NOT_PINNED") # policy passed but pins no trusted identity (attributes to nobody)
506
+ elif policy_warnings:
507
+ blockers.append("POLICY_WARNINGS_PRESENT") # signer pinned yet the policy still warns (forward-compat)
508
+ # AP-2 §6.4 lifecycle: an EXPIRED policy is unsafe to automate on even if it otherwise passed (a stale
509
+ # signer pin the relying party has since rotated away from). Independent of the signer/warning chain so
510
+ # it is reported alongside, never in place of, another reason.
511
+ if policy_expired is True:
512
+ blockers.append("POLICY_EXPIRED")
513
+ if anchor_ok is False:
514
+ blockers.append("ANCHOR_REQUIRED_FAILED")
515
+ if public_transparency_ok is False:
516
+ blockers.append("PUBLIC_TRANSPARENCY_REQUIRED_FAILED")
517
+ if replay_ok is False:
518
+ blockers.append("REPLAY_BINDING_REQUIRED_FAILED")
519
+ return {
520
+ "payloadSignature": _tri("ed25519-signature"),
521
+ "merkleConsistency": _tri("merkle-inclusion"),
522
+ "rootAuthenticity": root_auth,
523
+ "publicTransparency": "NOT_EVALUATED",
524
+ "safeForAutomation": not blockers,
525
+ "automationBlockers": blockers,
526
+ }
527
+
528
+
335
529
  def recompute_merkle_root_b64(bundle: Union[dict, str]) -> dict:
336
530
  """Recompute the Merkle root from the bundle's own payload + inclusion proof (v1.2, issue #2).
337
531