proofbundle 3.0.0__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.0/src/proofbundle.egg-info → proofbundle-3.1.0}/PKG-INFO +86 -44
  2. {proofbundle-3.0.0 → proofbundle-3.1.0}/README.md +85 -43
  3. {proofbundle-3.0.0 → proofbundle-3.1.0}/pyproject.toml +2 -2
  4. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/__init__.py +1 -1
  5. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/eee.py +17 -4
  6. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/bundle.py +86 -4
  7. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/cli.py +68 -9
  8. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/evalclaim.py +64 -0
  9. proofbundle-3.1.0/src/proofbundle/policies/decision-receipt-v1.json +14 -0
  10. proofbundle-3.1.0/src/proofbundle/policies/research-preview-v1.json +7 -0
  11. proofbundle-3.1.0/src/proofbundle/policies/strict-eval-authenticated-root-v1.json +9 -0
  12. proofbundle-3.1.0/src/proofbundle/policies/strict-eval-v1.json +9 -0
  13. proofbundle-3.1.0/src/proofbundle/policies/strict-prereg-v1.json +9 -0
  14. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/policy.py +66 -3
  15. proofbundle-3.1.0/src/proofbundle/policy_profiles.py +86 -0
  16. {proofbundle-3.0.0 → proofbundle-3.1.0/src/proofbundle.egg-info}/PKG-INFO +86 -44
  17. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle.egg-info/SOURCES.txt +10 -0
  18. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_claims_hygiene.py +86 -0
  19. proofbundle-3.1.0/tests/test_eval_evidence_class.py +113 -0
  20. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_hf_entry_verify_eee_digest.py +8 -0
  21. proofbundle-3.1.0/tests/test_policy_profiles.py +199 -0
  22. proofbundle-3.1.0/tests/test_release_integrity_gate.py +88 -0
  23. proofbundle-3.1.0/tests/test_root_authenticity.py +244 -0
  24. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_trust_policy.py +16 -0
  25. {proofbundle-3.0.0 → proofbundle-3.1.0}/LICENSE +0 -0
  26. {proofbundle-3.0.0 → proofbundle-3.1.0}/setup.cfg +0 -0
  27. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/_inspect_registry.py +0 -0
  28. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/_integration.py +0 -0
  29. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/_strict_json.py +0 -0
  30. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/__init__.py +0 -0
  31. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/_provenance.py +0 -0
  32. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
  33. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/lm_eval.py +0 -0
  34. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/promptfoo.py +0 -0
  35. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/samples.py +0 -0
  36. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/anchors.py +0 -0
  37. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/anchors_chia.py +0 -0
  38. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/anchors_chia_add.py +0 -0
  39. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/anchors_markovian.py +0 -0
  40. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/anchors_ots.py +0 -0
  41. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/anchors_rfc3161.py +0 -0
  42. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/beacon.py +0 -0
  43. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/canonical.py +0 -0
  44. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/checkpoint.py +0 -0
  45. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/decision.py +0 -0
  46. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/demo.py +0 -0
  47. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/dsse.py +0 -0
  48. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/eee_eval_schema.json +0 -0
  49. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/emit.py +0 -0
  50. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/errors.py +0 -0
  51. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/experimental/__init__.py +0 -0
  52. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/experimental/enclave.py +0 -0
  53. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/hf_evals.py +0 -0
  54. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/inspect_hook.py +0 -0
  55. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/intoto.py +0 -0
  56. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/kbjwt.py +0 -0
  57. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/merkle.py +0 -0
  58. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/persample.py +0 -0
  59. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/prereg.py +0 -0
  60. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/py.typed +0 -0
  61. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/pytest_plugin.py +0 -0
  62. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/sdjwt.py +0 -0
  63. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/sdjwt_issue.py +0 -0
  64. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/signature.py +0 -0
  65. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/statuslist.py +0 -0
  66. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/tlogproof.py +0 -0
  67. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
  68. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
  69. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle.egg-info/requires.txt +0 -0
  70. {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle.egg-info/top_level.txt +0 -0
  71. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_adapters.py +0 -0
  72. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_adversarial.py +0 -0
  73. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchor_target_trustedtime.py +0 -0
  74. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_chia.py +0 -0
  75. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_chia_add.py +0 -0
  76. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_chia_claims.py +0 -0
  77. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_generic.py +0 -0
  78. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_markovian.py +0 -0
  79. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_ots.py +0 -0
  80. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_rfc3161.py +0 -0
  81. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_beacon.py +0 -0
  82. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_branch_base_check.py +0 -0
  83. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_bundle.py +0 -0
  84. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_bundle_robustness.py +0 -0
  85. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_canonical.py +0 -0
  86. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_checkpoint.py +0 -0
  87. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_cli.py +0 -0
  88. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_cli_eval.py +0 -0
  89. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_cli_require_anchor.py +0 -0
  90. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_conformance.py +0 -0
  91. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_cosignature.py +0 -0
  92. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_cosignature_mldsa.py +0 -0
  93. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_anchors.py +0 -0
  94. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_cli.py +0 -0
  95. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_evidence.py +0 -0
  96. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_fuzz.py +0 -0
  97. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_hardening.py +0 -0
  98. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_policy.py +0 -0
  99. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_schema.py +0 -0
  100. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_validator_api.py +0 -0
  101. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_verify.py +0 -0
  102. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_demo.py +0 -0
  103. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_docs_truth.py +0 -0
  104. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_dup_key_reject.py +0 -0
  105. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_ed25519_semantics.py +0 -0
  106. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_eee.py +0 -0
  107. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_emit.py +0 -0
  108. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_eval_claim_schema.py +0 -0
  109. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_evalclaim.py +0 -0
  110. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_examples.py +0 -0
  111. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_experimental_enclave.py +0 -0
  112. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_fork_pr_secret_isolation.py +0 -0
  113. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_fuzz_parsers.py +0 -0
  114. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_hf_evals.py +0 -0
  115. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_inspect_hook.py +0 -0
  116. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto.py +0 -0
  117. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_claims_hygiene.py +0 -0
  118. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_content_root_migration.py +0 -0
  119. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_dsse.py +0 -0
  120. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_eval_result.py +0 -0
  121. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_examples.py +0 -0
  122. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_spec_diff.py +0 -0
  123. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_svr.py +0 -0
  124. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_kbjwt.py +0 -0
  125. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_merkle.py +0 -0
  126. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_merkle_property.py +0 -0
  127. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_ok_semantics.py +0 -0
  128. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_oversized_int_guard.py +0 -0
  129. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_persample.py +0 -0
  130. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_pinned_key_validation.py +0 -0
  131. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_policy_explain_lint.py +0 -0
  132. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_predicate_type_enforcement.py +0 -0
  133. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_prereg.py +0 -0
  134. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_promptfoo.py +0 -0
  135. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_provenance.py +0 -0
  136. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_pytest_plugin.py +0 -0
  137. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_rekor_interop.py +0 -0
  138. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_rfc6962_external_vectors.py +0 -0
  139. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_schema.py +0 -0
  140. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_sdjwt_duplicate_cnf.py +0 -0
  141. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_sdjwt_issue.py +0 -0
  142. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_sdjwt_reference.py +0 -0
  143. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_sdjwt_verify_binding.py +0 -0
  144. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_signature.py +0 -0
  145. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_statuslist.py +0 -0
  146. {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_tlogproof.py +0 -0
  147. {proofbundle-3.0.0 → 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.0
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>
@@ -75,12 +75,29 @@ Merkle, one file, no server, no network.
75
75
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21230467.svg)](https://doi.org/10.5281/zenodo.21230467)
76
76
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
77
77
  [![Mutation tested](https://img.shields.io/badge/tests-mutation_gated-D6248A.svg)](https://github.com/b7n0de/proofbundle/blob/main/scripts/mutation_check.py)
78
- <!-- SLSA / PEP 740 attestation badges follow once the first attested release lands, see RELEASE.md. -->
78
+ [![PyPI attestations](https://img.shields.io/badge/PyPI-PEP_740_attested-D6248A.svg)](https://pypi.org/project/proofbundle/#files)
79
+ [![SLSA provenance](https://img.shields.io/badge/SLSA-build_provenance-D6248A.svg)](https://slsa.dev/spec/v1.0/provenance)
79
80
 
80
81
  **Reviewing this for adoption?** Start with the 30-minute adversarial audit path: **[docs/REVIEWERS.md](https://github.com/b7n0de/proofbundle/blob/main/docs/REVIEWERS.md)**.
81
82
 
82
83
  </div>
83
84
 
85
+ ## Contents
86
+
87
+ - [60-second try](#60-second-try-offline)
88
+ - [Inspect-native?](#inspect-native-metr-task-standard-uk-aisi-ecosystem)
89
+ - [The problem](#the-problem)
90
+ - [What a receipt proves, and what it doesn't](#what-a-receipt-proves-and-what-it-doesnt)
91
+ - [How it fits together](#how-it-fits-together)
92
+ - [Cite this work](#cite-this-work)
93
+ - [Post-quantum posture](#post-quantum-posture-honest-two-layers)
94
+ - [What's in the box](#whats-in-the-box)
95
+ - [Docs](#docs)
96
+ - [Install](#install)
97
+ - [Status, scope and roadmap](#status-scope-and-roadmap)
98
+ - [Contributing](#contributing)
99
+ - [License](#license)
100
+
84
101
  ## 60-second try (offline)
85
102
 
86
103
  ```bash
@@ -91,21 +108,9 @@ git clone https://github.com/b7n0de/proofbundle && cd proofbundle
91
108
  pip install -e ".[eval,inspect]" && make demo # or `make full-demo` for log -> receipt -> verify
92
109
  ```
93
110
 
94
- ## The problem
95
-
96
- Every AI eval number you read a safety benchmark, a capability score, a leaderboard entry — is an
97
- **unverifiable claim**. You trust the lab. There's no portable way to check, offline, that a result
98
- was signed by a stated party, hasn't been altered, and covers the samples it claims.
99
-
100
- proofbundle is that check. It's a small MIT-licensed Python tool (a compact, auditable trusted core,
101
- depends only on [`cryptography`](https://cryptography.io)) that turns a result into a signed
102
- receipt anyone can verify from a single file — and it's honest about the line it does not cross.
103
-
104
- ## What the demo shows
105
-
106
- You'll see an honest receipt verify `=> OK`, then six independent tampers each verify `FAILED`, then
107
- a swapped sample get caught — all in memory. `proofbundle demo` exits non-zero if any tamper slips through,
108
- so it's also a self-test. Full walkthrough: **[docs/DEMO.md](https://github.com/b7n0de/proofbundle/blob/main/docs/DEMO.md)**.
111
+ The demo runs entirely in memory: an honest receipt verifies `=> OK`, six independent tampers each
112
+ verify `FAILED`, and a swapped sample gets caught. `proofbundle demo` exits non-zero if any tamper
113
+ slips through, so it doubles as a self-test. Full walkthrough: **[docs/DEMO.md](https://github.com/b7n0de/proofbundle/blob/main/docs/DEMO.md)**.
109
114
 
110
115
  ```bash
111
116
  # verify a real hosted receipt without writing any code:
@@ -120,7 +125,7 @@ proofbundle verify receipt.json # exit 0 = crypto OK, 1 = crypto/verifica
120
125
  proofbundle verify receipt.json --policy trust_policy.json # POLICY: OK | FAIL (exit 3) | NOT_EVALUATED
121
126
  ```
122
127
 
123
- ## Inspect-native? (METR Task Standard / UK-AISI ecosystem)
128
+ ## Inspect-native? (METR Task Standard, UK-AISI ecosystem)
124
129
 
125
130
  The receipt layer runs directly on [Inspect AI](https://inspect.aisi.org.uk/) — and the proof is
126
131
  reproducible offline in minutes:
@@ -135,7 +140,22 @@ In your own pipeline the end-of-task hook signs every run automatically. Walkthr
135
140
  **[docs/INSPECT_HAPPY_PATH.md](https://github.com/b7n0de/proofbundle/blob/main/docs/INSPECT_HAPPY_PATH.md)** · worked example:
136
141
  **[examples/inspect_receipt.py](https://github.com/b7n0de/proofbundle/blob/main/examples/inspect_receipt.py)**.
137
142
 
138
- ## What a receipt proves — and what it doesn't
143
+ ## The problem
144
+
145
+ Every AI eval number you read — a safety benchmark, a capability score, a leaderboard entry — is an
146
+ **unverifiable claim**. You trust the lab. There's no portable way to check, offline, that a result
147
+ was signed by a stated party, hasn't been altered, and covers the samples it claims.
148
+
149
+ proofbundle is that check. It's a small MIT-licensed Python tool (a compact, auditable trusted core,
150
+ depends only on [`cryptography`](https://cryptography.io)) that turns a result into a signed
151
+ receipt anyone can verify from a single file — and it's honest about the line it does not cross.
152
+
153
+ In plain language: a proofbundle receipt is the cash-register receipt of an AI test result. It shows
154
+ who claimed the number and that nobody quietly changed it afterwards. It does not show the test was
155
+ good — the way a cash-register receipt does not show the meal was good — but without a receipt there
156
+ is nothing to check at all.
157
+
158
+ ## What a receipt proves, and what it doesn't
139
159
 
140
160
  | ✅ It proves | ❌ It does **not** prove |
141
161
  |---|---|
@@ -149,6 +169,43 @@ This boundary is the point, not a weakness. A receipt makes a claim **attributab
149
169
  and — with pre-registration and per-sample auditing — bounded and spot-checkable**. Full detail:
150
170
  **[THREAT_MODEL.md](https://github.com/b7n0de/proofbundle/blob/main/THREAT_MODEL.md)**.
151
171
 
172
+ ## How it fits together
173
+
174
+ *(diagram renders on GitHub — [view it there](https://github.com/b7n0de/proofbundle#how-it-fits-together); PyPI shows the source)*
175
+
176
+ ```mermaid
177
+ flowchart LR
178
+ H["eval harness<br/>inspect_ai · lm-eval · promptfoo · pytest"] --> A["adapter → signed claim<br/>salted commitments · provenance · samples root"]
179
+ A --> R["receipt<br/>one portable file"]
180
+ R --> V{{"proofbundle verify — offline"}}
181
+ V --> C["signature · Merkle inclusion · SD-JWT/KB ·<br/>witness quorum · status list · sample openings"]
182
+ C --> OK(["CRYPTO: OK / FAILED"])
183
+ style V fill:#D6248A,stroke:#D6248A,color:#fff
184
+ style OK fill:#D6248A,stroke:#D6248A,color:#fff
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
+
152
209
  ## Post-quantum posture (honest, two layers)
153
210
 
154
211
  proofbundle is **not** "quantum-proof" or "quantum-safe" as a whole. It combines two cryptographic layers
@@ -173,28 +230,6 @@ On the witness side, C2SP checkpoints already carry post-quantum **ML-DSA-44** (
173
230
  (`proofbundle[pq]`); a post-quantum *payload* signature — crypto-agility for the receipt itself — is on the
174
231
  roadmap.
175
232
 
176
- ## In plain language
177
-
178
- A proofbundle receipt is the cash-register receipt of an AI test result: it shows who claimed the
179
- number and that nobody quietly changed it afterwards. It does not show the test was good — the way a
180
- cash-register receipt does not show the meal was good — but without a receipt there is nothing to
181
- check at all.
182
-
183
- ## How it fits together
184
-
185
- *(diagram renders on GitHub — [view it there](https://github.com/b7n0de/proofbundle#how-it-fits-together); PyPI shows the source)*
186
-
187
- ```mermaid
188
- flowchart LR
189
- H["eval harness<br/>inspect_ai · lm-eval · promptfoo · pytest"] --> A["adapter → signed claim<br/>salted commitments · provenance · samples root"]
190
- A --> R["receipt<br/>one portable file"]
191
- R --> V{{"proofbundle verify — offline"}}
192
- V --> C["signature · Merkle inclusion · SD-JWT/KB ·<br/>witness quorum · status list · sample openings"]
193
- C --> OK(["CRYPTO: OK / FAILED"])
194
- style V fill:#D6248A,stroke:#D6248A,color:#fff
195
- style OK fill:#D6248A,stroke:#D6248A,color:#fff
196
- ```
197
-
198
233
  ## What's in the box
199
234
 
200
235
  - **Core** — Ed25519 signature + RFC 6962 / 9162 Merkle inclusion, verified fully offline. Checks a
@@ -285,7 +320,7 @@ pip install "proofbundle[pq]" # verify ML-DSA-44 (post-quantum) witnes
285
320
  Requires Python 3.10+. The verify path never rolls its own crypto — Ed25519 comes from
286
321
  `cryptography`; Merkle hashing is RFC 6962.
287
322
 
288
- ## Status & scope
323
+ ## Status, scope and roadmap
289
324
 
290
325
  Beta, SemVer-committed, with a CI test suite behind a mutation gate + property-based parser fuzzing. Correctness
291
326
  is anchored to external RFC 6962 vectors and a real Rekor proof, not just its own bundles. It is
@@ -293,6 +328,13 @@ is anchored to external RFC 6962 vectors and a real Rekor proof, not just its ow
293
328
  by itself — it is the small, offline, standards-native receipt layer between them. Security policy:
294
329
  [SECURITY.md](https://github.com/b7n0de/proofbundle/blob/main/SECURITY.md).
295
330
 
331
+ **Roadmap (stated honestly, all forward-looking):**
332
+
333
+ - A post-quantum *payload* signature (crypto-agility for the receipt itself) is on the roadmap; today the
334
+ post-quantum coverage is witness-side ML-DSA-44 cosignatures only.
335
+ - A CLI flag to select the content-root algorithm is still deferred (`jcs-sha256-v1` is the signed default).
336
+ - **Preview:** a TEE-attestation bridge — see [docs/EXPERIMENTAL_ENCLAVE.md](https://github.com/b7n0de/proofbundle/blob/main/docs/EXPERIMENTAL_ENCLAVE.md).
337
+
296
338
  ## Contributing
297
339
 
298
340
  See [CONTRIBUTING.md](https://github.com/b7n0de/proofbundle/blob/main/CONTRIBUTING.md) and the [Code of Conduct](https://github.com/b7n0de/proofbundle/blob/main/CODE_OF_CONDUCT.md). Good first
@@ -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>
@@ -21,12 +21,29 @@ Merkle, one file, no server, no network.
21
21
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21230467.svg)](https://doi.org/10.5281/zenodo.21230467)
22
22
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
23
23
  [![Mutation tested](https://img.shields.io/badge/tests-mutation_gated-D6248A.svg)](https://github.com/b7n0de/proofbundle/blob/main/scripts/mutation_check.py)
24
- <!-- SLSA / PEP 740 attestation badges follow once the first attested release lands, see RELEASE.md. -->
24
+ [![PyPI attestations](https://img.shields.io/badge/PyPI-PEP_740_attested-D6248A.svg)](https://pypi.org/project/proofbundle/#files)
25
+ [![SLSA provenance](https://img.shields.io/badge/SLSA-build_provenance-D6248A.svg)](https://slsa.dev/spec/v1.0/provenance)
25
26
 
26
27
  **Reviewing this for adoption?** Start with the 30-minute adversarial audit path: **[docs/REVIEWERS.md](https://github.com/b7n0de/proofbundle/blob/main/docs/REVIEWERS.md)**.
27
28
 
28
29
  </div>
29
30
 
31
+ ## Contents
32
+
33
+ - [60-second try](#60-second-try-offline)
34
+ - [Inspect-native?](#inspect-native-metr-task-standard-uk-aisi-ecosystem)
35
+ - [The problem](#the-problem)
36
+ - [What a receipt proves, and what it doesn't](#what-a-receipt-proves-and-what-it-doesnt)
37
+ - [How it fits together](#how-it-fits-together)
38
+ - [Cite this work](#cite-this-work)
39
+ - [Post-quantum posture](#post-quantum-posture-honest-two-layers)
40
+ - [What's in the box](#whats-in-the-box)
41
+ - [Docs](#docs)
42
+ - [Install](#install)
43
+ - [Status, scope and roadmap](#status-scope-and-roadmap)
44
+ - [Contributing](#contributing)
45
+ - [License](#license)
46
+
30
47
  ## 60-second try (offline)
31
48
 
32
49
  ```bash
@@ -37,21 +54,9 @@ git clone https://github.com/b7n0de/proofbundle && cd proofbundle
37
54
  pip install -e ".[eval,inspect]" && make demo # or `make full-demo` for log -> receipt -> verify
38
55
  ```
39
56
 
40
- ## The problem
41
-
42
- Every AI eval number you read a safety benchmark, a capability score, a leaderboard entry — is an
43
- **unverifiable claim**. You trust the lab. There's no portable way to check, offline, that a result
44
- was signed by a stated party, hasn't been altered, and covers the samples it claims.
45
-
46
- proofbundle is that check. It's a small MIT-licensed Python tool (a compact, auditable trusted core,
47
- depends only on [`cryptography`](https://cryptography.io)) that turns a result into a signed
48
- receipt anyone can verify from a single file — and it's honest about the line it does not cross.
49
-
50
- ## What the demo shows
51
-
52
- You'll see an honest receipt verify `=> OK`, then six independent tampers each verify `FAILED`, then
53
- a swapped sample get caught — all in memory. `proofbundle demo` exits non-zero if any tamper slips through,
54
- so it's also a self-test. Full walkthrough: **[docs/DEMO.md](https://github.com/b7n0de/proofbundle/blob/main/docs/DEMO.md)**.
57
+ The demo runs entirely in memory: an honest receipt verifies `=> OK`, six independent tampers each
58
+ verify `FAILED`, and a swapped sample gets caught. `proofbundle demo` exits non-zero if any tamper
59
+ slips through, so it doubles as a self-test. Full walkthrough: **[docs/DEMO.md](https://github.com/b7n0de/proofbundle/blob/main/docs/DEMO.md)**.
55
60
 
56
61
  ```bash
57
62
  # verify a real hosted receipt without writing any code:
@@ -66,7 +71,7 @@ proofbundle verify receipt.json # exit 0 = crypto OK, 1 = crypto/verifica
66
71
  proofbundle verify receipt.json --policy trust_policy.json # POLICY: OK | FAIL (exit 3) | NOT_EVALUATED
67
72
  ```
68
73
 
69
- ## Inspect-native? (METR Task Standard / UK-AISI ecosystem)
74
+ ## Inspect-native? (METR Task Standard, UK-AISI ecosystem)
70
75
 
71
76
  The receipt layer runs directly on [Inspect AI](https://inspect.aisi.org.uk/) — and the proof is
72
77
  reproducible offline in minutes:
@@ -81,7 +86,22 @@ In your own pipeline the end-of-task hook signs every run automatically. Walkthr
81
86
  **[docs/INSPECT_HAPPY_PATH.md](https://github.com/b7n0de/proofbundle/blob/main/docs/INSPECT_HAPPY_PATH.md)** · worked example:
82
87
  **[examples/inspect_receipt.py](https://github.com/b7n0de/proofbundle/blob/main/examples/inspect_receipt.py)**.
83
88
 
84
- ## What a receipt proves — and what it doesn't
89
+ ## The problem
90
+
91
+ Every AI eval number you read — a safety benchmark, a capability score, a leaderboard entry — is an
92
+ **unverifiable claim**. You trust the lab. There's no portable way to check, offline, that a result
93
+ was signed by a stated party, hasn't been altered, and covers the samples it claims.
94
+
95
+ proofbundle is that check. It's a small MIT-licensed Python tool (a compact, auditable trusted core,
96
+ depends only on [`cryptography`](https://cryptography.io)) that turns a result into a signed
97
+ receipt anyone can verify from a single file — and it's honest about the line it does not cross.
98
+
99
+ In plain language: a proofbundle receipt is the cash-register receipt of an AI test result. It shows
100
+ who claimed the number and that nobody quietly changed it afterwards. It does not show the test was
101
+ good — the way a cash-register receipt does not show the meal was good — but without a receipt there
102
+ is nothing to check at all.
103
+
104
+ ## What a receipt proves, and what it doesn't
85
105
 
86
106
  | ✅ It proves | ❌ It does **not** prove |
87
107
  |---|---|
@@ -95,6 +115,43 @@ This boundary is the point, not a weakness. A receipt makes a claim **attributab
95
115
  and — with pre-registration and per-sample auditing — bounded and spot-checkable**. Full detail:
96
116
  **[THREAT_MODEL.md](https://github.com/b7n0de/proofbundle/blob/main/THREAT_MODEL.md)**.
97
117
 
118
+ ## How it fits together
119
+
120
+ *(diagram renders on GitHub — [view it there](https://github.com/b7n0de/proofbundle#how-it-fits-together); PyPI shows the source)*
121
+
122
+ ```mermaid
123
+ flowchart LR
124
+ H["eval harness<br/>inspect_ai · lm-eval · promptfoo · pytest"] --> A["adapter → signed claim<br/>salted commitments · provenance · samples root"]
125
+ A --> R["receipt<br/>one portable file"]
126
+ R --> V{{"proofbundle verify — offline"}}
127
+ V --> C["signature · Merkle inclusion · SD-JWT/KB ·<br/>witness quorum · status list · sample openings"]
128
+ C --> OK(["CRYPTO: OK / FAILED"])
129
+ style V fill:#D6248A,stroke:#D6248A,color:#fff
130
+ style OK fill:#D6248A,stroke:#D6248A,color:#fff
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
+
98
155
  ## Post-quantum posture (honest, two layers)
99
156
 
100
157
  proofbundle is **not** "quantum-proof" or "quantum-safe" as a whole. It combines two cryptographic layers
@@ -119,28 +176,6 @@ On the witness side, C2SP checkpoints already carry post-quantum **ML-DSA-44** (
119
176
  (`proofbundle[pq]`); a post-quantum *payload* signature — crypto-agility for the receipt itself — is on the
120
177
  roadmap.
121
178
 
122
- ## In plain language
123
-
124
- A proofbundle receipt is the cash-register receipt of an AI test result: it shows who claimed the
125
- number and that nobody quietly changed it afterwards. It does not show the test was good — the way a
126
- cash-register receipt does not show the meal was good — but without a receipt there is nothing to
127
- check at all.
128
-
129
- ## How it fits together
130
-
131
- *(diagram renders on GitHub — [view it there](https://github.com/b7n0de/proofbundle#how-it-fits-together); PyPI shows the source)*
132
-
133
- ```mermaid
134
- flowchart LR
135
- H["eval harness<br/>inspect_ai · lm-eval · promptfoo · pytest"] --> A["adapter → signed claim<br/>salted commitments · provenance · samples root"]
136
- A --> R["receipt<br/>one portable file"]
137
- R --> V{{"proofbundle verify — offline"}}
138
- V --> C["signature · Merkle inclusion · SD-JWT/KB ·<br/>witness quorum · status list · sample openings"]
139
- C --> OK(["CRYPTO: OK / FAILED"])
140
- style V fill:#D6248A,stroke:#D6248A,color:#fff
141
- style OK fill:#D6248A,stroke:#D6248A,color:#fff
142
- ```
143
-
144
179
  ## What's in the box
145
180
 
146
181
  - **Core** — Ed25519 signature + RFC 6962 / 9162 Merkle inclusion, verified fully offline. Checks a
@@ -231,7 +266,7 @@ pip install "proofbundle[pq]" # verify ML-DSA-44 (post-quantum) witnes
231
266
  Requires Python 3.10+. The verify path never rolls its own crypto — Ed25519 comes from
232
267
  `cryptography`; Merkle hashing is RFC 6962.
233
268
 
234
- ## Status & scope
269
+ ## Status, scope and roadmap
235
270
 
236
271
  Beta, SemVer-committed, with a CI test suite behind a mutation gate + property-based parser fuzzing. Correctness
237
272
  is anchored to external RFC 6962 vectors and a real Rekor proof, not just its own bundles. It is
@@ -239,6 +274,13 @@ is anchored to external RFC 6962 vectors and a real Rekor proof, not just its ow
239
274
  by itself — it is the small, offline, standards-native receipt layer between them. Security policy:
240
275
  [SECURITY.md](https://github.com/b7n0de/proofbundle/blob/main/SECURITY.md).
241
276
 
277
+ **Roadmap (stated honestly, all forward-looking):**
278
+
279
+ - A post-quantum *payload* signature (crypto-agility for the receipt itself) is on the roadmap; today the
280
+ post-quantum coverage is witness-side ML-DSA-44 cosignatures only.
281
+ - A CLI flag to select the content-root algorithm is still deferred (`jcs-sha256-v1` is the signed default).
282
+ - **Preview:** a TEE-attestation bridge — see [docs/EXPERIMENTAL_ENCLAVE.md](https://github.com/b7n0de/proofbundle/blob/main/docs/EXPERIMENTAL_ENCLAVE.md).
283
+
242
284
  ## Contributing
243
285
 
244
286
  See [CONTRIBUTING.md](https://github.com/b7n0de/proofbundle/blob/main/CONTRIBUTING.md) and the [Code of Conduct](https://github.com/b7n0de/proofbundle/blob/main/CODE_OF_CONDUCT.md). Good first
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proofbundle"
7
- version = "3.0.0"
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.0"
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
@@ -99,14 +99,27 @@ def _extract_score(score_details: dict, metric_config: dict) -> str:
99
99
 
100
100
  def _model_id_stripped(record: dict) -> dict:
101
101
  """A deep copy of the EEE record with the cleartext model identity removed (WP-I3 privacy fix).
102
- Removes ``model_info.id`` and the top-level ``evaluation_id`` (which embeds the id) so a digest
103
- over the result cannot be used as a model-id confirmation / enumeration oracle, while still
104
- binding every score, timestamp, dataset and metric for tamper-evidence."""
102
+ Removes ``model_info.id`` and the id-bearing top-level fields (``evaluation_id`` which embeds the
103
+ id, and ``evaluation_result_id``) so a digest over the result cannot be used as a model-id
104
+ confirmation / enumeration oracle, while still binding every score, timestamp, dataset and metric
105
+ for tamper-evidence.
106
+
107
+ M2 (6-lens review 2026-07-11): ``evaluation_result_id`` was left IN the digest record while the
108
+ run_id provenance path already `_leaks_model_id`-guards it — an asymmetry that kept the digest a
109
+ model-id oracle (the id can correlate to / embed the model). It is a provenance identifier, not a
110
+ scored/content field, so stripping it from the DIGEST removes the oracle without weakening the
111
+ tamper-evidence over the actual result content (it stays available for run_id provenance, which
112
+ applies its own leak guard)."""
105
113
  import copy # noqa: PLC0415
106
114
  r = copy.deepcopy(record)
107
115
  if isinstance(r.get("model_info"), dict):
108
116
  r["model_info"].pop("id", None)
109
- r.pop("evaluation_id", None) # format eval_name/model_id/timestamp — embeds the id
117
+ r.pop("evaluation_id", None) # format eval_name/model_id/timestamp — embeds the id
118
+ # M2: strip the per-result ``evaluation_result_id`` (nested in each ``evaluation_results[*]``) —
119
+ # it is a provenance id that can embed/correlate the model id (a digest over it is an oracle).
120
+ for er in r.get("evaluation_results", []) or []:
121
+ if isinstance(er, dict):
122
+ er.pop("evaluation_result_id", None)
110
123
  return r
111
124
 
112
125
 
@@ -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