proofbundle 3.0.1__tar.gz → 3.1.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-3.0.1/src/proofbundle.egg-info → proofbundle-3.1.1}/PKG-INFO +33 -3
- {proofbundle-3.0.1 → proofbundle-3.1.1}/README.md +32 -2
- {proofbundle-3.0.1 → proofbundle-3.1.1}/pyproject.toml +2 -2
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/__init__.py +1 -1
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/bundle.py +198 -4
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/cli.py +225 -12
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/evalclaim.py +64 -0
- proofbundle-3.1.1/src/proofbundle/policies/decision-receipt-template-v1.json +24 -0
- proofbundle-3.1.1/src/proofbundle/policies/research-preview-v1.json +7 -0
- proofbundle-3.1.1/src/proofbundle/policies/strict-eval-authenticated-root-template-v1.json +25 -0
- proofbundle-3.1.1/src/proofbundle/policies/strict-eval-template-v1.json +24 -0
- proofbundle-3.1.1/src/proofbundle/policies/strict-prereg-template-v1.json +25 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/policy.py +150 -7
- proofbundle-3.1.1/src/proofbundle/policy_profiles.py +236 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1/src/proofbundle.egg-info}/PKG-INFO +33 -3
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle.egg-info/SOURCES.txt +10 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_claims_hygiene.py +86 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_policy.py +73 -0
- proofbundle-3.1.1/tests/test_eval_evidence_class.py +113 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_kbjwt.py +29 -8
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_ok_semantics.py +13 -3
- proofbundle-3.1.1/tests/test_policy_profiles.py +211 -0
- proofbundle-3.1.1/tests/test_policy_templates.py +185 -0
- proofbundle-3.1.1/tests/test_root_authenticity.py +679 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_sdjwt_verify_binding.py +94 -1
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_trust_policy.py +30 -4
- {proofbundle-3.0.1 → proofbundle-3.1.1}/LICENSE +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/setup.cfg +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/_integration.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/_strict_json.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/__init__.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/_provenance.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/eee.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/promptfoo.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/adapters/samples.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/anchors.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/anchors_chia.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/anchors_chia_add.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/anchors_markovian.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/anchors_ots.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/anchors_rfc3161.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/beacon.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/canonical.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/checkpoint.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/decision.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/demo.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/dsse.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/emit.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/errors.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/experimental/__init__.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/experimental/enclave.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/hf_evals.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/intoto.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/kbjwt.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/merkle.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/persample.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/prereg.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/py.typed +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/sdjwt.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/sdjwt_issue.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/signature.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/statuslist.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle/tlogproof.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle.egg-info/requires.txt +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_adapters.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_adversarial.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchor_target_trustedtime.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_chia.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_chia_add.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_chia_claims.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_generic.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_markovian.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_ots.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_anchors_rfc3161.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_beacon.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_branch_base_check.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_bundle.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_canonical.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_checkpoint.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_cli.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_cli_eval.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_cli_require_anchor.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_conformance.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_cosignature.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_anchors.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_cli.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_evidence.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_fuzz.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_hardening.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_schema.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_validator_api.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_decision_verify.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_demo.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_docs_truth.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_dup_key_reject.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_ed25519_semantics.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_eee.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_emit.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_evalclaim.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_examples.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_experimental_enclave.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_fork_pr_secret_isolation.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_fuzz_parsers.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_hf_entry_verify_eee_digest.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_hf_evals.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_inspect_hook.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_claims_hygiene.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_content_root_migration.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_eval_result.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_examples.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_spec_diff.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_intoto_svr.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_merkle.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_merkle_property.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_oversized_int_guard.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_persample.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_pinned_key_validation.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_policy_explain_lint.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_predicate_type_enforcement.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_prereg.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_promptfoo.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_provenance.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_rekor_interop.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_release_integrity_gate.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_schema.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_sdjwt_duplicate_cnf.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_signature.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_statuslist.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.1}/tests/test_tlogproof.py +0 -0
- {proofbundle-3.0.1 → proofbundle-3.1.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.
|
|
3
|
+
Version: 3.1.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
|
|
@@ -55,8 +55,8 @@ Dynamic: license-file
|
|
|
55
55
|
<div align="center">
|
|
56
56
|
|
|
57
57
|
<picture>
|
|
58
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-logo-dark.
|
|
59
|
-
<img alt="b7n0de, Verified AI Work" src="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-logo.
|
|
58
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-hase-logo-dark.png">
|
|
59
|
+
<img alt="b7n0de, Verified AI Work, pink rabbit mascot over the B7N0DE wordmark" src="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-hase-logo.png" width="200">
|
|
60
60
|
</picture>
|
|
61
61
|
|
|
62
62
|
<h1>proofbundle</h1>
|
|
@@ -89,6 +89,7 @@ Merkle, one file, no server, no network.
|
|
|
89
89
|
- [The problem](#the-problem)
|
|
90
90
|
- [What a receipt proves, and what it doesn't](#what-a-receipt-proves-and-what-it-doesnt)
|
|
91
91
|
- [How it fits together](#how-it-fits-together)
|
|
92
|
+
- [Cite this work](#cite-this-work)
|
|
92
93
|
- [Post-quantum posture](#post-quantum-posture-honest-two-layers)
|
|
93
94
|
- [What's in the box](#whats-in-the-box)
|
|
94
95
|
- [Docs](#docs)
|
|
@@ -122,6 +123,13 @@ proofbundle verify receipt.json # exit 0 = crypto OK, 1 = crypto/verifica
|
|
|
122
123
|
|
|
123
124
|
# apply YOUR trust decision — verify makes NO trust decision on its own:
|
|
124
125
|
proofbundle verify receipt.json --policy trust_policy.json # POLICY: OK | FAIL (exit 3) | NOT_EVALUATED
|
|
126
|
+
|
|
127
|
+
# start from a shipped TEMPLATE and pin your own signer — offline, no network:
|
|
128
|
+
proofbundle policy instantiate strict-eval-template-v1 \
|
|
129
|
+
--issuer-key org-eval.pub --policy-id org/strict-eval-v1 --output org.json
|
|
130
|
+
proofbundle verify receipt.json --json --policy org.json --expected-root <b64>
|
|
131
|
+
# → root_authenticity.safeForAutomation: true only when the policy pins a trusted signer AND the
|
|
132
|
+
# root is authenticated (else false, with automationBlockers naming every reason)
|
|
125
133
|
```
|
|
126
134
|
|
|
127
135
|
## Inspect-native? (METR Task Standard, UK-AISI ecosystem)
|
|
@@ -183,6 +191,28 @@ flowchart LR
|
|
|
183
191
|
style OK fill:#D6248A,stroke:#D6248A,color:#fff
|
|
184
192
|
```
|
|
185
193
|
|
|
194
|
+
### Where it sits in the research neighbourhood
|
|
195
|
+
|
|
196
|
+
proofbundle is a **practical, released, offline verifier — complementary to TEE and zero-knowledge
|
|
197
|
+
approaches**, not a replacement for any of them. It is honest about the line each neighbour crosses
|
|
198
|
+
that a receipt does not. Maturity labels are stated so nothing reads as a settled standard when it is
|
|
199
|
+
a preprint.
|
|
200
|
+
|
|
201
|
+
| Neighbour | What it contributes that a receipt does not | Maturity | Where proofbundle draws the line |
|
|
202
|
+
|---|---|---|---|
|
|
203
|
+
| **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 |
|
|
204
|
+
| **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 |
|
|
205
|
+
| **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 |
|
|
206
|
+
| **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 |
|
|
207
|
+
| 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 |
|
|
208
|
+
|
|
209
|
+
## Cite this work
|
|
210
|
+
|
|
211
|
+
If proofbundle helped your evaluation pipeline, please cite it. Machine-readable metadata is in
|
|
212
|
+
[`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff); the archival record and
|
|
213
|
+
concept DOI are on Zenodo ([10.5281/zenodo.21230467](https://doi.org/10.5281/zenodo.21230467)), and the
|
|
214
|
+
current design write-up is the Technical Note linked from [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
|
|
215
|
+
|
|
186
216
|
## Post-quantum posture (honest, two layers)
|
|
187
217
|
|
|
188
218
|
proofbundle is **not** "quantum-proof" or "quantum-safe" as a whole. It combines two cryptographic layers
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
3
|
<picture>
|
|
4
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-logo-dark.
|
|
5
|
-
<img alt="b7n0de, Verified AI Work" src="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-logo.
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-hase-logo-dark.png">
|
|
5
|
+
<img alt="b7n0de, Verified AI Work, pink rabbit mascot over the B7N0DE wordmark" src="https://raw.githubusercontent.com/b7n0de/proofbundle/main/assets/b7n0de-hase-logo.png" width="200">
|
|
6
6
|
</picture>
|
|
7
7
|
|
|
8
8
|
<h1>proofbundle</h1>
|
|
@@ -35,6 +35,7 @@ Merkle, one file, no server, no network.
|
|
|
35
35
|
- [The problem](#the-problem)
|
|
36
36
|
- [What a receipt proves, and what it doesn't](#what-a-receipt-proves-and-what-it-doesnt)
|
|
37
37
|
- [How it fits together](#how-it-fits-together)
|
|
38
|
+
- [Cite this work](#cite-this-work)
|
|
38
39
|
- [Post-quantum posture](#post-quantum-posture-honest-two-layers)
|
|
39
40
|
- [What's in the box](#whats-in-the-box)
|
|
40
41
|
- [Docs](#docs)
|
|
@@ -68,6 +69,13 @@ proofbundle verify receipt.json # exit 0 = crypto OK, 1 = crypto/verifica
|
|
|
68
69
|
|
|
69
70
|
# apply YOUR trust decision — verify makes NO trust decision on its own:
|
|
70
71
|
proofbundle verify receipt.json --policy trust_policy.json # POLICY: OK | FAIL (exit 3) | NOT_EVALUATED
|
|
72
|
+
|
|
73
|
+
# start from a shipped TEMPLATE and pin your own signer — offline, no network:
|
|
74
|
+
proofbundle policy instantiate strict-eval-template-v1 \
|
|
75
|
+
--issuer-key org-eval.pub --policy-id org/strict-eval-v1 --output org.json
|
|
76
|
+
proofbundle verify receipt.json --json --policy org.json --expected-root <b64>
|
|
77
|
+
# → root_authenticity.safeForAutomation: true only when the policy pins a trusted signer AND the
|
|
78
|
+
# root is authenticated (else false, with automationBlockers naming every reason)
|
|
71
79
|
```
|
|
72
80
|
|
|
73
81
|
## Inspect-native? (METR Task Standard, UK-AISI ecosystem)
|
|
@@ -129,6 +137,28 @@ flowchart LR
|
|
|
129
137
|
style OK fill:#D6248A,stroke:#D6248A,color:#fff
|
|
130
138
|
```
|
|
131
139
|
|
|
140
|
+
### Where it sits in the research neighbourhood
|
|
141
|
+
|
|
142
|
+
proofbundle is a **practical, released, offline verifier — complementary to TEE and zero-knowledge
|
|
143
|
+
approaches**, not a replacement for any of them. It is honest about the line each neighbour crosses
|
|
144
|
+
that a receipt does not. Maturity labels are stated so nothing reads as a settled standard when it is
|
|
145
|
+
a preprint.
|
|
146
|
+
|
|
147
|
+
| Neighbour | What it contributes that a receipt does not | Maturity | Where proofbundle draws the line |
|
|
148
|
+
|---|---|---|---|
|
|
149
|
+
| **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 |
|
|
150
|
+
| **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 |
|
|
151
|
+
| **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 |
|
|
152
|
+
| **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 |
|
|
153
|
+
| 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 |
|
|
154
|
+
|
|
155
|
+
## Cite this work
|
|
156
|
+
|
|
157
|
+
If proofbundle helped your evaluation pipeline, please cite it. Machine-readable metadata is in
|
|
158
|
+
[`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff); the archival record and
|
|
159
|
+
concept DOI are on Zenodo ([10.5281/zenodo.21230467](https://doi.org/10.5281/zenodo.21230467)), and the
|
|
160
|
+
current design write-up is the Technical Note linked from [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
|
|
161
|
+
|
|
132
162
|
## Post-quantum posture (honest, two layers)
|
|
133
163
|
|
|
134
164
|
proofbundle is **not** "quantum-proof" or "quantum-safe" as a whole. It combines two cryptographic layers
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proofbundle"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.1.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"
|
|
@@ -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.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
|
|
@@ -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,28 @@ 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", "AUTOMATION_BLOCKER_REASONS"]
|
|
37
|
+
|
|
38
|
+
# AP-1 §5.3: the human-legible reason for each automationBlockers enum value. Kept HERE, next to the
|
|
39
|
+
# blocker logic in root_authenticity_summary, so the human `SAFE_FOR_AUTOMATION` line and the JSON flag
|
|
40
|
+
# can never drift apart (one source of truth — Iteration F). A blocker with no entry falls back to its
|
|
41
|
+
# raw enum string, so a future blocker is never silently unexplained.
|
|
42
|
+
AUTOMATION_BLOCKER_REASONS = {
|
|
43
|
+
"CRYPTO_FAILED": "Cryptographic verification did not pass",
|
|
44
|
+
"ROOT_NOT_AUTHENTICATED": "The Merkle root was not authenticated against a relying-party value "
|
|
45
|
+
"(--expected-root or a policy trusted_roots entry)",
|
|
46
|
+
"POLICY_NOT_EVALUATED": "No trust policy was evaluated (supply --policy to authorise a signer)",
|
|
47
|
+
"POLICY_FAILED": "The supplied trust policy was not satisfied",
|
|
48
|
+
"SIGNER_NOT_PINNED": "The trust policy pins no trusted signer identity (attributes to nobody)",
|
|
49
|
+
"TEMPLATE_NOT_INSTANTIATED": "The trust policy is a raw template (requiresIdentityOverlay:true) — "
|
|
50
|
+
"instantiate it with a signer identity before depending on it for automation",
|
|
51
|
+
"POLICY_EXPIRED": "The trust policy has expired (its valid_until is in the past)",
|
|
52
|
+
"POLICY_WARNINGS_PRESENT": "The trust policy carries a warning that blocks automation",
|
|
53
|
+
"ANCHOR_REQUIRED_FAILED": "A required external time anchor did not verify",
|
|
54
|
+
"PUBLIC_TRANSPARENCY_REQUIRED_FAILED": "A required public-transparency proof did not verify",
|
|
55
|
+
"REPLAY_BINDING_REQUIRED_FAILED": "A required replay/audience binding did not verify",
|
|
56
|
+
}
|
|
35
57
|
|
|
36
58
|
|
|
37
59
|
def _issuer_requires_holder_binding(sd_part: str) -> bool:
|
|
@@ -68,6 +90,27 @@ _MERKLE_KEYS = {"hash_alg", "leaf_index", "tree_size", "inclusion_proof_b64", "r
|
|
|
68
90
|
_SD_KEYS = {"compact", "issuer_public_key_b64"}
|
|
69
91
|
|
|
70
92
|
|
|
93
|
+
def _sd_jwt_carries_eval_root_commitment(sd_payload) -> bool:
|
|
94
|
+
"""N1 (audit 2026-07-13; discriminator hardened after pre-land L1 review): True iff an SD-JWT issuer
|
|
95
|
+
payload carries the eval-binding ROOT COMMITMENT (``receipt.root_b64``, a non-empty base64 string)
|
|
96
|
+
that ``check_binds_bundle`` binds against — i.e. it CLAIMS to be anchored to a proofbundle merkle
|
|
97
|
+
root. That commitment is the exact cross-receipt substitution vector, so it (NOT a heuristic
|
|
98
|
+
word-match on generic keys like ``passed``/``suite``/``threshold``) is what marks an eval SD-JWT
|
|
99
|
+
grafted onto a non-eval payload. `issue_sd_jwt` always writes ``receipt.root_b64`` always-open, so a
|
|
100
|
+
genuine eval SD-JWT is caught even if its ``passed``/``threshold`` facts are moved into selective
|
|
101
|
+
disclosures; a generic SD-JWT-VC (``iss``/``vct``, no receipt commitment) is not an eval graft and
|
|
102
|
+
stays in scope (backward-compatible). Residual (documented, out of scope): a SELF-SIGNED credential
|
|
103
|
+
that hides even ``receipt.root_b64`` in a disclosure asserts no always-open anchoring claim and cannot
|
|
104
|
+
forge a trusted-issuer graft (the genuine emitter never does this)."""
|
|
105
|
+
if not isinstance(sd_payload, dict):
|
|
106
|
+
return False
|
|
107
|
+
receipt = sd_payload.get("receipt")
|
|
108
|
+
# Fire on the PRESENCE of a receipt.root_b64 string, INCLUDING "" (L1 pre-land audit F3): an empty root
|
|
109
|
+
# commits nothing concrete, but "an eval-shaped commitment present yet evading N1" should not exist. A
|
|
110
|
+
# generic SD-JWT-VC has no receipt object at all, so this never false-refuses one.
|
|
111
|
+
return isinstance(receipt, dict) and isinstance(receipt.get("root_b64"), str)
|
|
112
|
+
|
|
113
|
+
|
|
71
114
|
def _b64d(value: str, field: str) -> bytes:
|
|
72
115
|
try:
|
|
73
116
|
return base64.b64decode(value, validate=True)
|
|
@@ -134,7 +177,9 @@ def load_bundle(path: str) -> dict:
|
|
|
134
177
|
return loads_strict(handle.read())
|
|
135
178
|
|
|
136
179
|
|
|
137
|
-
def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce=None
|
|
180
|
+
def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce=None,
|
|
181
|
+
expected_root_b64: Optional[str] = None,
|
|
182
|
+
expected_tree_size: Optional[int] = None) -> VerificationResult:
|
|
138
183
|
"""Verify an evidence bundle (a dict or a path to a JSON file).
|
|
139
184
|
|
|
140
185
|
``expected_aud`` / ``expected_nonce`` (v1.3): when the bundle carries a Key Binding JWT, these enforce
|
|
@@ -142,6 +187,17 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
|
|
|
142
187
|
``nonce`` MUST match ``expected_nonce``. If omitted, the KB-JWT signature + disclosure binding are still
|
|
143
188
|
checked, but the relying party has NOT bound the presentation to itself/this transaction — a stale or
|
|
144
189
|
cross-audience replay would still verify. A relying party doing challenge-response MUST pass both.
|
|
190
|
+
|
|
191
|
+
``expected_root_b64`` / ``expected_tree_size`` (P0-A, Hardening 3.0.1 §6.2): RELYING-PARTY root
|
|
192
|
+
authentication. The native Merkle root is NOT part of the signature input (SPEC §5), so the SAME
|
|
193
|
+
signed payload verifies under DIFFERENT roots — a *coherent one-leaf rewrap* re-anchors the payload
|
|
194
|
+
at index 0 of a 2-leaf tree with a foreign sibling, and inclusion still holds. Merkle inclusion alone
|
|
195
|
+
therefore proves CONSISTENCY under the stated root, NOT that the root is authentic. When the relying
|
|
196
|
+
party supplies an authenticated root / tree size (out of band: a pinned value, a signed checkpoint,
|
|
197
|
+
the trusted_roots of a policy), these are enforced bit-exactly and a mismatch FAILS (adds the
|
|
198
|
+
``root-authenticity`` / ``tree-size`` checks). ``expected_root_b64`` is decoded and compared to the
|
|
199
|
+
stated root's BYTES (canonicalization-agnostic). Absent, root authenticity stays NOT_EVALUATED and
|
|
200
|
+
the crypto verdict is unchanged (backward-compatible) — see ``root_authenticity_summary``.
|
|
145
201
|
"""
|
|
146
202
|
if isinstance(bundle, str):
|
|
147
203
|
bundle = load_bundle(bundle)
|
|
@@ -198,9 +254,28 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
|
|
|
198
254
|
result.add(
|
|
199
255
|
"merkle-inclusion",
|
|
200
256
|
incl_ok,
|
|
201
|
-
f"anchored at index {leaf_index} of {tree_size}
|
|
257
|
+
f"anchored at index {leaf_index} of {tree_size} (Merkle-consistent under the STATED root)"
|
|
258
|
+
if incl_ok else "inclusion proof failed",
|
|
202
259
|
)
|
|
203
260
|
|
|
261
|
+
# 2b. P0-A (§6.2): relying-party root authentication. The stated root is NOT signed, so inclusion
|
|
262
|
+
# alone does not authenticate it; only a bit-exact match against a root/size the relying party
|
|
263
|
+
# obtained out of band does. Adds a check ONLY when the RP supplies an expectation — absent, root
|
|
264
|
+
# authenticity is NOT_EVALUATED and the verdict is unchanged (backward-compatible).
|
|
265
|
+
if expected_root_b64 is not None:
|
|
266
|
+
exp_root = _b64d(expected_root_b64, "expected_root_b64")
|
|
267
|
+
root_ok = hmac.compare_digest(root, exp_root)
|
|
268
|
+
result.add("root-authenticity", root_ok,
|
|
269
|
+
"stated root matches the expected authenticated root" if root_ok
|
|
270
|
+
else "stated root does NOT match the expected root — possible root/rewrap substitution")
|
|
271
|
+
if expected_tree_size is not None:
|
|
272
|
+
# strict: a real int only — reject bool (1==True) and float (1==1.0), matching _require_int.
|
|
273
|
+
size_ok = (isinstance(expected_tree_size, int) and not isinstance(expected_tree_size, bool)
|
|
274
|
+
and tree_size == expected_tree_size)
|
|
275
|
+
result.add("tree-size", size_ok,
|
|
276
|
+
f"tree_size {tree_size} matches the expected size" if size_ok
|
|
277
|
+
else f"tree_size {tree_size} != expected {expected_tree_size} — possible tree-size substitution")
|
|
278
|
+
|
|
204
279
|
# 3. optional SD-JWT selective disclosure credential
|
|
205
280
|
sd = bundle.get("sd_jwt_vc")
|
|
206
281
|
kb_binding_checked = False # F4: did a KB-JWT (the aud/nonce carrier) actually get verified?
|
|
@@ -316,6 +391,25 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
|
|
|
316
391
|
"sd_jwt_vc disclosures do NOT bind this bundle (reason: unbound/mismatch — cross-receipt "
|
|
317
392
|
"substitution; the SD-JWT's passed/threshold/comparator/suite/issuer/root differ from the "
|
|
318
393
|
"signed claim)")
|
|
394
|
+
elif _sd_jwt_carries_eval_root_commitment(_sd_p):
|
|
395
|
+
# N1 (audit 2026-07-13, L1 live PoC; discriminator hardened after pre-land L1 review): an
|
|
396
|
+
# EVAL SD-JWT (issue_sd_jwt writes the always-open root commitment receipt.root_b64) MUST
|
|
397
|
+
# bind to a proofbundle/eval-claim/v0.1 payload — check_binds_bundle above is the only
|
|
398
|
+
# binding for it. Grafted onto a NON-eval payload it has nothing to bind against, so
|
|
399
|
+
# previously the exact same issuer-valid eval SD-JWT verified CRYPTO: OK on ANY
|
|
400
|
+
# opaque-payload bundle (cross-receipt substitution, sd_jwt_ok stayed true, zero operator
|
|
401
|
+
# signal). Fail-closed (Reifegradpolitik §0.6: never a silent PASS for insecure legacy
|
|
402
|
+
# behaviour). Keying on receipt.root_b64 (the real substitution vector) rather than a
|
|
403
|
+
# word-match on generic keys catches a graft even when passed/threshold are moved into
|
|
404
|
+
# disclosures, and never false-refuses a GENERIC SD-JWT-VC (iss/vct, no receipt commitment,
|
|
405
|
+
# e.g. examples/example_bundle.json), which carries no eval anchoring claim and is out of
|
|
406
|
+
# scope (backward-compatible).
|
|
407
|
+
result.add(
|
|
408
|
+
"sd-jwt-bundle-binding", False,
|
|
409
|
+
"sd_jwt_vc carries an eval-claim root commitment (receipt.root_b64) but the signed "
|
|
410
|
+
"payload is not a proofbundle/eval-claim/v0.1 claim, so that anchoring claim cannot be "
|
|
411
|
+
"bound to this bundle (reason: unbindable eval SD-JWT — a valid issuer signature does not "
|
|
412
|
+
"make an unbound eval anchoring claim belong to this receipt; refused fail-closed)")
|
|
319
413
|
|
|
320
414
|
# F4 (v1.9.2, fail-closed): supplying expected_aud/expected_nonce asks for RFC 9901 §7.3
|
|
321
415
|
# replay/audience binding. A bundle with no verifiable KB-JWT (no sd_jwt_vc at all, or an
|
|
@@ -332,6 +426,106 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
|
|
|
332
426
|
return result
|
|
333
427
|
|
|
334
428
|
|
|
429
|
+
def root_authenticity_summary(result: VerificationResult, *,
|
|
430
|
+
policy_authenticated_root: Optional[bool] = None,
|
|
431
|
+
policy_ok: Optional[bool] = None,
|
|
432
|
+
anchor_ok: Optional[bool] = None,
|
|
433
|
+
signer_trusted: Optional[bool] = None,
|
|
434
|
+
policy_warnings: Optional[list] = None,
|
|
435
|
+
policy_expired: Optional[bool] = None,
|
|
436
|
+
requires_identity_overlay: Optional[bool] = None,
|
|
437
|
+
public_transparency_ok: Optional[bool] = None,
|
|
438
|
+
replay_ok: Optional[bool] = None) -> dict:
|
|
439
|
+
"""Structured root-authenticity verdicts (P0-A §6.3), derived from a completed VerificationResult.
|
|
440
|
+
|
|
441
|
+
Separates what Merkle inclusion actually proves from what it does NOT, as three-state strings so a
|
|
442
|
+
consumer never mistakes 'not checked' for 'passed':
|
|
443
|
+
|
|
444
|
+
payloadSignature PASS/FAIL — the payload is signed by the stated key
|
|
445
|
+
merkleConsistency PASS/FAIL — the payload is Merkle-consistent under the STATED root
|
|
446
|
+
rootAuthenticity PASS/FAIL/NOT_EVALUATED — was the stated root authenticated against a
|
|
447
|
+
relying-party value (``expected_root``, or a policy's ``trusted_roots``)?
|
|
448
|
+
publicTransparency NOT_EVALUATED — a public-log receipt is the separate §10 profile
|
|
449
|
+
safeForAutomation bool — True ONLY if the whole crypto verdict passed, the root was
|
|
450
|
+
affirmatively authenticated, AND a supplied trust policy
|
|
451
|
+
PASSED with a real signer pin (P0-B, audit 2026-07-13)
|
|
452
|
+
automationBlockers list[str] — every reason safeForAutomation is false, so a consumer
|
|
453
|
+
keying off the flag sees exactly WHY (fail-closed)
|
|
454
|
+
|
|
455
|
+
``policy_authenticated_root`` folds the policy layer's root verdict in when no explicit
|
|
456
|
+
``root-authenticity`` check ran (e.g. the root matched a policy ``trusted_roots`` entry).
|
|
457
|
+
``policy_ok`` / ``anchor_ok`` are the relying-party gate verdicts (True/False/None=not-evaluated).
|
|
458
|
+
P0-B: ``safeForAutomation`` is a GLOBAL trust verdict, so ``policy_ok`` must be True (a policy that
|
|
459
|
+
passed) — ``None`` (no policy evaluated) can never make it true. ``policy_warnings`` (the vacuous
|
|
460
|
+
'attributes to nobody' lint) forces it false too: a policy that pins no signer authorises no
|
|
461
|
+
identity, so a crypto-valid, root-pinned receipt under it is NOT automation-safe.
|
|
462
|
+
"""
|
|
463
|
+
by = {c.name: c.ok for c in result.checks}
|
|
464
|
+
|
|
465
|
+
def _tri(name: str) -> str:
|
|
466
|
+
return "PASS" if by.get(name) else ("FAIL" if name in by else "NOT_EVALUATED")
|
|
467
|
+
|
|
468
|
+
if "root-authenticity" in by:
|
|
469
|
+
root_auth = "PASS" if by["root-authenticity"] else "FAIL"
|
|
470
|
+
elif policy_authenticated_root is True:
|
|
471
|
+
root_auth = "PASS"
|
|
472
|
+
elif policy_authenticated_root is False:
|
|
473
|
+
root_auth = "FAIL"
|
|
474
|
+
else:
|
|
475
|
+
root_auth = "NOT_EVALUATED"
|
|
476
|
+
# P0-B (audit 2026-07-13): the former `policy_ok is not False` let policy_ok=None (no policy
|
|
477
|
+
# evaluated) through → a crypto-valid, root-pinned receipt looked automation-safe though NO trusted
|
|
478
|
+
# signer was ever authorised. safeForAutomation is now a GLOBAL trust verdict: policy_ok must be True
|
|
479
|
+
# AND carry no vacuous 'attributes to nobody' warning. Every failed condition is surfaced in
|
|
480
|
+
# automationBlockers (fail-closed, so the flag is never a silent yes).
|
|
481
|
+
# P0-B / AP-1 §5 (audit 2026-07-13): safeForAutomation is a GLOBAL trust verdict. Variant A (strict):
|
|
482
|
+
# true ONLY when crypto passed, the root was authenticated, a supplied policy PASSED (policy_ok is
|
|
483
|
+
# True — None/no-policy never qualifies), that policy actually PINS a trusted signer (signer_trusted),
|
|
484
|
+
# and no required anchor / public-transparency / replay gate FAILED. automationBlockers enumerates
|
|
485
|
+
# every reason it is false (fail-closed, never a silent yes). NOTE for 3.1.1: public_transparency_ok
|
|
486
|
+
# and replay_ok are relying-party gates that are None (not-requested) on the current core — the §10
|
|
487
|
+
# public-transparency policy section is 3.2.0, and replay (aud/nonce) already fails the crypto verdict
|
|
488
|
+
# (CRYPTO_FAILED) when a required KB-JWT is absent — so these two blockers are defined for forward
|
|
489
|
+
# compatibility and stay dormant unless a future policy layer supplies a False verdict.
|
|
490
|
+
blockers: list[str] = []
|
|
491
|
+
if not bool(result.ok):
|
|
492
|
+
blockers.append("CRYPTO_FAILED")
|
|
493
|
+
if root_auth != "PASS":
|
|
494
|
+
blockers.append("ROOT_NOT_AUTHENTICATED")
|
|
495
|
+
if policy_ok is None:
|
|
496
|
+
blockers.append("POLICY_NOT_EVALUATED")
|
|
497
|
+
elif policy_ok is False:
|
|
498
|
+
blockers.append("POLICY_FAILED")
|
|
499
|
+
elif requires_identity_overlay:
|
|
500
|
+
# AP-2 §6.2 (L2 pre-land audit): a RAW template (requiresIdentityOverlay:true) is never automation-safe
|
|
501
|
+
# — reported as its OWN blocker, not SIGNER_NOT_PINNED, which would be factually wrong when the template
|
|
502
|
+
# actually does match a signer (the real reason is the un-cleared template-lifecycle flag).
|
|
503
|
+
blockers.append("TEMPLATE_NOT_INSTANTIATED")
|
|
504
|
+
elif signer_trusted is not True:
|
|
505
|
+
blockers.append("SIGNER_NOT_PINNED") # policy passed but pins no trusted identity (attributes to nobody)
|
|
506
|
+
elif policy_warnings:
|
|
507
|
+
blockers.append("POLICY_WARNINGS_PRESENT") # signer pinned yet the policy still warns (forward-compat)
|
|
508
|
+
# AP-2 §6.4 lifecycle: an EXPIRED policy is unsafe to automate on even if it otherwise passed (a stale
|
|
509
|
+
# signer pin the relying party has since rotated away from). Independent of the signer/warning chain so
|
|
510
|
+
# it is reported alongside, never in place of, another reason.
|
|
511
|
+
if policy_expired is True:
|
|
512
|
+
blockers.append("POLICY_EXPIRED")
|
|
513
|
+
if anchor_ok is False:
|
|
514
|
+
blockers.append("ANCHOR_REQUIRED_FAILED")
|
|
515
|
+
if public_transparency_ok is False:
|
|
516
|
+
blockers.append("PUBLIC_TRANSPARENCY_REQUIRED_FAILED")
|
|
517
|
+
if replay_ok is False:
|
|
518
|
+
blockers.append("REPLAY_BINDING_REQUIRED_FAILED")
|
|
519
|
+
return {
|
|
520
|
+
"payloadSignature": _tri("ed25519-signature"),
|
|
521
|
+
"merkleConsistency": _tri("merkle-inclusion"),
|
|
522
|
+
"rootAuthenticity": root_auth,
|
|
523
|
+
"publicTransparency": "NOT_EVALUATED",
|
|
524
|
+
"safeForAutomation": not blockers,
|
|
525
|
+
"automationBlockers": blockers,
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
|
|
335
529
|
def recompute_merkle_root_b64(bundle: Union[dict, str]) -> dict:
|
|
336
530
|
"""Recompute the Merkle root from the bundle's own payload + inclusion proof (v1.2, issue #2).
|
|
337
531
|
|