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.
- {proofbundle-3.0.1/src/proofbundle.egg-info → proofbundle-3.1.0}/PKG-INFO +26 -3
- {proofbundle-3.0.1 → proofbundle-3.1.0}/README.md +25 -2
- {proofbundle-3.0.1 → proofbundle-3.1.0}/pyproject.toml +2 -2
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/__init__.py +1 -1
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/bundle.py +86 -4
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/cli.py +68 -9
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/evalclaim.py +64 -0
- proofbundle-3.1.0/src/proofbundle/policies/decision-receipt-v1.json +14 -0
- proofbundle-3.1.0/src/proofbundle/policies/research-preview-v1.json +7 -0
- proofbundle-3.1.0/src/proofbundle/policies/strict-eval-authenticated-root-v1.json +9 -0
- proofbundle-3.1.0/src/proofbundle/policies/strict-eval-v1.json +9 -0
- proofbundle-3.1.0/src/proofbundle/policies/strict-prereg-v1.json +9 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/policy.py +66 -3
- proofbundle-3.1.0/src/proofbundle/policy_profiles.py +86 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0/src/proofbundle.egg-info}/PKG-INFO +26 -3
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle.egg-info/SOURCES.txt +9 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_claims_hygiene.py +86 -0
- proofbundle-3.1.0/tests/test_eval_evidence_class.py +113 -0
- proofbundle-3.1.0/tests/test_policy_profiles.py +199 -0
- proofbundle-3.1.0/tests/test_root_authenticity.py +244 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_trust_policy.py +16 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/LICENSE +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/setup.cfg +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/_integration.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/_strict_json.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/__init__.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/_provenance.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/eee.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/promptfoo.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/adapters/samples.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/anchors.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/anchors_chia.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/anchors_chia_add.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/anchors_markovian.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/anchors_ots.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/anchors_rfc3161.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/beacon.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/canonical.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/checkpoint.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/decision.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/demo.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/dsse.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/emit.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/errors.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/experimental/__init__.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/experimental/enclave.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/hf_evals.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/intoto.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/kbjwt.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/merkle.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/persample.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/prereg.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/py.typed +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/sdjwt.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/sdjwt_issue.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/signature.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/statuslist.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle/tlogproof.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle.egg-info/requires.txt +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_adapters.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_adversarial.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchor_target_trustedtime.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_chia.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_chia_add.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_chia_claims.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_generic.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_markovian.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_ots.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_anchors_rfc3161.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_beacon.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_branch_base_check.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_bundle.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_canonical.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_checkpoint.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_cli.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_cli_eval.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_cli_require_anchor.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_conformance.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_cosignature.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_anchors.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_cli.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_evidence.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_fuzz.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_hardening.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_policy.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_schema.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_validator_api.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_decision_verify.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_demo.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_docs_truth.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_dup_key_reject.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_ed25519_semantics.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_eee.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_emit.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_evalclaim.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_examples.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_experimental_enclave.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_fork_pr_secret_isolation.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_fuzz_parsers.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_hf_entry_verify_eee_digest.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_hf_evals.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_inspect_hook.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_claims_hygiene.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_content_root_migration.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_eval_result.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_examples.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_spec_diff.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_intoto_svr.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_kbjwt.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_merkle.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_merkle_property.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_ok_semantics.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_oversized_int_guard.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_persample.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_pinned_key_validation.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_policy_explain_lint.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_predicate_type_enforcement.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_prereg.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_promptfoo.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_provenance.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_rekor_interop.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_release_integrity_gate.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_schema.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_sdjwt_duplicate_cnf.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_sdjwt_verify_binding.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_signature.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_statuslist.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.0}/tests/test_tlogproof.py +0 -0
- {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
|
|
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.
|
|
59
|
-
<img alt="b7n0de, Verified AI Work" src="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-logo.
|
|
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.
|
|
5
|
-
<img alt="b7n0de, Verified AI Work" src="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-logo.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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}
|
|
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,
|
|
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)
|
|
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=
|
|
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=
|
|
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,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
|
+
}
|