proofbundle 2.1.0__tar.gz → 3.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {proofbundle-2.1.0/src/proofbundle.egg-info → proofbundle-3.0.1}/PKG-INFO +88 -46
- {proofbundle-2.1.0 → proofbundle-3.0.1}/README.md +87 -45
- {proofbundle-2.1.0 → proofbundle-3.0.1}/pyproject.toml +4 -1
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/__init__.py +2 -2
- proofbundle-3.0.1/src/proofbundle/_strict_json.py +72 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/adapters/eee.py +75 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/anchors.py +66 -9
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/anchors_chia.py +2 -2
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/anchors_markovian.py +29 -12
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/anchors_ots.py +44 -20
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/anchors_rfc3161.py +58 -20
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/bundle.py +91 -8
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/checkpoint.py +2 -2
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/cli.py +262 -41
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/decision.py +101 -14
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/evalclaim.py +21 -12
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/experimental/enclave.py +7 -3
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/hf_evals.py +91 -7
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/intoto.py +69 -29
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/kbjwt.py +12 -3
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/merkle.py +9 -1
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/persample.py +8 -1
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/policy.py +233 -7
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/sdjwt.py +19 -3
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/sdjwt_issue.py +25 -12
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/statuslist.py +10 -3
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/tlogproof.py +5 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1/src/proofbundle.egg-info}/PKG-INFO +88 -46
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle.egg-info/SOURCES.txt +14 -0
- proofbundle-3.0.1/tests/test_anchor_target_trustedtime.py +209 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_anchors_markovian.py +25 -12
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_anchors_ots.py +27 -17
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_anchors_rfc3161.py +57 -33
- proofbundle-3.0.1/tests/test_claims_hygiene.py +194 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_cli_require_anchor.py +60 -0
- proofbundle-3.0.1/tests/test_conformance.py +167 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_decision_anchors.py +40 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_decision_policy.py +2 -1
- proofbundle-3.0.1/tests/test_decision_validator_api.py +82 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_decision_verify.py +68 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_docs_truth.py +50 -6
- proofbundle-3.0.1/tests/test_dup_key_reject.py +319 -0
- proofbundle-3.0.1/tests/test_ed25519_semantics.py +99 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_evalclaim.py +17 -0
- proofbundle-3.0.1/tests/test_hf_entry_verify_eee_digest.py +195 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_kbjwt.py +11 -6
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_ok_semantics.py +43 -8
- proofbundle-3.0.1/tests/test_oversized_int_guard.py +41 -0
- proofbundle-3.0.1/tests/test_pinned_key_validation.py +76 -0
- proofbundle-3.0.1/tests/test_policy_explain_lint.py +192 -0
- proofbundle-3.0.1/tests/test_predicate_type_enforcement.py +102 -0
- proofbundle-3.0.1/tests/test_release_integrity_gate.py +88 -0
- proofbundle-3.0.1/tests/test_sdjwt_duplicate_cnf.py +181 -0
- proofbundle-3.0.1/tests/test_sdjwt_verify_binding.py +168 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_trust_policy.py +7 -4
- proofbundle-2.1.0/tests/test_claims_hygiene.py +0 -48
- {proofbundle-2.1.0 → proofbundle-3.0.1}/LICENSE +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/setup.cfg +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/_integration.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/adapters/__init__.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/adapters/_provenance.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/adapters/promptfoo.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/adapters/samples.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/anchors_chia_add.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/beacon.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/canonical.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/demo.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/dsse.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/emit.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/errors.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/experimental/__init__.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/prereg.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/py.typed +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle/signature.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle.egg-info/requires.txt +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_adapters.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_adversarial.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_anchors_chia.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_anchors_chia_add.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_anchors_chia_claims.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_anchors_generic.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_beacon.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_branch_base_check.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_bundle.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_canonical.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_checkpoint.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_cli.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_cli_eval.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_cosignature.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_decision_cli.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_decision_evidence.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_decision_fuzz.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_decision_hardening.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_decision_schema.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_demo.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_eee.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_emit.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_examples.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_experimental_enclave.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_fork_pr_secret_isolation.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_fuzz_parsers.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_hf_evals.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_inspect_hook.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_intoto.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_intoto_claims_hygiene.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_intoto_content_root_migration.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_intoto_eval_result.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_intoto_examples.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_intoto_spec_diff.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_intoto_svr.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_merkle.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_merkle_property.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_persample.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_prereg.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_promptfoo.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_provenance.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_rekor_interop.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_schema.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_signature.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_statuslist.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_tlogproof.py +0 -0
- {proofbundle-2.1.0 → proofbundle-3.0.1}/tests/test_verify_matrix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofbundle
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.1
|
|
4
4
|
Summary: Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT.
|
|
5
5
|
Author: Konrad Gruszka
|
|
6
6
|
License: MIT
|
|
@@ -75,12 +75,28 @@ 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
|
+
- [Post-quantum posture](#post-quantum-posture-honest-two-layers)
|
|
93
|
+
- [What's in the box](#whats-in-the-box)
|
|
94
|
+
- [Docs](#docs)
|
|
95
|
+
- [Install](#install)
|
|
96
|
+
- [Status, scope and roadmap](#status-scope-and-roadmap)
|
|
97
|
+
- [Contributing](#contributing)
|
|
98
|
+
- [License](#license)
|
|
99
|
+
|
|
84
100
|
## 60-second try (offline)
|
|
85
101
|
|
|
86
102
|
```bash
|
|
@@ -91,21 +107,9 @@ git clone https://github.com/b7n0de/proofbundle && cd proofbundle
|
|
|
91
107
|
pip install -e ".[eval,inspect]" && make demo # or `make full-demo` for log -> receipt -> verify
|
|
92
108
|
```
|
|
93
109
|
|
|
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)**.
|
|
110
|
+
The demo runs entirely in memory: an honest receipt verifies `=> OK`, six independent tampers each
|
|
111
|
+
verify `FAILED`, and a swapped sample gets caught. `proofbundle demo` exits non-zero if any tamper
|
|
112
|
+
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
113
|
|
|
110
114
|
```bash
|
|
111
115
|
# verify a real hosted receipt without writing any code:
|
|
@@ -120,7 +124,7 @@ proofbundle verify receipt.json # exit 0 = crypto OK, 1 = crypto/verifica
|
|
|
120
124
|
proofbundle verify receipt.json --policy trust_policy.json # POLICY: OK | FAIL (exit 3) | NOT_EVALUATED
|
|
121
125
|
```
|
|
122
126
|
|
|
123
|
-
## Inspect-native? (METR Task Standard
|
|
127
|
+
## Inspect-native? (METR Task Standard, UK-AISI ecosystem)
|
|
124
128
|
|
|
125
129
|
The receipt layer runs directly on [Inspect AI](https://inspect.aisi.org.uk/) — and the proof is
|
|
126
130
|
reproducible offline in minutes:
|
|
@@ -135,7 +139,22 @@ In your own pipeline the end-of-task hook signs every run automatically. Walkthr
|
|
|
135
139
|
**[docs/INSPECT_HAPPY_PATH.md](https://github.com/b7n0de/proofbundle/blob/main/docs/INSPECT_HAPPY_PATH.md)** · worked example:
|
|
136
140
|
**[examples/inspect_receipt.py](https://github.com/b7n0de/proofbundle/blob/main/examples/inspect_receipt.py)**.
|
|
137
141
|
|
|
138
|
-
##
|
|
142
|
+
## The problem
|
|
143
|
+
|
|
144
|
+
Every AI eval number you read — a safety benchmark, a capability score, a leaderboard entry — is an
|
|
145
|
+
**unverifiable claim**. You trust the lab. There's no portable way to check, offline, that a result
|
|
146
|
+
was signed by a stated party, hasn't been altered, and covers the samples it claims.
|
|
147
|
+
|
|
148
|
+
proofbundle is that check. It's a small MIT-licensed Python tool (a compact, auditable trusted core,
|
|
149
|
+
depends only on [`cryptography`](https://cryptography.io)) that turns a result into a signed
|
|
150
|
+
receipt anyone can verify from a single file — and it's honest about the line it does not cross.
|
|
151
|
+
|
|
152
|
+
In plain language: a proofbundle receipt is the cash-register receipt of an AI test result. It shows
|
|
153
|
+
who claimed the number and that nobody quietly changed it afterwards. It does not show the test was
|
|
154
|
+
good — the way a cash-register receipt does not show the meal was good — but without a receipt there
|
|
155
|
+
is nothing to check at all.
|
|
156
|
+
|
|
157
|
+
## What a receipt proves, and what it doesn't
|
|
139
158
|
|
|
140
159
|
| ✅ It proves | ❌ It does **not** prove |
|
|
141
160
|
|---|---|
|
|
@@ -149,6 +168,21 @@ This boundary is the point, not a weakness. A receipt makes a claim **attributab
|
|
|
149
168
|
and — with pre-registration and per-sample auditing — bounded and spot-checkable**. Full detail:
|
|
150
169
|
**[THREAT_MODEL.md](https://github.com/b7n0de/proofbundle/blob/main/THREAT_MODEL.md)**.
|
|
151
170
|
|
|
171
|
+
## How it fits together
|
|
172
|
+
|
|
173
|
+
*(diagram renders on GitHub — [view it there](https://github.com/b7n0de/proofbundle#how-it-fits-together); PyPI shows the source)*
|
|
174
|
+
|
|
175
|
+
```mermaid
|
|
176
|
+
flowchart LR
|
|
177
|
+
H["eval harness<br/>inspect_ai · lm-eval · promptfoo · pytest"] --> A["adapter → signed claim<br/>salted commitments · provenance · samples root"]
|
|
178
|
+
A --> R["receipt<br/>one portable file"]
|
|
179
|
+
R --> V{{"proofbundle verify — offline"}}
|
|
180
|
+
V --> C["signature · Merkle inclusion · SD-JWT/KB ·<br/>witness quorum · status list · sample openings"]
|
|
181
|
+
C --> OK(["CRYPTO: OK / FAILED"])
|
|
182
|
+
style V fill:#D6248A,stroke:#D6248A,color:#fff
|
|
183
|
+
style OK fill:#D6248A,stroke:#D6248A,color:#fff
|
|
184
|
+
```
|
|
185
|
+
|
|
152
186
|
## Post-quantum posture (honest, two layers)
|
|
153
187
|
|
|
154
188
|
proofbundle is **not** "quantum-proof" or "quantum-safe" as a whole. It combines two cryptographic layers
|
|
@@ -173,28 +207,6 @@ On the witness side, C2SP checkpoints already carry post-quantum **ML-DSA-44** (
|
|
|
173
207
|
(`proofbundle[pq]`); a post-quantum *payload* signature — crypto-agility for the receipt itself — is on the
|
|
174
208
|
roadmap.
|
|
175
209
|
|
|
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
210
|
## What's in the box
|
|
199
211
|
|
|
200
212
|
- **Core** — Ed25519 signature + RFC 6962 / 9162 Merkle inclusion, verified fully offline. Checks a
|
|
@@ -202,7 +214,8 @@ flowchart LR
|
|
|
202
214
|
- **Eval receipts** — a signed claim (`metric ⋈ threshold`, `n`, salted model/dataset commitments,
|
|
203
215
|
assurance level, provenance) from your run. See [EVAL_CLAIM.md](https://github.com/b7n0de/proofbundle/blob/main/EVAL_CLAIM.md).
|
|
204
216
|
- **Selective disclosure** — SD-JWT ([RFC 9901](https://datatracker.ietf.org/doc/rfc9901/)) with Key
|
|
205
|
-
Binding: prove a threshold while withholding the exact score.
|
|
217
|
+
Binding: prove a threshold while withholding the exact score. Secure-by-default in 3.0.0 (breaking): an
|
|
218
|
+
unsigned SD-JWT, or one whose disclosures do not bind this bundle, now fails verification (was warn-only).
|
|
206
219
|
- **Transparency-log interop** — C2SP `tlog-checkpoint` / cosignature / `.tlog-proof`, with
|
|
207
220
|
post-quantum **ML-DSA-44** witness cosignatures. Optional Token-Status-List revocation snapshots.
|
|
208
221
|
- **Per-sample audit** — commit to every sample; an auditor challenges random indices (with a fresh
|
|
@@ -218,12 +231,34 @@ flowchart LR
|
|
|
218
231
|
[docs/INSPECT_HAPPY_PATH.md](https://github.com/b7n0de/proofbundle/blob/main/docs/INSPECT_HAPPY_PATH.md) — run an eval, get a receipt, verify it offline.
|
|
219
232
|
- **External time anchors** *(v2.0 beta, the `[anchors]` extra)* — an optional `anchors[]` layer that
|
|
220
233
|
attaches external evidence of *when* a commitment or receipt existed, from a party the producer does not
|
|
221
|
-
control. Two built-in types verify offline: **RFC 3161** TSA tokens (against a
|
|
222
|
-
**OpenTimestamps** Bitcoin proofs (honest pending → confirmed lifecycle). A `register_anchor_type`
|
|
234
|
+
control. Two built-in types verify offline: **RFC 3161** TSA tokens (against a relying-party-supplied TSA
|
|
235
|
+
root, see the 3.0.0 trust note below) and **OpenTimestamps** Bitcoin proofs (honest pending → confirmed lifecycle). A `register_anchor_type`
|
|
223
236
|
extension interface lets a third party ship its own fail-closed type; two worked examples ship — a
|
|
224
237
|
first-party **`chia-datalayer/v1`** (offline Merkle inclusion of a canonical root under a published Chia
|
|
225
238
|
DataLayer root) and a third-party **`markovian-provenance/v1`** (a wallet-attributable, Bitcoin-anchored
|
|
226
|
-
stamp).
|
|
239
|
+
stamp). **Since 2.1:** a `verify --require-anchor` relying-party gate (optionally narrowed by
|
|
240
|
+
`--anchor-type`) turns "no verifying anchor of that type" into a failure layered over the crypto result
|
|
241
|
+
(exit 3, like `--policy`); a pending anchor does not satisfy it unless `--allow-pending`. Plus RFC 3161
|
|
242
|
+
hardening — the frozen cert chain is validated at the token's own `gen_time`, with optional `policyOid`
|
|
243
|
+
pinning. **Breaking in 3.0.0:** an anchor's TRUST now comes only from the relying party — supply a TSA
|
|
244
|
+
root (`--trusted-tsa-root`) or a Bitcoin block header (`--bitcoin-header`), or the equivalent `anchors`
|
|
245
|
+
policy keys; the bundle's producer-controlled `frozen` block is evidence, never a trust source, so
|
|
246
|
+
`--require-anchor` without relying-party trust material is unmet (exit 3). An anchor stays detached from
|
|
247
|
+
the content root, and the `statement` target is RESERVED for decision receipts.
|
|
248
|
+
See [docs/ANCHORS.md](https://github.com/b7n0de/proofbundle/blob/main/docs/ANCHORS.md).
|
|
249
|
+
- **Universal content root** *(2.1, `jcs-sha256-v1`, [ADR 0002](https://github.com/b7n0de/proofbundle/blob/main/docs/adr/0002-universal-content-root.md))* — one shared primitive now underlies both the
|
|
250
|
+
decision-receipt path and the in-toto eval-result / test-result / SVR exports: SHA-256 over the RFC 8785
|
|
251
|
+
(JCS) canonical bytes of the full pre-signature Statement, so a content root survives counter-signing and
|
|
252
|
+
key rotation. The algorithm is a versioned id signed inside the payload (`contentRootAlg`, default
|
|
253
|
+
`jcs-sha256-v1`); a verifier re-serializes with exactly the declared algorithm, never falls back, and an
|
|
254
|
+
unknown algorithm fails closed. Migration is a compatible evolution, not a cutover: absent `contentRootAlg`
|
|
255
|
+
⇒ the historic `legacy-sortkeys-json-v0` mode, so every already-signed 2.0.0 receipt keeps verifying
|
|
256
|
+
byte-for-byte. This is **not** a completed universal migration — a CLI flag to select the algorithm is still
|
|
257
|
+
deferred. Independent cross-implementation (MarkovianProtocol) interop is now proven for RFC 8785
|
|
258
|
+
canonicalization + content-root binding (see `conformance/decision/crossimpl/`); the same corpus additionally
|
|
259
|
+
verifies a confirmed Bitcoin anchor (block 957504) offline. The external fixture currently reports 12 findings
|
|
260
|
+
against the enforced `decision-receipt/v0.1` validator — recorded as an expected-fail, not hidden — so full
|
|
261
|
+
schema conformance awaits a further schema-conformant regeneration.
|
|
227
262
|
- **Decision Receipts** *(2.1, vendored `decision-receipt/v0.1` predicate)* — a separate predicate for agent
|
|
228
263
|
*decisions* (not eval metrics): who decided, the proposed action, the policy boundary, digest-bound evidence,
|
|
229
264
|
the verdict (`ALLOW`/`DENY`/`REFUSE`/`ESCALATE`/`DEFER`/`OBSERVE`), and explicitly what was *not* checked.
|
|
@@ -262,7 +297,7 @@ pip install "proofbundle[pq]" # verify ML-DSA-44 (post-quantum) witnes
|
|
|
262
297
|
Requires Python 3.10+. The verify path never rolls its own crypto — Ed25519 comes from
|
|
263
298
|
`cryptography`; Merkle hashing is RFC 6962.
|
|
264
299
|
|
|
265
|
-
## Status
|
|
300
|
+
## Status, scope and roadmap
|
|
266
301
|
|
|
267
302
|
Beta, SemVer-committed, with a CI test suite behind a mutation gate + property-based parser fuzzing. Correctness
|
|
268
303
|
is anchored to external RFC 6962 vectors and a real Rekor proof, not just its own bundles. It is
|
|
@@ -270,6 +305,13 @@ is anchored to external RFC 6962 vectors and a real Rekor proof, not just its ow
|
|
|
270
305
|
by itself — it is the small, offline, standards-native receipt layer between them. Security policy:
|
|
271
306
|
[SECURITY.md](https://github.com/b7n0de/proofbundle/blob/main/SECURITY.md).
|
|
272
307
|
|
|
308
|
+
**Roadmap (stated honestly, all forward-looking):**
|
|
309
|
+
|
|
310
|
+
- A post-quantum *payload* signature (crypto-agility for the receipt itself) is on the roadmap; today the
|
|
311
|
+
post-quantum coverage is witness-side ML-DSA-44 cosignatures only.
|
|
312
|
+
- A CLI flag to select the content-root algorithm is still deferred (`jcs-sha256-v1` is the signed default).
|
|
313
|
+
- **Preview:** a TEE-attestation bridge — see [docs/EXPERIMENTAL_ENCLAVE.md](https://github.com/b7n0de/proofbundle/blob/main/docs/EXPERIMENTAL_ENCLAVE.md).
|
|
314
|
+
|
|
273
315
|
## Contributing
|
|
274
316
|
|
|
275
317
|
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
|
|
@@ -21,12 +21,28 @@ 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
|
+
- [Post-quantum posture](#post-quantum-posture-honest-two-layers)
|
|
39
|
+
- [What's in the box](#whats-in-the-box)
|
|
40
|
+
- [Docs](#docs)
|
|
41
|
+
- [Install](#install)
|
|
42
|
+
- [Status, scope and roadmap](#status-scope-and-roadmap)
|
|
43
|
+
- [Contributing](#contributing)
|
|
44
|
+
- [License](#license)
|
|
45
|
+
|
|
30
46
|
## 60-second try (offline)
|
|
31
47
|
|
|
32
48
|
```bash
|
|
@@ -37,21 +53,9 @@ git clone https://github.com/b7n0de/proofbundle && cd proofbundle
|
|
|
37
53
|
pip install -e ".[eval,inspect]" && make demo # or `make full-demo` for log -> receipt -> verify
|
|
38
54
|
```
|
|
39
55
|
|
|
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)**.
|
|
56
|
+
The demo runs entirely in memory: an honest receipt verifies `=> OK`, six independent tampers each
|
|
57
|
+
verify `FAILED`, and a swapped sample gets caught. `proofbundle demo` exits non-zero if any tamper
|
|
58
|
+
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
59
|
|
|
56
60
|
```bash
|
|
57
61
|
# verify a real hosted receipt without writing any code:
|
|
@@ -66,7 +70,7 @@ proofbundle verify receipt.json # exit 0 = crypto OK, 1 = crypto/verifica
|
|
|
66
70
|
proofbundle verify receipt.json --policy trust_policy.json # POLICY: OK | FAIL (exit 3) | NOT_EVALUATED
|
|
67
71
|
```
|
|
68
72
|
|
|
69
|
-
## Inspect-native? (METR Task Standard
|
|
73
|
+
## Inspect-native? (METR Task Standard, UK-AISI ecosystem)
|
|
70
74
|
|
|
71
75
|
The receipt layer runs directly on [Inspect AI](https://inspect.aisi.org.uk/) — and the proof is
|
|
72
76
|
reproducible offline in minutes:
|
|
@@ -81,7 +85,22 @@ In your own pipeline the end-of-task hook signs every run automatically. Walkthr
|
|
|
81
85
|
**[docs/INSPECT_HAPPY_PATH.md](https://github.com/b7n0de/proofbundle/blob/main/docs/INSPECT_HAPPY_PATH.md)** · worked example:
|
|
82
86
|
**[examples/inspect_receipt.py](https://github.com/b7n0de/proofbundle/blob/main/examples/inspect_receipt.py)**.
|
|
83
87
|
|
|
84
|
-
##
|
|
88
|
+
## The problem
|
|
89
|
+
|
|
90
|
+
Every AI eval number you read — a safety benchmark, a capability score, a leaderboard entry — is an
|
|
91
|
+
**unverifiable claim**. You trust the lab. There's no portable way to check, offline, that a result
|
|
92
|
+
was signed by a stated party, hasn't been altered, and covers the samples it claims.
|
|
93
|
+
|
|
94
|
+
proofbundle is that check. It's a small MIT-licensed Python tool (a compact, auditable trusted core,
|
|
95
|
+
depends only on [`cryptography`](https://cryptography.io)) that turns a result into a signed
|
|
96
|
+
receipt anyone can verify from a single file — and it's honest about the line it does not cross.
|
|
97
|
+
|
|
98
|
+
In plain language: a proofbundle receipt is the cash-register receipt of an AI test result. It shows
|
|
99
|
+
who claimed the number and that nobody quietly changed it afterwards. It does not show the test was
|
|
100
|
+
good — the way a cash-register receipt does not show the meal was good — but without a receipt there
|
|
101
|
+
is nothing to check at all.
|
|
102
|
+
|
|
103
|
+
## What a receipt proves, and what it doesn't
|
|
85
104
|
|
|
86
105
|
| ✅ It proves | ❌ It does **not** prove |
|
|
87
106
|
|---|---|
|
|
@@ -95,6 +114,21 @@ This boundary is the point, not a weakness. A receipt makes a claim **attributab
|
|
|
95
114
|
and — with pre-registration and per-sample auditing — bounded and spot-checkable**. Full detail:
|
|
96
115
|
**[THREAT_MODEL.md](https://github.com/b7n0de/proofbundle/blob/main/THREAT_MODEL.md)**.
|
|
97
116
|
|
|
117
|
+
## How it fits together
|
|
118
|
+
|
|
119
|
+
*(diagram renders on GitHub — [view it there](https://github.com/b7n0de/proofbundle#how-it-fits-together); PyPI shows the source)*
|
|
120
|
+
|
|
121
|
+
```mermaid
|
|
122
|
+
flowchart LR
|
|
123
|
+
H["eval harness<br/>inspect_ai · lm-eval · promptfoo · pytest"] --> A["adapter → signed claim<br/>salted commitments · provenance · samples root"]
|
|
124
|
+
A --> R["receipt<br/>one portable file"]
|
|
125
|
+
R --> V{{"proofbundle verify — offline"}}
|
|
126
|
+
V --> C["signature · Merkle inclusion · SD-JWT/KB ·<br/>witness quorum · status list · sample openings"]
|
|
127
|
+
C --> OK(["CRYPTO: OK / FAILED"])
|
|
128
|
+
style V fill:#D6248A,stroke:#D6248A,color:#fff
|
|
129
|
+
style OK fill:#D6248A,stroke:#D6248A,color:#fff
|
|
130
|
+
```
|
|
131
|
+
|
|
98
132
|
## Post-quantum posture (honest, two layers)
|
|
99
133
|
|
|
100
134
|
proofbundle is **not** "quantum-proof" or "quantum-safe" as a whole. It combines two cryptographic layers
|
|
@@ -119,28 +153,6 @@ On the witness side, C2SP checkpoints already carry post-quantum **ML-DSA-44** (
|
|
|
119
153
|
(`proofbundle[pq]`); a post-quantum *payload* signature — crypto-agility for the receipt itself — is on the
|
|
120
154
|
roadmap.
|
|
121
155
|
|
|
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
156
|
## What's in the box
|
|
145
157
|
|
|
146
158
|
- **Core** — Ed25519 signature + RFC 6962 / 9162 Merkle inclusion, verified fully offline. Checks a
|
|
@@ -148,7 +160,8 @@ flowchart LR
|
|
|
148
160
|
- **Eval receipts** — a signed claim (`metric ⋈ threshold`, `n`, salted model/dataset commitments,
|
|
149
161
|
assurance level, provenance) from your run. See [EVAL_CLAIM.md](https://github.com/b7n0de/proofbundle/blob/main/EVAL_CLAIM.md).
|
|
150
162
|
- **Selective disclosure** — SD-JWT ([RFC 9901](https://datatracker.ietf.org/doc/rfc9901/)) with Key
|
|
151
|
-
Binding: prove a threshold while withholding the exact score.
|
|
163
|
+
Binding: prove a threshold while withholding the exact score. Secure-by-default in 3.0.0 (breaking): an
|
|
164
|
+
unsigned SD-JWT, or one whose disclosures do not bind this bundle, now fails verification (was warn-only).
|
|
152
165
|
- **Transparency-log interop** — C2SP `tlog-checkpoint` / cosignature / `.tlog-proof`, with
|
|
153
166
|
post-quantum **ML-DSA-44** witness cosignatures. Optional Token-Status-List revocation snapshots.
|
|
154
167
|
- **Per-sample audit** — commit to every sample; an auditor challenges random indices (with a fresh
|
|
@@ -164,12 +177,34 @@ flowchart LR
|
|
|
164
177
|
[docs/INSPECT_HAPPY_PATH.md](https://github.com/b7n0de/proofbundle/blob/main/docs/INSPECT_HAPPY_PATH.md) — run an eval, get a receipt, verify it offline.
|
|
165
178
|
- **External time anchors** *(v2.0 beta, the `[anchors]` extra)* — an optional `anchors[]` layer that
|
|
166
179
|
attaches external evidence of *when* a commitment or receipt existed, from a party the producer does not
|
|
167
|
-
control. Two built-in types verify offline: **RFC 3161** TSA tokens (against a
|
|
168
|
-
**OpenTimestamps** Bitcoin proofs (honest pending → confirmed lifecycle). A `register_anchor_type`
|
|
180
|
+
control. Two built-in types verify offline: **RFC 3161** TSA tokens (against a relying-party-supplied TSA
|
|
181
|
+
root, see the 3.0.0 trust note below) and **OpenTimestamps** Bitcoin proofs (honest pending → confirmed lifecycle). A `register_anchor_type`
|
|
169
182
|
extension interface lets a third party ship its own fail-closed type; two worked examples ship — a
|
|
170
183
|
first-party **`chia-datalayer/v1`** (offline Merkle inclusion of a canonical root under a published Chia
|
|
171
184
|
DataLayer root) and a third-party **`markovian-provenance/v1`** (a wallet-attributable, Bitcoin-anchored
|
|
172
|
-
stamp).
|
|
185
|
+
stamp). **Since 2.1:** a `verify --require-anchor` relying-party gate (optionally narrowed by
|
|
186
|
+
`--anchor-type`) turns "no verifying anchor of that type" into a failure layered over the crypto result
|
|
187
|
+
(exit 3, like `--policy`); a pending anchor does not satisfy it unless `--allow-pending`. Plus RFC 3161
|
|
188
|
+
hardening — the frozen cert chain is validated at the token's own `gen_time`, with optional `policyOid`
|
|
189
|
+
pinning. **Breaking in 3.0.0:** an anchor's TRUST now comes only from the relying party — supply a TSA
|
|
190
|
+
root (`--trusted-tsa-root`) or a Bitcoin block header (`--bitcoin-header`), or the equivalent `anchors`
|
|
191
|
+
policy keys; the bundle's producer-controlled `frozen` block is evidence, never a trust source, so
|
|
192
|
+
`--require-anchor` without relying-party trust material is unmet (exit 3). An anchor stays detached from
|
|
193
|
+
the content root, and the `statement` target is RESERVED for decision receipts.
|
|
194
|
+
See [docs/ANCHORS.md](https://github.com/b7n0de/proofbundle/blob/main/docs/ANCHORS.md).
|
|
195
|
+
- **Universal content root** *(2.1, `jcs-sha256-v1`, [ADR 0002](https://github.com/b7n0de/proofbundle/blob/main/docs/adr/0002-universal-content-root.md))* — one shared primitive now underlies both the
|
|
196
|
+
decision-receipt path and the in-toto eval-result / test-result / SVR exports: SHA-256 over the RFC 8785
|
|
197
|
+
(JCS) canonical bytes of the full pre-signature Statement, so a content root survives counter-signing and
|
|
198
|
+
key rotation. The algorithm is a versioned id signed inside the payload (`contentRootAlg`, default
|
|
199
|
+
`jcs-sha256-v1`); a verifier re-serializes with exactly the declared algorithm, never falls back, and an
|
|
200
|
+
unknown algorithm fails closed. Migration is a compatible evolution, not a cutover: absent `contentRootAlg`
|
|
201
|
+
⇒ the historic `legacy-sortkeys-json-v0` mode, so every already-signed 2.0.0 receipt keeps verifying
|
|
202
|
+
byte-for-byte. This is **not** a completed universal migration — a CLI flag to select the algorithm is still
|
|
203
|
+
deferred. Independent cross-implementation (MarkovianProtocol) interop is now proven for RFC 8785
|
|
204
|
+
canonicalization + content-root binding (see `conformance/decision/crossimpl/`); the same corpus additionally
|
|
205
|
+
verifies a confirmed Bitcoin anchor (block 957504) offline. The external fixture currently reports 12 findings
|
|
206
|
+
against the enforced `decision-receipt/v0.1` validator — recorded as an expected-fail, not hidden — so full
|
|
207
|
+
schema conformance awaits a further schema-conformant regeneration.
|
|
173
208
|
- **Decision Receipts** *(2.1, vendored `decision-receipt/v0.1` predicate)* — a separate predicate for agent
|
|
174
209
|
*decisions* (not eval metrics): who decided, the proposed action, the policy boundary, digest-bound evidence,
|
|
175
210
|
the verdict (`ALLOW`/`DENY`/`REFUSE`/`ESCALATE`/`DEFER`/`OBSERVE`), and explicitly what was *not* checked.
|
|
@@ -208,7 +243,7 @@ pip install "proofbundle[pq]" # verify ML-DSA-44 (post-quantum) witnes
|
|
|
208
243
|
Requires Python 3.10+. The verify path never rolls its own crypto — Ed25519 comes from
|
|
209
244
|
`cryptography`; Merkle hashing is RFC 6962.
|
|
210
245
|
|
|
211
|
-
## Status
|
|
246
|
+
## Status, scope and roadmap
|
|
212
247
|
|
|
213
248
|
Beta, SemVer-committed, with a CI test suite behind a mutation gate + property-based parser fuzzing. Correctness
|
|
214
249
|
is anchored to external RFC 6962 vectors and a real Rekor proof, not just its own bundles. It is
|
|
@@ -216,6 +251,13 @@ is anchored to external RFC 6962 vectors and a real Rekor proof, not just its ow
|
|
|
216
251
|
by itself — it is the small, offline, standards-native receipt layer between them. Security policy:
|
|
217
252
|
[SECURITY.md](https://github.com/b7n0de/proofbundle/blob/main/SECURITY.md).
|
|
218
253
|
|
|
254
|
+
**Roadmap (stated honestly, all forward-looking):**
|
|
255
|
+
|
|
256
|
+
- A post-quantum *payload* signature (crypto-agility for the receipt itself) is on the roadmap; today the
|
|
257
|
+
post-quantum coverage is witness-side ML-DSA-44 cosignatures only.
|
|
258
|
+
- A CLI flag to select the content-root algorithm is still deferred (`jcs-sha256-v1` is the signed default).
|
|
259
|
+
- **Preview:** a TEE-attestation bridge — see [docs/EXPERIMENTAL_ENCLAVE.md](https://github.com/b7n0de/proofbundle/blob/main/docs/EXPERIMENTAL_ENCLAVE.md).
|
|
260
|
+
|
|
219
261
|
## Contributing
|
|
220
262
|
|
|
221
263
|
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 = "
|
|
7
|
+
version = "3.0.1"
|
|
8
8
|
description = "Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -22,6 +22,9 @@ keywords = [
|
|
|
22
22
|
"rfc6962",
|
|
23
23
|
]
|
|
24
24
|
classifiers = [
|
|
25
|
+
# Stays Beta until the funded external security review passes (Owner decision E1, 2026-07-12):
|
|
26
|
+
# "stable is evidenced, not asserted". The move to "5 - Production/Stable" is a separate,
|
|
27
|
+
# audit-gated milestone (see docs/GRANT_MILESTONES.md), never claimed pre-audit.
|
|
25
28
|
"Development Status :: 4 - Beta",
|
|
26
29
|
"Intended Audience :: Developers",
|
|
27
30
|
"License :: OSI Approved :: MIT License",
|
|
@@ -13,12 +13,12 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
15
|
|
|
16
|
-
__version__ = "
|
|
16
|
+
__version__ = "3.0.1"
|
|
17
17
|
|
|
18
18
|
# The `proofbundle/v0.1` normative spec revision this build implements — kept in sync with the
|
|
19
19
|
# `Revision:` line at the top of SPEC.md by tests/test_docs_truth.py (WP-B1, closes #28). Bump
|
|
20
20
|
# both together whenever SPEC.md's normative text changes (not on every package release).
|
|
21
|
-
SPEC_REVISION = "2026-07-
|
|
21
|
+
SPEC_REVISION = "2026-07-11"
|
|
22
22
|
|
|
23
23
|
__all__ = [
|
|
24
24
|
"__version__",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Duplicate-key-rejecting JSON parsing — the one strict parser for every verify path (WP-C1).
|
|
2
|
+
|
|
3
|
+
``json.loads`` silently keeps the LAST occurrence of a duplicated key (last-wins). On a verify
|
|
4
|
+
path that is a classic parser differential (Bishop Fox 2021; the exact bug class behind several
|
|
5
|
+
JWT/JOSE CVEs): two implementations parsing the same bytes can disagree about which ``root_b64``
|
|
6
|
+
or ``sig_b64`` they verified, so "cross-verifier consensus" silently stops meaning one thing.
|
|
7
|
+
RFC 8785 (JCS) forbids duplicate keys outright, and the DSSE statement paths already reject them
|
|
8
|
+
INDIRECTLY (a duplicated payload cannot be byte-equal to its own canonical re-serialization) —
|
|
9
|
+
but the native bundle path accepted them silently, and an explicit reject with a clear message
|
|
10
|
+
beats an incidental byte-mismatch everywhere.
|
|
11
|
+
|
|
12
|
+
Converted paths: the native bundle (``load_bundle``, the HF ``pb1.`` token), the DSSE statement
|
|
13
|
+
verifiers (eval-result / test-result / SVR / decision), the trust-policy loader, the per-sample
|
|
14
|
+
disclosure record, the chia-datalayer and markovian anchor envelopes, the status-list token, the
|
|
15
|
+
enclave EAT, and every ``json.load`` in the CLI. Emit-side inputs (a claim/predicate file the
|
|
16
|
+
caller authored) use it too — a duplicate key in something about to be signed is at best an
|
|
17
|
+
authoring bug, at worst an attempted differential.
|
|
18
|
+
|
|
19
|
+
Resolved 2026-07-12 (F12, release-audit): the SD-JWT/KB-JWT payload sites in ``sdjwt.py`` /
|
|
20
|
+
``kbjwt.py``, ``bundle._issuer_requires_holder_binding``, ``sdjwt_issue._jwt_payload`` and
|
|
21
|
+
``evalclaim.sd_jwt_hidden_count`` now parse with ``loads_strict`` too, each routed fail-closed —
|
|
22
|
+
a duplicate ``cnf`` is rejected, and ``_issuer_requires_holder_binding`` returns True on a duplicate
|
|
23
|
+
(binding REQUIRED), never the inverted "no holder binding required". Keys that differ only by Unicode normalization
|
|
24
|
+
(NFC/NFD) or a BOM are DISTINCT JSON keys by spec and stay distinct here — normalization games are
|
|
25
|
+
a downstream concern of the field validators, not of the parser.
|
|
26
|
+
|
|
27
|
+
Stdlib-only (``object_pairs_hook``), so the base install keeps rejecting duplicates without any
|
|
28
|
+
extra. The hook fires for every nested object (including objects inside arrays), so duplicates are
|
|
29
|
+
rejected at ANY depth.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
from typing import Any, Union
|
|
35
|
+
|
|
36
|
+
from .errors import BundleFormatError
|
|
37
|
+
|
|
38
|
+
__all__ = ["loads_strict"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _reject_duplicate_keys(pairs: list) -> dict:
|
|
42
|
+
obj: dict = {}
|
|
43
|
+
for key, value in pairs:
|
|
44
|
+
if key in obj:
|
|
45
|
+
raise BundleFormatError(
|
|
46
|
+
f"duplicate JSON key {key!r} — rejected fail-closed (a duplicated key parses "
|
|
47
|
+
"differently across JSON implementations; parser-differential guard, WP-C1)")
|
|
48
|
+
obj[key] = value
|
|
49
|
+
return obj
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def loads_strict(text: Union[str, bytes]) -> Any:
|
|
53
|
+
"""``json.loads`` that rejects duplicate object keys at any nesting depth.
|
|
54
|
+
|
|
55
|
+
Raises :class:`BundleFormatError` for a duplicate key (fail-closed, clear message), maps
|
|
56
|
+
``RecursionError`` from pathologically deep nesting to the same documented malformed-input
|
|
57
|
+
error, and maps the ``int``/``str`` conversion-limit ``ValueError`` from a JSON integer literal
|
|
58
|
+
with more than ``sys.get_int_max_str_digits()`` digits (CWE-674 / CVE-2020-10735) to it too —
|
|
59
|
+
never a raw traceback — mirroring :func:`proofbundle.bundle.load_bundle`. Ordinary JSON syntax
|
|
60
|
+
errors keep raising ``ValueError`` (``json.JSONDecodeError``) so existing ``except (ValueError,
|
|
61
|
+
...)`` handling at the call sites stays correct."""
|
|
62
|
+
try:
|
|
63
|
+
return json.loads(text, object_pairs_hook=_reject_duplicate_keys)
|
|
64
|
+
except RecursionError as exc:
|
|
65
|
+
raise BundleFormatError("JSON nesting is too deep") from exc
|
|
66
|
+
except ValueError as exc:
|
|
67
|
+
# The int<->str conversion cap raises a plain ValueError DURING parsing (not a JSONDecodeError),
|
|
68
|
+
# which a pre-auth caller without a broad `except ValueError` would surface as a raw traceback.
|
|
69
|
+
# Map only that specific case; a normal JSONDecodeError keeps raising ValueError as documented.
|
|
70
|
+
if "integer string conversion" in str(exc):
|
|
71
|
+
raise BundleFormatError("JSON integer literal is implausibly long (fail-closed)") from exc
|
|
72
|
+
raise
|
|
@@ -22,10 +22,12 @@ has_unknown_level == true means Unknown and is rejected (not silently mapped to
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
24
|
import json
|
|
25
|
+
import re
|
|
25
26
|
from pathlib import Path
|
|
26
27
|
from typing import Optional, Union
|
|
27
28
|
|
|
28
29
|
from ..evalclaim import build_eval_claim
|
|
30
|
+
from ._provenance import add_provenance
|
|
29
31
|
|
|
30
32
|
_SCHEMA_PATH = Path(__file__).resolve().parent.parent / "eee_eval_schema.json"
|
|
31
33
|
_SCHEMA_VERSION = "0.2.2"
|
|
@@ -95,6 +97,65 @@ def _extract_score(score_details: dict, metric_config: dict) -> str:
|
|
|
95
97
|
return _num_to_decimal_str(raw)
|
|
96
98
|
|
|
97
99
|
|
|
100
|
+
def _model_id_stripped(record: dict) -> dict:
|
|
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 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)."""
|
|
113
|
+
import copy # noqa: PLC0415
|
|
114
|
+
r = copy.deepcopy(record)
|
|
115
|
+
if isinstance(r.get("model_info"), dict):
|
|
116
|
+
r["model_info"].pop("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)
|
|
123
|
+
return r
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _record_digest(record: dict) -> str:
|
|
127
|
+
"""``"<alg>:<hex>"`` over the canonical EEE record JSON with the cleartext model identity removed
|
|
128
|
+
(WP-I3; six-lens review: an UNSALTED digest over a record embedding ``model_info.id`` is a model
|
|
129
|
+
confirmation/enumeration oracle, so it is stripped first — the digest still binds scores,
|
|
130
|
+
timestamps, dataset and metrics for tamper-evidence). JCS when the ``[eval]`` extra is present,
|
|
131
|
+
else a labeled deterministic sort_keys fallback (the label tells a verifier which normalization
|
|
132
|
+
produced the hex, never a silent difference)."""
|
|
133
|
+
import hashlib # noqa: PLC0415
|
|
134
|
+
stripped = _model_id_stripped(record)
|
|
135
|
+
try:
|
|
136
|
+
import rfc8785 # noqa: PLC0415
|
|
137
|
+
return "sha256-jcs:" + hashlib.sha256(rfc8785.dumps(stripped)).hexdigest()
|
|
138
|
+
except (ImportError, ValueError, TypeError):
|
|
139
|
+
canonical = json.dumps(stripped, sort_keys=True, separators=(",", ":"),
|
|
140
|
+
ensure_ascii=False).encode("utf-8")
|
|
141
|
+
return "sha256-sortkeys:" + hashlib.sha256(canonical).hexdigest()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _leaks_model_id(text: str, model_id: str) -> bool:
|
|
145
|
+
"""True iff ``text`` contains the model id or any of its name components (WP-I3 privacy, six-lens
|
|
146
|
+
review). Case-insensitive; checks the full ``org/name`` id, the bare name after the last '/',
|
|
147
|
+
and slug variants where '-', '_' and '.' are unified — so 'arc/gpt2/run1' is caught for a model
|
|
148
|
+
id whose bare name is 'gpt2'."""
|
|
149
|
+
hay = text.lower()
|
|
150
|
+
hay_norm = re.sub(r"[-_.]+", "", hay)
|
|
151
|
+
tokens = {model_id.lower()}
|
|
152
|
+
bare = model_id.split("/")[-1].lower()
|
|
153
|
+
if bare:
|
|
154
|
+
tokens.add(bare)
|
|
155
|
+
tokens.add(re.sub(r"[-_.]+", "", bare)) # slug-normalized ('gpt-2' -> 'gpt2')
|
|
156
|
+
return any(t and (t in hay or t in hay_norm) for t in tokens)
|
|
157
|
+
|
|
158
|
+
|
|
98
159
|
def from_eee_dataset(source: Union[str, Path, dict], *, comparator: str, threshold: str,
|
|
99
160
|
timestamp: Optional[str] = None, eval_index: int = 0, metric_name: Optional[str] = None,
|
|
100
161
|
model_salt: Optional[bytes] = None, dataset_salt: Optional[bytes] = None,
|
|
@@ -149,6 +210,20 @@ def from_eee_dataset(source: Union[str, Path, dict], *, comparator: str, thresho
|
|
|
149
210
|
raise EEEAdapterError("no timestamp: pass timestamp= or set retrieved_timestamp/evaluation_timestamp")
|
|
150
211
|
|
|
151
212
|
provenance = {"source": "every_eval_ever", "eee_schema_version": record.get("schema_version") or _SCHEMA_VERSION}
|
|
213
|
+
# WP-I3: bind the receipt to the EXACT source record — the only adapter without a provenance
|
|
214
|
+
# binding. sha256 over the RFC-8785 (JCS) canonical record JSON with the cleartext model identity
|
|
215
|
+
# stripped first (see _record_digest — the digest binds scores/timestamps/dataset for
|
|
216
|
+
# tamper-evidence but is not a model-id oracle). Same "<alg>:<hex>" label scheme as
|
|
217
|
+
# adapters/_provenance.config_hash, but note it uses NO domain-separation tag (the label
|
|
218
|
+
# distinguishes it); it is provenance metadata, never a security commitment.
|
|
219
|
+
provenance["eee_record_sha256"] = _record_digest(record)
|
|
220
|
+
# the RESULT-level id is traceability metadata; the TOP-level evaluation_id embeds the model id
|
|
221
|
+
# in cleartext and stays deliberately excluded. Guard the result id: drop it if ANY model-name
|
|
222
|
+
# component appears (six-lens review: the exact full-repo-id substring test missed the bare
|
|
223
|
+
# name and slug variants — 'arc/gpt2/run1' leaked 'gpt2'). Case-insensitive over a token set.
|
|
224
|
+
_rid = chosen.get("evaluation_result_id")
|
|
225
|
+
if isinstance(_rid, str) and _rid and not _leaks_model_id(_rid, str(model_id)):
|
|
226
|
+
add_provenance(provenance, run_id=_rid)
|
|
152
227
|
if eval_library.get("name"):
|
|
153
228
|
provenance["harness"] = str(eval_library["name"])
|
|
154
229
|
if eval_library.get("version"):
|