proofbundle 3.0.1__tar.gz → 3.1.0__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 (147) hide show
  1. {proofbundle-3.0.1/src/proofbundle.egg-info → proofbundle-3.1.0}/PKG-INFO +26 -3
  2. {proofbundle-3.0.1 → proofbundle-3.1.0}/README.md +25 -2
  3. {proofbundle-3.0.1 → proofbundle-3.1.0}/pyproject.toml +2 -2
  4. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/__init__.py +1 -1
  5. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/bundle.py +86 -4
  6. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/cli.py +68 -9
  7. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/evalclaim.py +64 -0
  8. proofbundle-3.1.0/src/proofbundle/policies/decision-receipt-v1.json +14 -0
  9. proofbundle-3.1.0/src/proofbundle/policies/research-preview-v1.json +7 -0
  10. proofbundle-3.1.0/src/proofbundle/policies/strict-eval-authenticated-root-v1.json +9 -0
  11. proofbundle-3.1.0/src/proofbundle/policies/strict-eval-v1.json +9 -0
  12. proofbundle-3.1.0/src/proofbundle/policies/strict-prereg-v1.json +9 -0
  13. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/policy.py +66 -3
  14. proofbundle-3.1.0/src/proofbundle/policy_profiles.py +86 -0
  15. {proofbundle-3.0.1 → proofbundle-3.1.0/src/proofbundle.egg-info}/PKG-INFO +26 -3
  16. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle.egg-info/SOURCES.txt +9 -0
  17. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_claims_hygiene.py +86 -0
  18. proofbundle-3.1.0/tests/test_eval_evidence_class.py +113 -0
  19. proofbundle-3.1.0/tests/test_policy_profiles.py +199 -0
  20. proofbundle-3.1.0/tests/test_root_authenticity.py +244 -0
  21. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_trust_policy.py +16 -0
  22. {proofbundle-3.0.1 → proofbundle-3.1.0}/LICENSE +0 -0
  23. {proofbundle-3.0.1 → proofbundle-3.1.0}/setup.cfg +0 -0
  24. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/_inspect_registry.py +0 -0
  25. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/_integration.py +0 -0
  26. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/_strict_json.py +0 -0
  27. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/__init__.py +0 -0
  28. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/_provenance.py +0 -0
  29. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/eee.py +0 -0
  30. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
  31. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/lm_eval.py +0 -0
  32. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/promptfoo.py +0 -0
  33. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/samples.py +0 -0
  34. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/anchors.py +0 -0
  35. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/anchors_chia.py +0 -0
  36. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/anchors_chia_add.py +0 -0
  37. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/anchors_markovian.py +0 -0
  38. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/anchors_ots.py +0 -0
  39. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/anchors_rfc3161.py +0 -0
  40. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/beacon.py +0 -0
  41. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/canonical.py +0 -0
  42. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/checkpoint.py +0 -0
  43. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/decision.py +0 -0
  44. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/demo.py +0 -0
  45. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/dsse.py +0 -0
  46. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/eee_eval_schema.json +0 -0
  47. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/emit.py +0 -0
  48. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/errors.py +0 -0
  49. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/experimental/__init__.py +0 -0
  50. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/experimental/enclave.py +0 -0
  51. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/hf_evals.py +0 -0
  52. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/inspect_hook.py +0 -0
  53. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/intoto.py +0 -0
  54. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/kbjwt.py +0 -0
  55. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/merkle.py +0 -0
  56. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/persample.py +0 -0
  57. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/prereg.py +0 -0
  58. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/py.typed +0 -0
  59. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/pytest_plugin.py +0 -0
  60. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/sdjwt.py +0 -0
  61. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/sdjwt_issue.py +0 -0
  62. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/signature.py +0 -0
  63. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/statuslist.py +0 -0
  64. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/tlogproof.py +0 -0
  65. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
  66. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
  67. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle.egg-info/requires.txt +0 -0
  68. {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle.egg-info/top_level.txt +0 -0
  69. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_adapters.py +0 -0
  70. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_adversarial.py +0 -0
  71. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchor_target_trustedtime.py +0 -0
  72. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_chia.py +0 -0
  73. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_chia_add.py +0 -0
  74. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_chia_claims.py +0 -0
  75. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_generic.py +0 -0
  76. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_markovian.py +0 -0
  77. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_ots.py +0 -0
  78. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_rfc3161.py +0 -0
  79. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_beacon.py +0 -0
  80. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_branch_base_check.py +0 -0
  81. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_bundle.py +0 -0
  82. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_bundle_robustness.py +0 -0
  83. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_canonical.py +0 -0
  84. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_checkpoint.py +0 -0
  85. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_cli.py +0 -0
  86. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_cli_eval.py +0 -0
  87. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_cli_require_anchor.py +0 -0
  88. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_conformance.py +0 -0
  89. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_cosignature.py +0 -0
  90. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_cosignature_mldsa.py +0 -0
  91. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_anchors.py +0 -0
  92. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_cli.py +0 -0
  93. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_evidence.py +0 -0
  94. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_fuzz.py +0 -0
  95. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_hardening.py +0 -0
  96. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_policy.py +0 -0
  97. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_schema.py +0 -0
  98. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_validator_api.py +0 -0
  99. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_verify.py +0 -0
  100. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_demo.py +0 -0
  101. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_docs_truth.py +0 -0
  102. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_dup_key_reject.py +0 -0
  103. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_ed25519_semantics.py +0 -0
  104. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_eee.py +0 -0
  105. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_emit.py +0 -0
  106. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_eval_claim_schema.py +0 -0
  107. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_evalclaim.py +0 -0
  108. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_examples.py +0 -0
  109. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_experimental_enclave.py +0 -0
  110. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_fork_pr_secret_isolation.py +0 -0
  111. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_fuzz_parsers.py +0 -0
  112. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_hf_entry_verify_eee_digest.py +0 -0
  113. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_hf_evals.py +0 -0
  114. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_inspect_hook.py +0 -0
  115. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto.py +0 -0
  116. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_claims_hygiene.py +0 -0
  117. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_content_root_migration.py +0 -0
  118. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_dsse.py +0 -0
  119. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_eval_result.py +0 -0
  120. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_examples.py +0 -0
  121. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_spec_diff.py +0 -0
  122. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_svr.py +0 -0
  123. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_kbjwt.py +0 -0
  124. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_merkle.py +0 -0
  125. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_merkle_property.py +0 -0
  126. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_ok_semantics.py +0 -0
  127. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_oversized_int_guard.py +0 -0
  128. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_persample.py +0 -0
  129. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_pinned_key_validation.py +0 -0
  130. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_policy_explain_lint.py +0 -0
  131. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_predicate_type_enforcement.py +0 -0
  132. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_prereg.py +0 -0
  133. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_promptfoo.py +0 -0
  134. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_provenance.py +0 -0
  135. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_pytest_plugin.py +0 -0
  136. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_rekor_interop.py +0 -0
  137. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_release_integrity_gate.py +0 -0
  138. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_rfc6962_external_vectors.py +0 -0
  139. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_schema.py +0 -0
  140. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_sdjwt_duplicate_cnf.py +0 -0
  141. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_sdjwt_issue.py +0 -0
  142. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_sdjwt_reference.py +0 -0
  143. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_sdjwt_verify_binding.py +0 -0
  144. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_signature.py +0 -0
  145. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_statuslist.py +0 -0
  146. {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_tlogproof.py +0 -0
  147. {proofbundle-3.0.1 → proofbundle-3.1.0}/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.0
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)
@@ -183,6 +184,28 @@ flowchart LR
183
184
  style OK fill:#D6248A,stroke:#D6248A,color:#fff
184
185
  ```
185
186
 
187
+ ### Where it sits in the research neighbourhood
188
+
189
+ proofbundle is a **practical, released, offline verifier — complementary to TEE and zero-knowledge
190
+ approaches**, not a replacement for any of them. It is honest about the line each neighbour crosses
191
+ that a receipt does not. Maturity labels are stated so nothing reads as a settled standard when it is
192
+ a preprint.
193
+
194
+ | Neighbour | What it contributes that a receipt does not | Maturity | Where proofbundle draws the line |
195
+ |---|---|---|---|
196
+ | **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 |
197
+ | **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 |
198
+ | **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 |
199
+ | **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 |
200
+ | 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 |
201
+
202
+ ## Cite this work
203
+
204
+ If proofbundle helped your evaluation pipeline, please cite it. Machine-readable metadata is in
205
+ [`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff); the archival record and
206
+ concept DOI are on Zenodo ([10.5281/zenodo.21230467](https://doi.org/10.5281/zenodo.21230467)), and the
207
+ current design write-up is the Technical Note linked from [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
208
+
186
209
  ## Post-quantum posture (honest, two layers)
187
210
 
188
211
  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)
@@ -129,6 +130,28 @@ flowchart LR
129
130
  style OK fill:#D6248A,stroke:#D6248A,color:#fff
130
131
  ```
131
132
 
133
+ ### Where it sits in the research neighbourhood
134
+
135
+ proofbundle is a **practical, released, offline verifier — complementary to TEE and zero-knowledge
136
+ approaches**, not a replacement for any of them. It is honest about the line each neighbour crosses
137
+ that a receipt does not. Maturity labels are stated so nothing reads as a settled standard when it is
138
+ a preprint.
139
+
140
+ | Neighbour | What it contributes that a receipt does not | Maturity | Where proofbundle draws the line |
141
+ |---|---|---|---|
142
+ | **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 |
143
+ | **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 |
144
+ | **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 |
145
+ | **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 |
146
+ | 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 |
147
+
148
+ ## Cite this work
149
+
150
+ If proofbundle helped your evaluation pipeline, please cite it. Machine-readable metadata is in
151
+ [`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff); the archival record and
152
+ concept DOI are on Zenodo ([10.5281/zenodo.21230467](https://doi.org/10.5281/zenodo.21230467)), and the
153
+ current design write-up is the Technical Note linked from [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
154
+
132
155
  ## Post-quantum posture (honest, two layers)
133
156
 
134
157
  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.0"
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.0"
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,8 @@ 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"]
35
37
 
36
38
 
37
39
  def _issuer_requires_holder_binding(sd_part: str) -> bool:
@@ -134,7 +136,9 @@ def load_bundle(path: str) -> dict:
134
136
  return loads_strict(handle.read())
135
137
 
136
138
 
137
- def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce=None) -> VerificationResult:
139
+ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce=None,
140
+ expected_root_b64: Optional[str] = None,
141
+ expected_tree_size: Optional[int] = None) -> VerificationResult:
138
142
  """Verify an evidence bundle (a dict or a path to a JSON file).
139
143
 
140
144
  ``expected_aud`` / ``expected_nonce`` (v1.3): when the bundle carries a Key Binding JWT, these enforce
@@ -142,6 +146,17 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
142
146
  ``nonce`` MUST match ``expected_nonce``. If omitted, the KB-JWT signature + disclosure binding are still
143
147
  checked, but the relying party has NOT bound the presentation to itself/this transaction — a stale or
144
148
  cross-audience replay would still verify. A relying party doing challenge-response MUST pass both.
149
+
150
+ ``expected_root_b64`` / ``expected_tree_size`` (P0-A, Hardening 3.0.1 §6.2): RELYING-PARTY root
151
+ authentication. The native Merkle root is NOT part of the signature input (SPEC §5), so the SAME
152
+ signed payload verifies under DIFFERENT roots — a *coherent one-leaf rewrap* re-anchors the payload
153
+ at index 0 of a 2-leaf tree with a foreign sibling, and inclusion still holds. Merkle inclusion alone
154
+ therefore proves CONSISTENCY under the stated root, NOT that the root is authentic. When the relying
155
+ party supplies an authenticated root / tree size (out of band: a pinned value, a signed checkpoint,
156
+ the trusted_roots of a policy), these are enforced bit-exactly and a mismatch FAILS (adds the
157
+ ``root-authenticity`` / ``tree-size`` checks). ``expected_root_b64`` is decoded and compared to the
158
+ stated root's BYTES (canonicalization-agnostic). Absent, root authenticity stays NOT_EVALUATED and
159
+ the crypto verdict is unchanged (backward-compatible) — see ``root_authenticity_summary``.
145
160
  """
146
161
  if isinstance(bundle, str):
147
162
  bundle = load_bundle(bundle)
@@ -198,9 +213,28 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
198
213
  result.add(
199
214
  "merkle-inclusion",
200
215
  incl_ok,
201
- f"anchored at index {leaf_index} of {tree_size}" if incl_ok else "inclusion proof failed",
216
+ f"anchored at index {leaf_index} of {tree_size} (Merkle-consistent under the STATED root)"
217
+ if incl_ok else "inclusion proof failed",
202
218
  )
203
219
 
220
+ # 2b. P0-A (§6.2): relying-party root authentication. The stated root is NOT signed, so inclusion
221
+ # alone does not authenticate it; only a bit-exact match against a root/size the relying party
222
+ # obtained out of band does. Adds a check ONLY when the RP supplies an expectation — absent, root
223
+ # authenticity is NOT_EVALUATED and the verdict is unchanged (backward-compatible).
224
+ if expected_root_b64 is not None:
225
+ exp_root = _b64d(expected_root_b64, "expected_root_b64")
226
+ root_ok = hmac.compare_digest(root, exp_root)
227
+ result.add("root-authenticity", root_ok,
228
+ "stated root matches the expected authenticated root" if root_ok
229
+ else "stated root does NOT match the expected root — possible root/rewrap substitution")
230
+ if expected_tree_size is not None:
231
+ # strict: a real int only — reject bool (1==True) and float (1==1.0), matching _require_int.
232
+ size_ok = (isinstance(expected_tree_size, int) and not isinstance(expected_tree_size, bool)
233
+ and tree_size == expected_tree_size)
234
+ result.add("tree-size", size_ok,
235
+ f"tree_size {tree_size} matches the expected size" if size_ok
236
+ else f"tree_size {tree_size} != expected {expected_tree_size} — possible tree-size substitution")
237
+
204
238
  # 3. optional SD-JWT selective disclosure credential
205
239
  sd = bundle.get("sd_jwt_vc")
206
240
  kb_binding_checked = False # F4: did a KB-JWT (the aud/nonce carrier) actually get verified?
@@ -332,6 +366,54 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
332
366
  return result
333
367
 
334
368
 
369
+ def root_authenticity_summary(result: VerificationResult, *,
370
+ policy_authenticated_root: Optional[bool] = None,
371
+ policy_ok: Optional[bool] = None,
372
+ anchor_ok: Optional[bool] = None) -> dict:
373
+ """Structured root-authenticity verdicts (P0-A §6.3), derived from a completed VerificationResult.
374
+
375
+ Separates what Merkle inclusion actually proves from what it does NOT, as three-state strings so a
376
+ consumer never mistakes 'not checked' for 'passed':
377
+
378
+ payloadSignature PASS/FAIL — the payload is signed by the stated key
379
+ merkleConsistency PASS/FAIL — the payload is Merkle-consistent under the STATED root
380
+ rootAuthenticity PASS/FAIL/NOT_EVALUATED — was the stated root authenticated against a
381
+ relying-party value (``expected_root``, or a policy's ``trusted_roots``)?
382
+ publicTransparency NOT_EVALUATED — a public-log receipt is the separate §10 profile
383
+ safeForAutomation bool — True ONLY if the whole crypto verdict passed, the root was
384
+ affirmatively authenticated, AND no supplied trust policy /
385
+ anchor requirement FAILED (§6.3: root authenticity AND policy)
386
+
387
+ ``policy_authenticated_root`` folds the policy layer's root verdict in when no explicit
388
+ ``root-authenticity`` check ran (e.g. the root matched a policy ``trusted_roots`` entry).
389
+ ``policy_ok`` / ``anchor_ok`` are the relying-party gate verdicts (True/False/None=not-evaluated); a
390
+ FAILED gate makes ``safeForAutomation`` false even when the root itself authenticated, so a consumer
391
+ keying off this flag can never auto-trust a bundle its own policy rejected.
392
+ """
393
+ by = {c.name: c.ok for c in result.checks}
394
+
395
+ def _tri(name: str) -> str:
396
+ return "PASS" if by.get(name) else ("FAIL" if name in by else "NOT_EVALUATED")
397
+
398
+ if "root-authenticity" in by:
399
+ root_auth = "PASS" if by["root-authenticity"] else "FAIL"
400
+ elif policy_authenticated_root is True:
401
+ root_auth = "PASS"
402
+ elif policy_authenticated_root is False:
403
+ root_auth = "FAIL"
404
+ else:
405
+ root_auth = "NOT_EVALUATED"
406
+ safe = (bool(result.ok) and root_auth == "PASS"
407
+ and policy_ok is not False and anchor_ok is not False)
408
+ return {
409
+ "payloadSignature": _tri("ed25519-signature"),
410
+ "merkleConsistency": _tri("merkle-inclusion"),
411
+ "rootAuthenticity": root_auth,
412
+ "publicTransparency": "NOT_EVALUATED",
413
+ "safeForAutomation": safe,
414
+ }
415
+
416
+
335
417
  def recompute_merkle_root_b64(bundle: Union[dict, str]) -> dict:
336
418
  """Recompute the Merkle root from the bundle's own payload + inclusion proof (v1.2, issue #2).
337
419
 
@@ -69,7 +69,8 @@ def _safe_line(s: str) -> str:
69
69
  _VERIFY_NULLABLE_FIELDS = (
70
70
  "schema_ok", "signature_ok", "merkle_ok", "sd_jwt_ok", "sd_jwt_issuer_verified",
71
71
  "key_binding_ok", "audience_ok", "nonce_ok", "freshness_ok", "anchor_ok", "witness_ok",
72
- "status_ok", "assurance_policy_ok", "policy_ok", "assurance", "assurance_declared_by")
72
+ "status_ok", "assurance_policy_ok", "policy_ok", "assurance", "assurance_declared_by",
73
+ "root_authenticity")
73
74
 
74
75
 
75
76
  def _error_verify_fields(error: str) -> dict:
@@ -300,7 +301,8 @@ def _cmd_emit_eval(args: argparse.Namespace) -> int:
300
301
  def _cmd_show_eval(args: argparse.Namespace) -> int:
301
302
  from .bundle import load_bundle # noqa: PLC0415
302
303
  from .evalclaim import ( # noqa: PLC0415
303
- DEFAULT_ASSURANCE, check_freshness, claim_warnings, decode_eval_claim, sd_jwt_hidden_count,
304
+ DEFAULT_ASSURANCE, check_freshness, claim_warnings, decode_eval_claim, eval_evidence_class,
305
+ sd_jwt_hidden_count,
304
306
  )
305
307
  try:
306
308
  # Resolve the path to a dict ONCE and pass that object to every reader — a second per-function re-read of
@@ -316,6 +318,9 @@ def _cmd_show_eval(args: argparse.Namespace) -> int:
316
318
  print(f"suite {claim['suite']} ({claim['suite_version']})")
317
319
  print(f"metric {claim['metric']} {claim['comparator']} {claim['threshold']}")
318
320
  print(f"passed {claim['passed']} (n={claim['n']})")
321
+ ev = eval_evidence_class(claim)
322
+ print(f"evidence {ev['score_evidence']} ({ev['detail']})")
323
+ print(f"note {ev['methodology']} (the receipt never judges whether the suite is well designed)")
319
324
  print(f"assurance {claim.get('assurance_level', DEFAULT_ASSURANCE)}")
320
325
  print(f"model commit {claim['model_id_commit']}")
321
326
  print(f"dataset commit {claim['dataset_id_commit']}")
@@ -432,6 +437,7 @@ def _cmd_verify(args: argparse.Namespace) -> int:
432
437
  from .policy import ( # noqa: PLC0415
433
438
  evaluate_policy, load_policy, policy_anchor_trust, policy_expected_aud,
434
439
  )
440
+ from .policy_profiles import resolve_policy_source # noqa: PLC0415
435
441
 
436
442
  flag_aud = getattr(args, "aud", None)
437
443
  flag_nonce = getattr(args, "nonce", None)
@@ -460,7 +466,7 @@ def _cmd_verify(args: argparse.Namespace) -> int:
460
466
  # expected_aud are set and DIFFER, that is ambiguous → exit 2 (never a silent override).
461
467
  effective_aud = flag_aud
462
468
  if getattr(args, "policy", None):
463
- policy = load_policy(args.policy)
469
+ policy = load_policy(resolve_policy_source(args.policy))
464
470
  pol_aud = policy_expected_aud(policy)
465
471
  if pol_aud is not None and flag_aud is not None and pol_aud != flag_aud:
466
472
  from .policy import PolicyError # noqa: PLC0415
@@ -495,7 +501,9 @@ def _cmd_verify(args: argparse.Namespace) -> int:
495
501
  merged = dict(pol_trust)
496
502
  merged.update(rp_trust_material or {}) # CLI flags take precedence on the same key
497
503
  rp_trust_material = merged
498
- result = verify_bundle(bundle, expected_aud=effective_aud, expected_nonce=flag_nonce)
504
+ result = verify_bundle(bundle, expected_aud=effective_aud, expected_nonce=flag_nonce,
505
+ expected_root_b64=getattr(args, "expected_root", None),
506
+ expected_tree_size=getattr(args, "expected_tree_size", None))
499
507
  roots = recompute_merkle_root_b64(bundle) if args.verbose else None
500
508
  except (ProofBundleError, OSError, ValueError, RecursionError) as exc: # file/JSON/format/policy errors → clean exit 2, never a raw traceback
501
509
  # RecursionError: deeply-nested JSON overflows json.load's recursion; catch it here too so it
@@ -537,6 +545,14 @@ def _cmd_verify(args: argparse.Namespace) -> int:
537
545
  aud_requested=effective_aud is not None,
538
546
  nonce_requested=flag_nonce is not None,
539
547
  assurance=assurance, policy_ok=policy_ok)
548
+ # P0-A §6.3: structured root-authenticity verdicts, folding the policy layer's trusted_roots verdict
549
+ # in when no --expected-root was given. Always separate, so merkle-inclusion is never read as root
550
+ # authentication (additive JSON field + a human line).
551
+ from .bundle import root_authenticity_summary # noqa: PLC0415
552
+ root_summary = root_authenticity_summary(
553
+ result, policy_authenticated_root=(policy_result or {}).get("root_authenticated"),
554
+ policy_ok=policy_ok, anchor_ok=anchor_required_ok)
555
+ fields["root_authenticity"] = root_summary
540
556
  if policy is not None:
541
557
  fields["policy_id"] = policy.get("policy_id")
542
558
  # WP-TP1: non-fatal honesty warnings (e.g. "attributes to nobody") — exit code unchanged.
@@ -591,6 +607,13 @@ def _cmd_verify(args: argparse.Namespace) -> int:
591
607
  else:
592
608
  assurance_line = "n/a (not an eval receipt)" # a well-verified bundle that is not an eval receipt
593
609
  print(f"CRYPTO: {'OK' if crypto_ok else 'FAILED'}")
610
+ # P0-A §6.3: separate the root-authenticity verdicts — merkle-inclusion proves CONSISTENCY under
611
+ # the STATED root, never that the root is authentic. safe-for-automation is true only when the
612
+ # root was affirmatively authenticated (--expected-root or a policy trusted_roots).
613
+ print(f"ROOT-AUTHENTICITY: {root_summary['rootAuthenticity']} "
614
+ f"(payload-signature {root_summary['payloadSignature']}, "
615
+ f"merkle-consistency {root_summary['merkleConsistency']}, "
616
+ f"safe-for-automation {str(root_summary['safeForAutomation']).lower()})")
594
617
  if policy is not None and not crypto_ok:
595
618
  print("POLICY: NOT_EVALUATED (crypto failed — policy not checked)")
596
619
  else:
@@ -1076,8 +1099,9 @@ def _cmd_decision_init(args: argparse.Namespace) -> int:
1076
1099
 
1077
1100
  def _cmd_policy_explain(args: argparse.Namespace) -> int:
1078
1101
  from .policy import PolicyError, explain_policy, load_policy, policy_warnings # noqa: PLC0415
1102
+ from .policy_profiles import resolve_policy_source # noqa: PLC0415
1079
1103
  try:
1080
- policy = load_policy(args.policy)
1104
+ policy = load_policy(resolve_policy_source(args.policy))
1081
1105
  except PolicyError as exc: # malformed policy → exit 2; in --json emit an error object (six-lens
1082
1106
  if args.json: # review: an empty stdout on the error path breaks a JSON consumer)
1083
1107
  print(json.dumps({"ok": False, "policy_id": None, "error": str(exc)}))
@@ -1103,8 +1127,9 @@ def _cmd_policy_explain(args: argparse.Namespace) -> int:
1103
1127
 
1104
1128
  def _cmd_policy_lint(args: argparse.Namespace) -> int:
1105
1129
  from .policy import PolicyError, lint_policy, load_policy # noqa: PLC0415
1130
+ from .policy_profiles import resolve_policy_source # noqa: PLC0415
1106
1131
  try:
1107
- policy = load_policy(args.policy)
1132
+ policy = load_policy(resolve_policy_source(args.policy))
1108
1133
  except PolicyError as exc: # malformed policy is a lint failure too, with the parse reason
1109
1134
  if args.json: # emit an error object in --json (mirror _cmd_verify; exit 2 unchanged)
1110
1135
  print(json.dumps({"ok": False, "policy_id": None, "error": str(exc)}))
@@ -1124,6 +1149,22 @@ def _cmd_policy_lint(args: argparse.Namespace) -> int:
1124
1149
  return 0 if res["ok"] else 1
1125
1150
 
1126
1151
 
1152
+ def _cmd_policy_list_profiles(args: argparse.Namespace) -> int:
1153
+ from .policy import explain_policy, load_policy # noqa: PLC0415
1154
+ from .policy_profiles import list_profiles, profile_path # noqa: PLC0415
1155
+ rows = []
1156
+ for name in list_profiles():
1157
+ policy = load_policy(profile_path(name))
1158
+ rows.append({"name": name, "policy_id": policy.get("policy_id"),
1159
+ "schema": policy.get("schema"), "pin_count": len(explain_policy(policy))})
1160
+ if args.json:
1161
+ print(json.dumps(rows, indent=2, ensure_ascii=False))
1162
+ return 0
1163
+ for row in rows:
1164
+ print(f"{row['name']:22} {row['schema']:32} {row['pin_count']} pin(s) {row['policy_id']}")
1165
+ return 0
1166
+
1167
+
1127
1168
  def build_parser() -> argparse.ArgumentParser:
1128
1169
  parser = argparse.ArgumentParser(
1129
1170
  prog="proofbundle",
@@ -1156,8 +1197,19 @@ def build_parser() -> argparse.ArgumentParser:
1156
1197
  "bind a Key Binding JWT presentation to this verifier")
1157
1198
  verify.add_argument("--nonce", default=None,
1158
1199
  help="expected KB-JWT nonce (RFC 9901 §7.3 replay binding)")
1200
+ verify.add_argument("--expected-root", dest="expected_root", default=None, metavar="B64",
1201
+ help="authenticate the merkle root against a base64 value the relying party "
1202
+ "obtained OUT OF BAND (a pinned root, a signed checkpoint). The stated root "
1203
+ "is NOT signed, so a coherent one-leaf rewrap verifies under a different "
1204
+ "root; supplying this closes it — a mismatch FAILS (exit 1). Without it, "
1205
+ "root authenticity reads NOT_EVALUATED")
1206
+ verify.add_argument("--expected-tree-size", dest="expected_tree_size", default=None, type=int,
1207
+ metavar="N", help="require the merkle tree_size to equal N (guards tree-size "
1208
+ "substitution); a mismatch FAILS")
1159
1209
  verify.add_argument("--policy", default=None,
1160
- help="path to a trust-policy JSON (proofbundle/trust-policy/v0.1). Applies a "
1210
+ help="path to a trust-policy JSON (proofbundle/trust-policy/v0.1), OR the name "
1211
+ "of a packaged profile (WP3, e.g. strict-eval-v1 — see "
1212
+ "docs/POLICY_PROFILES.md; `policy list-profiles` lists them all). Applies a "
1161
1213
  "fail-closed, offline trust decision OVER the crypto result: without it "
1162
1214
  "POLICY reads NOT_EVALUATED; a policy failure is exit 3, distinct from a "
1163
1215
  "crypto failure (exit 1)")
@@ -1313,19 +1365,26 @@ def build_parser() -> argparse.ArgumentParser:
1313
1365
  policy_cmd = sub.add_parser(
1314
1366
  "policy", help="inspect a trust policy: explain its effective pins, lint for vacuousness")
1315
1367
  psub = policy_cmd.add_subparsers(dest="policy_command", required=True)
1368
+ _profile_help = ("path to a trust-policy JSON, OR the name of a packaged profile (WP3, "
1369
+ "see docs/POLICY_PROFILES.md) such as strict-eval-v1 — a real file of the "
1370
+ "same name always wins over a packaged profile")
1316
1371
  p_explain = psub.add_parser(
1317
1372
  "explain", help="list the effective pins a trust policy makes (what POLICY: OK will mean)")
1318
- p_explain.add_argument("policy", help="path to the trust-policy JSON")
1373
+ p_explain.add_argument("policy", help=_profile_help)
1319
1374
  p_explain.add_argument("--json", action="store_true", help="machine readable output")
1320
1375
  p_explain.set_defaults(func=_cmd_policy_explain)
1321
1376
  p_lint = psub.add_parser(
1322
1377
  "lint", help="fail (exit 1) on a WIRKUNGSLOSE policy that would produce a vacuous "
1323
1378
  "POLICY: OK; --strict also fails on attributes-to-nobody")
1324
- p_lint.add_argument("policy", help="path to the trust-policy JSON")
1379
+ p_lint.add_argument("policy", help=_profile_help)
1325
1380
  p_lint.add_argument("--strict", action="store_true",
1326
1381
  help="promote warnings (attributes to nobody) to lint failures")
1327
1382
  p_lint.add_argument("--json", action="store_true", help="machine readable output")
1328
1383
  p_lint.set_defaults(func=_cmd_policy_lint)
1384
+ p_list = psub.add_parser(
1385
+ "list-profiles", help="list the named trust-policy profiles shipped with this package (WP3)")
1386
+ p_list.add_argument("--json", action="store_true", help="machine readable output")
1387
+ p_list.set_defaults(func=_cmd_policy_list_profiles)
1329
1388
 
1330
1389
  prereg = sub.add_parser(
1331
1390
  "prereg",
@@ -52,6 +52,9 @@ __all__ = [
52
52
  "EVAL_CLAIM_SCHEMA", "COMMIT_ALG", "ASSURANCE_LEVELS", "canonicalize", "build_eval_claim",
53
53
  "emit_eval_receipt", "decode_eval_claim", "salted_commit", "issuer_fingerprint",
54
54
  "claim_warnings", "verify_commitment", "check_freshness", "sd_jwt_hidden_count",
55
+ "eval_evidence_class", "SCORE_EVIDENCE_CLASSES", "EXACT_SCORE_VERIFIED",
56
+ "THRESHOLD_VERDICT_VERIFIED", "SCORE_COMMITMENT_PRESENT", "SCORE_WITHHELD",
57
+ "METHODOLOGY_NOT_EVALUATED",
55
58
  ]
56
59
 
57
60
 
@@ -337,6 +340,67 @@ def claim_warnings(claim: dict) -> list:
337
340
  return out
338
341
 
339
342
 
343
+ # P0-B (Hardening 3.0.1 §7.1) — the machine-readable SCORE-evidence verdicts. A receipt today signs a
344
+ # THRESHOLD VERDICT (`passed` against the signed `comparator`/`threshold`): the exact score is used at
345
+ # emit time to COMPUTE `passed` and is then DISCARDED (build_eval_claim never stores it), so no output
346
+ # may imply an exact score was verified. The other classes are reachable only through the optional,
347
+ # additive exact-score profile (§7.2, EXPERIMENTAL, NOT part of the frozen 3.x core).
348
+ EXACT_SCORE_VERIFIED = "EXACT_SCORE_VERIFIED"
349
+ THRESHOLD_VERDICT_VERIFIED = "THRESHOLD_VERDICT_VERIFIED"
350
+ SCORE_COMMITMENT_PRESENT = "SCORE_COMMITMENT_PRESENT"
351
+ SCORE_WITHHELD = "SCORE_WITHHELD"
352
+ METHODOLOGY_NOT_EVALUATED = "METHODOLOGY_NOT_EVALUATED"
353
+ SCORE_EVIDENCE_CLASSES = (EXACT_SCORE_VERIFIED, THRESHOLD_VERDICT_VERIFIED,
354
+ SCORE_COMMITMENT_PRESENT, SCORE_WITHHELD)
355
+
356
+
357
+ def eval_evidence_class(claim: dict) -> dict:
358
+ """Classify what SCORE evidence a VERIFIED eval claim carries (never call on an unverified claim).
359
+
360
+ Returns ``{"score_evidence": <class>, "methodology": METHODOLOGY_NOT_EVALUATED, "detail": <str>}``.
361
+
362
+ Today every receipt is ``THRESHOLD_VERDICT_VERIFIED``: the frozen v0.1 schema has no ``score``
363
+ field, so a receipt proves only that ``passed`` holds for the signed ``comparator``/``threshold``.
364
+ ``methodology`` is ALWAYS ``METHODOLOGY_NOT_EVALUATED`` — a receipt never judges whether the suite
365
+ measures what it claims (No-Overclaim §0.5).
366
+
367
+ The remaining classes are reachable only through the optional, additive exact-score profile (§7.2,
368
+ field names provisional pending its ADR, EXPERIMENTAL, not in the 3.x core): a signed decimal-string
369
+ ``score`` whose recomputed ``passed`` AGREES → ``EXACT_SCORE_VERIFIED`` (a score present but
370
+ inconsistent with ``passed`` is a decode-time FAIL; if seen here it degrades to the threshold
371
+ verdict, never a false EXACT); a signed score COMMITMENT → ``SCORE_COMMITMENT_PRESENT`` (a binding,
372
+ NOT a range proof: it does not prove the hidden score crossed the threshold, §7.3); an explicit
373
+ withheld marker → ``SCORE_WITHHELD``.
374
+ """
375
+ methodology = METHODOLOGY_NOT_EVALUATED
376
+ comparator = claim.get("comparator")
377
+ threshold = claim.get("threshold")
378
+ passed = claim.get("passed")
379
+ score = claim.get("score")
380
+ if (isinstance(score, str) and _DECIMAL_RE.match(score) and comparator in _COMPARATORS
381
+ and isinstance(threshold, str) and _DECIMAL_RE.match(threshold) and isinstance(passed, bool)):
382
+ from decimal import Decimal, InvalidOperation # noqa: PLC0415
383
+ try:
384
+ recomputed = {">=": Decimal(score) >= Decimal(threshold), ">": Decimal(score) > Decimal(threshold),
385
+ "<=": Decimal(score) <= Decimal(threshold), "<": Decimal(score) < Decimal(threshold)}[comparator]
386
+ except InvalidOperation:
387
+ recomputed = None
388
+ if recomputed is passed:
389
+ return {"score_evidence": EXACT_SCORE_VERIFIED, "methodology": methodology,
390
+ "detail": "exact score signed and consistent with the threshold verdict"}
391
+ return {"score_evidence": THRESHOLD_VERDICT_VERIFIED, "methodology": methodology,
392
+ "detail": "score present but not consistent with `passed` — only the threshold verdict stands"}
393
+ if claim.get("score_commit") or claim.get("score_commitment"):
394
+ return {"score_evidence": SCORE_COMMITMENT_PRESENT, "methodology": methodology,
395
+ "detail": "a score COMMITMENT is present — a binding, NOT a range proof: it does not "
396
+ "prove the hidden score crossed the threshold"}
397
+ if claim.get("score_withheld") is True:
398
+ return {"score_evidence": SCORE_WITHHELD, "methodology": methodology,
399
+ "detail": "the exact score is deliberately withheld; only the threshold verdict is signed"}
400
+ return {"score_evidence": THRESHOLD_VERDICT_VERIFIED, "methodology": methodology,
401
+ "detail": "proves `passed` against the signed threshold, not an exact score"}
402
+
403
+
340
404
  def verify_commitment(identifier: str, salt: bytes, commitment: str) -> bool:
341
405
  """Check that a PRESENTED identifier (+ its salt) matches a salted commitment in a claim
342
406
  (``model_id_commit`` / ``dataset_id_commit``). Makes a model-swap visible: a claim that silently swapped
@@ -0,0 +1,14 @@
1
+ {
2
+ "schema": "proofbundle/trust-policy/v0.2",
3
+ "policy_id": "proofbundle-policy/decision-receipt-v1",
4
+ "allowed_schema_versions": ["proofbundle/v0.1"],
5
+ "signature": {"allowed_algs": ["ed25519"]},
6
+ "decision_receipt": {
7
+ "accepted_predicate_types": ["https://b7n0de.com/proofbundle/predicates/decision-receipt/v0.1"],
8
+ "require_not_checked": true,
9
+ "require_decision_change_conditions": true,
10
+ "require_audience": true,
11
+ "require_nonce": true,
12
+ "allow_raw_inputs": false
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "schema": "proofbundle/trust-policy/v0.1",
3
+ "policy_id": "proofbundle-policy/research-preview-v1",
4
+ "allowed_schema_versions": ["proofbundle/v0.1"],
5
+ "signature": {"allowed_algs": ["ed25519"]},
6
+ "merkle": {"required_hash_alg": "sha256-rfc6962"}
7
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schema": "proofbundle/trust-policy/v0.1",
3
+ "policy_id": "proofbundle-policy/strict-eval-authenticated-root-v1",
4
+ "allowed_schema_versions": ["proofbundle/v0.1"],
5
+ "signature": {"allowed_algs": ["ed25519"]},
6
+ "merkle": {"required_hash_alg": "sha256-rfc6962", "require_authenticated_root": true},
7
+ "sd_jwt": {"require_key_binding_when_cnf_present": true},
8
+ "assurance": {"minimum_level": "reproduced", "reject_self_attested_without_prereg": true}
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schema": "proofbundle/trust-policy/v0.1",
3
+ "policy_id": "proofbundle-policy/strict-eval-v1",
4
+ "allowed_schema_versions": ["proofbundle/v0.1"],
5
+ "signature": {"allowed_algs": ["ed25519"]},
6
+ "merkle": {"required_hash_alg": "sha256-rfc6962"},
7
+ "sd_jwt": {"require_key_binding_when_cnf_present": true},
8
+ "assurance": {"minimum_level": "reproduced", "reject_self_attested_without_prereg": true}
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schema": "proofbundle/trust-policy/v0.2",
3
+ "policy_id": "proofbundle-policy/strict-prereg-v1",
4
+ "allowed_schema_versions": ["proofbundle/v0.1"],
5
+ "signature": {"allowed_algs": ["ed25519"]},
6
+ "merkle": {"required_hash_alg": "sha256-rfc6962"},
7
+ "assurance": {"reject_self_attested_without_prereg": true},
8
+ "anchors": {"require_anchor": "any", "require_anchor_target": "preRegistration", "allow_pending": false}
9
+ }