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.
- {proofbundle-3.0.0/src/proofbundle.egg-info → proofbundle-3.1.0}/PKG-INFO +86 -44
- {proofbundle-3.0.0 → proofbundle-3.1.0}/README.md +85 -43
- {proofbundle-3.0.0 → proofbundle-3.1.0}/pyproject.toml +2 -2
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/__init__.py +1 -1
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/eee.py +17 -4
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/bundle.py +86 -4
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/cli.py +68 -9
- {proofbundle-3.0.0 → 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.0 → proofbundle-3.1.0}/src/proofbundle/policy.py +66 -3
- proofbundle-3.1.0/src/proofbundle/policy_profiles.py +86 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0/src/proofbundle.egg-info}/PKG-INFO +86 -44
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle.egg-info/SOURCES.txt +10 -0
- {proofbundle-3.0.0 → 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.0.0 → proofbundle-3.1.0}/tests/test_hf_entry_verify_eee_digest.py +8 -0
- proofbundle-3.1.0/tests/test_policy_profiles.py +199 -0
- proofbundle-3.1.0/tests/test_release_integrity_gate.py +88 -0
- proofbundle-3.1.0/tests/test_root_authenticity.py +244 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_trust_policy.py +16 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/LICENSE +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/setup.cfg +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/_integration.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/_strict_json.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/__init__.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/_provenance.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/promptfoo.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/adapters/samples.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/anchors.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/anchors_chia.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/anchors_chia_add.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/anchors_markovian.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/anchors_ots.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/anchors_rfc3161.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/beacon.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/canonical.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/checkpoint.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/decision.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/demo.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/dsse.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/emit.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/errors.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/experimental/__init__.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/experimental/enclave.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/hf_evals.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/intoto.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/kbjwt.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/merkle.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/persample.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/prereg.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/py.typed +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/sdjwt.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/sdjwt_issue.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/signature.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/statuslist.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle/tlogproof.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle.egg-info/requires.txt +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_adapters.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_adversarial.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchor_target_trustedtime.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_chia.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_chia_add.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_chia_claims.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_generic.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_markovian.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_ots.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_anchors_rfc3161.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_beacon.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_branch_base_check.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_bundle.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_canonical.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_checkpoint.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_cli.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_cli_eval.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_cli_require_anchor.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_conformance.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_cosignature.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_anchors.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_cli.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_evidence.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_fuzz.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_hardening.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_policy.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_schema.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_validator_api.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_decision_verify.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_demo.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_docs_truth.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_dup_key_reject.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_ed25519_semantics.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_eee.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_emit.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_evalclaim.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_examples.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_experimental_enclave.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_fork_pr_secret_isolation.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_fuzz_parsers.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_hf_evals.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_inspect_hook.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_claims_hygiene.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_content_root_migration.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_eval_result.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_examples.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_spec_diff.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_intoto_svr.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_kbjwt.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_merkle.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_merkle_property.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_ok_semantics.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_oversized_int_guard.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_persample.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_pinned_key_validation.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_policy_explain_lint.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_predicate_type_enforcement.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_prereg.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_promptfoo.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_provenance.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_rekor_interop.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_schema.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_sdjwt_duplicate_cnf.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_sdjwt_verify_binding.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_signature.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_statuslist.py +0 -0
- {proofbundle-3.0.0 → proofbundle-3.1.0}/tests/test_tlogproof.py +0 -0
- {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.
|
|
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>
|
|
@@ -75,12 +75,29 @@ Merkle, one file, no server, no network.
|
|
|
75
75
|
[](https://doi.org/10.5281/zenodo.21230467)
|
|
76
76
|
[](https://github.com/astral-sh/ruff)
|
|
77
77
|
[](https://github.com/b7n0de/proofbundle/blob/main/scripts/mutation_check.py)
|
|
78
|
-
|
|
78
|
+
[](https://pypi.org/project/proofbundle/#files)
|
|
79
|
+
[](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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
|
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.
|
|
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>
|
|
@@ -21,12 +21,29 @@ Merkle, one file, no server, no network.
|
|
|
21
21
|
[](https://doi.org/10.5281/zenodo.21230467)
|
|
22
22
|
[](https://github.com/astral-sh/ruff)
|
|
23
23
|
[](https://github.com/b7n0de/proofbundle/blob/main/scripts/mutation_check.py)
|
|
24
|
-
|
|
24
|
+
[](https://pypi.org/project/proofbundle/#files)
|
|
25
|
+
[](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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
|
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.
|
|
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.
|
|
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``
|
|
103
|
-
over the result cannot be used as a model-id
|
|
104
|
-
binding every score, timestamp, dataset and metric
|
|
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)
|
|
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
|
-
|
|
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
|
|