proofbundle 3.1.3__tar.gz → 3.2.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.1.3/src/proofbundle.egg-info → proofbundle-3.2.1}/PKG-INFO +5 -1
- {proofbundle-3.1.3 → proofbundle-3.2.1}/README.md +4 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/pyproject.toml +1 -1
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/__init__.py +42 -1
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/anchors_chia_add.py +51 -18
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/cli.py +160 -0
- proofbundle-3.2.1/src/proofbundle/evidence_pack.py +98 -0
- proofbundle-3.2.1/src/proofbundle/hashalg.py +177 -0
- proofbundle-3.2.1/src/proofbundle/outcome.py +358 -0
- proofbundle-3.2.1/src/proofbundle/pqsig.py +114 -0
- proofbundle-3.2.1/src/proofbundle/public_transparency.py +201 -0
- proofbundle-3.2.1/src/proofbundle/renewal.py +538 -0
- proofbundle-3.2.1/src/proofbundle/run_ledger.py +295 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/sdjwt.py +25 -2
- proofbundle-3.2.1/src/proofbundle/sdjwt_vc.py +182 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/signature.py +2 -0
- proofbundle-3.2.1/src/proofbundle/subject_binding.py +115 -0
- proofbundle-3.2.1/src/proofbundle/trust_pack.py +460 -0
- proofbundle-3.2.1/src/proofbundle/verification_summary.py +252 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1/src/proofbundle.egg-info}/PKG-INFO +5 -1
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle.egg-info/SOURCES.txt +36 -0
- proofbundle-3.2.1/tests/test_anchor_hardening_321.py +187 -0
- proofbundle-3.2.1/tests/test_anchor_longevity_property.py +169 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_anchors_chia_add.py +32 -0
- proofbundle-3.2.1/tests/test_checkpoint_external_vectors.py +163 -0
- proofbundle-3.2.1/tests/test_checkpoint_quorum_property.py +85 -0
- proofbundle-3.2.1/tests/test_content_root_property.py +132 -0
- proofbundle-3.2.1/tests/test_dsse_adversarial.py +102 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_evalclaim.py +15 -0
- proofbundle-3.2.1/tests/test_evidence_pack.py +147 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_fuzz_parsers.py +77 -0
- proofbundle-3.2.1/tests/test_hashalg.py +145 -0
- proofbundle-3.2.1/tests/test_merkle_consistency_property.py +83 -0
- proofbundle-3.2.1/tests/test_mldsa_acvp_vectors.py +150 -0
- proofbundle-3.2.1/tests/test_official_vectors.py +94 -0
- proofbundle-3.2.1/tests/test_ots_external_vectors.py +183 -0
- proofbundle-3.2.1/tests/test_outcome_verify.py +281 -0
- proofbundle-3.2.1/tests/test_pqsig.py +126 -0
- proofbundle-3.2.1/tests/test_public_transparency.py +193 -0
- proofbundle-3.2.1/tests/test_renewal.py +179 -0
- proofbundle-3.2.1/tests/test_renewal_policy.py +100 -0
- proofbundle-3.2.1/tests/test_renewal_signed.py +214 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_rfc6962_external_vectors.py +20 -5
- proofbundle-3.2.1/tests/test_run_ledger.py +152 -0
- proofbundle-3.2.1/tests/test_sdjwt_adversarial.py +134 -0
- proofbundle-3.2.1/tests/test_sdjwt_vc.py +222 -0
- proofbundle-3.2.1/tests/test_sdjwtvc_external_vectors.py +162 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_statuslist.py +46 -0
- proofbundle-3.2.1/tests/test_subject_binding.py +95 -0
- proofbundle-3.2.1/tests/test_trust_pack.py +466 -0
- proofbundle-3.2.1/tests/test_verification_summary.py +167 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/LICENSE +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/setup.cfg +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/_integration.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/_strict_json.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/adapters/__init__.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/adapters/_provenance.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/adapters/eee.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/adapters/promptfoo.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/adapters/samples.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/anchors.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/anchors_chia.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/anchors_markovian.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/anchors_ots.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/anchors_rfc3161.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/beacon.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/bundle.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/canonical.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/checkpoint.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/decision.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/demo.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/dsse.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/emit.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/errors.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/evalclaim.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/experimental/__init__.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/experimental/enclave.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/hf_evals.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/intoto.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/kbjwt.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/merkle.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/persample.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/policies/decision-receipt-template-v1.json +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/policies/research-preview-v1.json +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/policies/strict-eval-authenticated-root-template-v1.json +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/policies/strict-eval-template-v1.json +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/policies/strict-prereg-template-v1.json +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/policy.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/policy_profiles.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/prereg.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/py.typed +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/sdjwt_issue.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/statuslist.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle/tlogproof.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle.egg-info/requires.txt +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_adapters.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_adversarial.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_anchor_target_trustedtime.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_anchors_chia.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_anchors_chia_claims.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_anchors_generic.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_anchors_markovian.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_anchors_ots.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_anchors_rfc3161.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_beacon.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_branch_base_check.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_bundle.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_canonical.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_checkpoint.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_claims_hygiene.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_cli.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_cli_eval.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_cli_require_anchor.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_conformance.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_cosignature.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_decision_anchors.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_decision_cli.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_decision_evidence.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_decision_fuzz.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_decision_hardening.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_decision_policy.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_decision_schema.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_decision_validator_api.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_decision_verify.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_demo.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_docs_truth.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_dup_key_reject.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_ed25519_semantics.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_eee.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_emit.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_eval_evidence_class.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_examples.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_experimental_enclave.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_fork_pr_secret_isolation.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_hf_entry_verify_eee_digest.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_hf_evals.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_inspect_hook.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_intoto.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_intoto_claims_hygiene.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_intoto_content_root_migration.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_intoto_eval_result.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_intoto_examples.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_intoto_spec_diff.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_intoto_svr.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_kbjwt.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_lens_review_fixes_3_1_3.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_merkle.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_merkle_property.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_ok_semantics.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_oversized_int_guard.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_persample.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_pinned_key_validation.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_policy_explain_lint.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_policy_lifecycle_purpose.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_policy_profiles.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_policy_templates.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_predicate_type_enforcement.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_prereg.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_promptfoo.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_provenance.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_rekor_interop.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_release_integrity_gate.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_root_authenticity.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_schema.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_sdjwt_duplicate_cnf.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_sdjwt_verify_binding.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_signature.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_tlogproof.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_tree_context_authenticity.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.1}/tests/test_trust_policy.py +0 -0
- {proofbundle-3.1.3 → proofbundle-3.2.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.1
|
|
3
|
+
Version: 3.2.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
|
|
@@ -300,6 +300,10 @@ roadmap.
|
|
|
300
300
|
a signed outcome is self-assertion. See
|
|
301
301
|
[docs/predicates/decision-receipt.md](https://github.com/b7n0de/proofbundle/blob/main/docs/predicates/decision-receipt.md)
|
|
302
302
|
and [ADR 0001](https://github.com/b7n0de/proofbundle/blob/main/docs/adr/0001-decision-receipt-separate-predicate.md).
|
|
303
|
+
That signed outcome is the `action-outcome/v0.1` predicate (EXPERIMENTAL, 3.2.0): an executor signs what was
|
|
304
|
+
actually done, bound by content root to the Decision Receipt, with role separation (executor ≠ decision maker)
|
|
305
|
+
and `execution_proven` only when an effect digest backs an `executed` status. See
|
|
306
|
+
[docs/predicates/action-outcome.md](https://github.com/b7n0de/proofbundle/blob/main/docs/predicates/action-outcome.md).
|
|
303
307
|
|
|
304
308
|
## Docs
|
|
305
309
|
|
|
@@ -246,6 +246,10 @@ roadmap.
|
|
|
246
246
|
a signed outcome is self-assertion. See
|
|
247
247
|
[docs/predicates/decision-receipt.md](https://github.com/b7n0de/proofbundle/blob/main/docs/predicates/decision-receipt.md)
|
|
248
248
|
and [ADR 0001](https://github.com/b7n0de/proofbundle/blob/main/docs/adr/0001-decision-receipt-separate-predicate.md).
|
|
249
|
+
That signed outcome is the `action-outcome/v0.1` predicate (EXPERIMENTAL, 3.2.0): an executor signs what was
|
|
250
|
+
actually done, bound by content root to the Decision Receipt, with role separation (executor ≠ decision maker)
|
|
251
|
+
and `execution_proven` only when an effect digest backs an `executed` status. See
|
|
252
|
+
[docs/predicates/action-outcome.md](https://github.com/b7n0de/proofbundle/blob/main/docs/predicates/action-outcome.md).
|
|
249
253
|
|
|
250
254
|
## Docs
|
|
251
255
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proofbundle"
|
|
7
|
-
version = "3.1
|
|
7
|
+
version = "3.2.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"
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
15
|
|
|
16
|
-
__version__ = "3.1
|
|
16
|
+
__version__ = "3.2.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
|
|
@@ -47,6 +47,23 @@ __all__ = [
|
|
|
47
47
|
"beacon_audit_challenge",
|
|
48
48
|
"canonicalize_statement",
|
|
49
49
|
"statement_content_root",
|
|
50
|
+
"resolve_hash_alg",
|
|
51
|
+
"compute_dual_hash",
|
|
52
|
+
"verify_dual_hash",
|
|
53
|
+
"build_evidence_pack",
|
|
54
|
+
"verify_evidence_pack",
|
|
55
|
+
"ots_upgraded_proof_is_self_contained",
|
|
56
|
+
"build_initial_sequence",
|
|
57
|
+
"renew_timestamp",
|
|
58
|
+
"renew_hashtree",
|
|
59
|
+
"verify_sequence",
|
|
60
|
+
"last_ats",
|
|
61
|
+
"evaluate_renewal_policy",
|
|
62
|
+
"ArchiveTimeStamp",
|
|
63
|
+
"RenewalPolicy",
|
|
64
|
+
"verify_mldsa",
|
|
65
|
+
"verify_slhdsa",
|
|
66
|
+
"verify_hybrid",
|
|
50
67
|
"VerificationResult",
|
|
51
68
|
"Check",
|
|
52
69
|
"ProofBundleError",
|
|
@@ -75,6 +92,23 @@ _LAZY = {
|
|
|
75
92
|
"beacon_audit_challenge": ".beacon",
|
|
76
93
|
"canonicalize_statement": ".canonical",
|
|
77
94
|
"statement_content_root": ".canonical",
|
|
95
|
+
"resolve_hash_alg": ".hashalg",
|
|
96
|
+
"compute_dual_hash": ".hashalg",
|
|
97
|
+
"verify_dual_hash": ".hashalg",
|
|
98
|
+
"build_evidence_pack": ".evidence_pack",
|
|
99
|
+
"verify_evidence_pack": ".evidence_pack",
|
|
100
|
+
"ots_upgraded_proof_is_self_contained": ".evidence_pack",
|
|
101
|
+
"build_initial_sequence": ".renewal",
|
|
102
|
+
"renew_timestamp": ".renewal",
|
|
103
|
+
"renew_hashtree": ".renewal",
|
|
104
|
+
"verify_sequence": ".renewal",
|
|
105
|
+
"last_ats": ".renewal",
|
|
106
|
+
"evaluate_renewal_policy": ".renewal",
|
|
107
|
+
"ArchiveTimeStamp": ".renewal",
|
|
108
|
+
"RenewalPolicy": ".renewal",
|
|
109
|
+
"verify_mldsa": ".pqsig",
|
|
110
|
+
"verify_slhdsa": ".pqsig",
|
|
111
|
+
"verify_hybrid": ".pqsig",
|
|
78
112
|
}
|
|
79
113
|
|
|
80
114
|
if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime stays lazy
|
|
@@ -88,6 +122,13 @@ if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime s
|
|
|
88
122
|
verify_sample_opening)
|
|
89
123
|
from .beacon import beacon_audit_challenge
|
|
90
124
|
from .canonical import canonicalize_statement, statement_content_root
|
|
125
|
+
from .hashalg import compute_dual_hash, resolve_hash_alg, verify_dual_hash
|
|
126
|
+
from .evidence_pack import (build_evidence_pack, ots_upgraded_proof_is_self_contained,
|
|
127
|
+
verify_evidence_pack)
|
|
128
|
+
from .renewal import (ArchiveTimeStamp, RenewalPolicy, build_initial_sequence,
|
|
129
|
+
evaluate_renewal_policy, last_ats, renew_hashtree, renew_timestamp,
|
|
130
|
+
verify_sequence)
|
|
131
|
+
from .pqsig import verify_hybrid, verify_mldsa, verify_slhdsa
|
|
91
132
|
from .prereg import prereg_hash, verify_prereg
|
|
92
133
|
from .statuslist import verify_status_snapshot
|
|
93
134
|
from .tlogproof import verify_tlog_proof
|
|
@@ -19,13 +19,41 @@ import os
|
|
|
19
19
|
import shutil
|
|
20
20
|
import subprocess
|
|
21
21
|
import time
|
|
22
|
-
from
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Iterator, Optional
|
|
23
25
|
|
|
24
26
|
from .anchors_chia import ANCHOR_TYPE, verify_offline_merkle
|
|
25
27
|
|
|
26
28
|
_CHIA_BIN = os.getenv("CHIA_CLI", shutil.which("chia") or "chia")
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
@contextmanager
|
|
32
|
+
def _anchor_in_progress_lock(lock_path: Optional[str]) -> Iterator[None]:
|
|
33
|
+
"""Advisory 'anchor in progress' marker held around the on-chain batch_update + confirmation.
|
|
34
|
+
|
|
35
|
+
While an anchor's wallet-using transaction is submitted and confirmed, write a marker FILE at
|
|
36
|
+
``lock_path`` (or the ``PROOFBUNDLE_ANCHOR_LOCK_PATH`` env). A SEPARATE process — e.g. a wallet-switch
|
|
37
|
+
guard that must not log the wallet out from under the transaction — can treat the file's PRESENCE as
|
|
38
|
+
'an anchor is running'. Removed on completion; a crash leaves it stale, which a conservative guard
|
|
39
|
+
treats as still-active (fail-safe, never breaks a possibly-live anchor). No-op when no path is set, so
|
|
40
|
+
the default anchor flow is unchanged and proofbundle stays free of any consumer-specific naming."""
|
|
41
|
+
path = lock_path or os.getenv("PROOFBUNDLE_ANCHOR_LOCK_PATH")
|
|
42
|
+
if not path:
|
|
43
|
+
yield
|
|
44
|
+
return
|
|
45
|
+
p = Path(path)
|
|
46
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
p.write_text(str(os.getpid()))
|
|
48
|
+
try:
|
|
49
|
+
yield
|
|
50
|
+
finally:
|
|
51
|
+
try:
|
|
52
|
+
p.unlink()
|
|
53
|
+
except OSError:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
29
57
|
class ChiaRpcError(RuntimeError):
|
|
30
58
|
"""A Chia RPC/CLI call failed. Fail-closed: the caller aborts, no partial anchor is written."""
|
|
31
59
|
|
|
@@ -125,11 +153,15 @@ def _wait_confirmed(store_id: str, prev_root: Optional[str], *, timeout: int = 1
|
|
|
125
153
|
|
|
126
154
|
def anchor_add(canonical_root_hex: str, *, store_id: str, value_digest_hex: Optional[str] = None,
|
|
127
155
|
target: str = "receipt", network: str = "mainnet", fee: int = 100_000_000,
|
|
128
|
-
wait: bool = True) -> dict:
|
|
156
|
+
wait: bool = True, lock_path: Optional[str] = None) -> dict:
|
|
129
157
|
"""Full flow: insert ``key=canonicalRoot`` (value = ``value_digest`` or the canonicalRoot itself) into the
|
|
130
158
|
store, wait for on-chain confirmation, then export the anchor. Idempotent: a key already present ("Key
|
|
131
159
|
already present") is treated as "already anchored" and re-exported, not an error. Fail-closed: a network
|
|
132
|
-
failure raises ChiaRpcError and writes NOTHING partial (the caller keeps its receipt untouched).
|
|
160
|
+
failure raises ChiaRpcError and writes NOTHING partial (the caller keeps its receipt untouched).
|
|
161
|
+
|
|
162
|
+
``lock_path`` (or the ``PROOFBUNDLE_ANCHOR_LOCK_PATH`` env) holds an advisory 'anchor in progress' marker
|
|
163
|
+
file AUTOMATICALLY around the wallet-using batch_update + confirmation, so a separate wallet-switch guard
|
|
164
|
+
never logs the wallet out mid-anchor. No path → unchanged behaviour."""
|
|
133
165
|
canonical_root_hex = canonical_root_hex if canonical_root_hex.startswith(("0x", "0X")) else "0x" + canonical_root_hex
|
|
134
166
|
canonical_root = bytes.fromhex(canonical_root_hex[2:])
|
|
135
167
|
if len(canonical_root) != 32:
|
|
@@ -137,18 +169,19 @@ def anchor_add(canonical_root_hex: str, *, store_id: str, value_digest_hex: Opti
|
|
|
137
169
|
value_hex = value_digest_hex or canonical_root_hex
|
|
138
170
|
value_hex = value_hex if value_hex.startswith(("0x", "0X")) else "0x" + value_hex
|
|
139
171
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
already
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
with _anchor_in_progress_lock(lock_path):
|
|
173
|
+
prev_root = _rpc("data_layer", "get_root", {"id": store_id}).get("hash")
|
|
174
|
+
changelist = [{"action": "insert", "key": canonical_root_hex, "value": value_hex}]
|
|
175
|
+
already = False
|
|
176
|
+
try:
|
|
177
|
+
_rpc("data_layer", "batch_update",
|
|
178
|
+
{"id": store_id, "changelist": changelist, "fee": fee, "submit_on_chain": True})
|
|
179
|
+
except ChiaRpcError as exc:
|
|
180
|
+
if "already present" in str(exc).lower():
|
|
181
|
+
already = True # idempotent: the digest is already anchored
|
|
182
|
+
else:
|
|
183
|
+
raise # any other failure → clean abort, nothing written
|
|
184
|
+
if wait and not already:
|
|
185
|
+
_wait_confirmed(store_id, prev_root)
|
|
186
|
+
return export_anchor(store_id, canonical_root=canonical_root,
|
|
187
|
+
target=target, network=network, value=value_hex)
|
|
@@ -1303,6 +1303,127 @@ def _cmd_decision_init(args: argparse.Namespace) -> int:
|
|
|
1303
1303
|
return 0
|
|
1304
1304
|
|
|
1305
1305
|
|
|
1306
|
+
# ── Action Outcome Receipt (3.2.0, action-outcome/v0.1, EXPERIMENTAL) ──────────
|
|
1307
|
+
def _cmd_outcome_init(args: argparse.Namespace) -> int:
|
|
1308
|
+
template = {
|
|
1309
|
+
"schemaVersion": "0.1.0",
|
|
1310
|
+
"outcomeId": "urn:uuid:00000000-0000-0000-0000-000000000000",
|
|
1311
|
+
"decisionRef": {"sha256": "0" * 64},
|
|
1312
|
+
"executor": {"id": "executor://example/runner", "keyId": ""},
|
|
1313
|
+
"requestedActionDigest": {"sha256": "0" * 64},
|
|
1314
|
+
"actualActionDigest": {"sha256": "0" * 64},
|
|
1315
|
+
"responseDigest": {"sha256": "0" * 64},
|
|
1316
|
+
"effectDigest": {"sha256": "0" * 64},
|
|
1317
|
+
"status": "executed",
|
|
1318
|
+
"performedAt": "2026-01-01T00:00:00Z",
|
|
1319
|
+
"policyPurpose": "outcome",
|
|
1320
|
+
"traceContext": {"traceparent": ""},
|
|
1321
|
+
"limitations": [],
|
|
1322
|
+
}
|
|
1323
|
+
out = json.dumps(template, indent=2, ensure_ascii=False)
|
|
1324
|
+
if args.out:
|
|
1325
|
+
with open(args.out, "w", encoding="utf-8") as handle:
|
|
1326
|
+
handle.write(out + "\n")
|
|
1327
|
+
print(f"wrote outcome predicate template {args.out}")
|
|
1328
|
+
else:
|
|
1329
|
+
print(out)
|
|
1330
|
+
return 0
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
def _cmd_outcome_emit(args: argparse.Namespace) -> int:
|
|
1334
|
+
from .outcome import OutcomeReceiptError, emit_outcome_receipt # noqa: PLC0415
|
|
1335
|
+
signer = _resolve_signer(args)
|
|
1336
|
+
if signer is None:
|
|
1337
|
+
return 2
|
|
1338
|
+
try:
|
|
1339
|
+
with open(args.predicate, encoding="utf-8") as handle:
|
|
1340
|
+
predicate = loads_strict(handle.read()) # WP-C1: a duplicate key must never be signed
|
|
1341
|
+
env = emit_outcome_receipt(predicate, signer, strict=not args.lenient)
|
|
1342
|
+
except (OutcomeReceiptError, ProofBundleError, OSError, ValueError) as exc:
|
|
1343
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
1344
|
+
return 2
|
|
1345
|
+
with open(args.out, "w", encoding="utf-8") as handle:
|
|
1346
|
+
json.dump(env, handle, indent=2)
|
|
1347
|
+
handle.write("\n")
|
|
1348
|
+
print(f"wrote outcome receipt {args.out}")
|
|
1349
|
+
return 0
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
def _cmd_outcome_verify(args: argparse.Namespace) -> int:
|
|
1353
|
+
import base64 # noqa: PLC0415
|
|
1354
|
+
from .outcome import verify_outcome_receipt # noqa: PLC0415
|
|
1355
|
+
if not args.pub:
|
|
1356
|
+
print("ERROR: --pub <base64 Ed25519 public key> is required", file=sys.stderr)
|
|
1357
|
+
return 2
|
|
1358
|
+
try:
|
|
1359
|
+
with open(args.envelope, encoding="utf-8") as handle:
|
|
1360
|
+
env = loads_strict(handle.read()) # WP-C1: duplicate keys rejected
|
|
1361
|
+
pub = base64.b64decode(args.pub)
|
|
1362
|
+
result = verify_outcome_receipt(
|
|
1363
|
+
env, pub, strict=args.strict,
|
|
1364
|
+
expected_decision_ref=args.expected_decision_ref, decision_maker_id=args.decision_maker_id,
|
|
1365
|
+
expected_audience=args.aud, expected_nonce=args.nonce)
|
|
1366
|
+
except (ProofBundleError, OSError, ValueError) as exc:
|
|
1367
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
1368
|
+
return 2
|
|
1369
|
+
if args.json:
|
|
1370
|
+
report = {k: result[k] for k in (
|
|
1371
|
+
"ok", "crypto_ok", "structure_ok", "predicate_type_ok", "decision_bound", "role_separation_ok",
|
|
1372
|
+
"execution_proven", "audience_ok", "nonce_ok", "warnings", "errors",
|
|
1373
|
+
)}
|
|
1374
|
+
print(json.dumps(report, indent=2))
|
|
1375
|
+
else:
|
|
1376
|
+
print(f"CRYPTO: {'OK' if result['crypto_ok'] else 'FAIL'}")
|
|
1377
|
+
print(f"STRUCTURE: {'OK' if result['structure_ok'] else 'FAIL'}")
|
|
1378
|
+
if result["decision_bound"] is not None:
|
|
1379
|
+
print(f"DECISION_BINDING: {'OK' if result['decision_bound'] else 'MISMATCH'}")
|
|
1380
|
+
if result["role_separation_ok"] is not None:
|
|
1381
|
+
print(f"ROLE_SEPARATION: {'OK' if result['role_separation_ok'] else 'VIOLATED'}")
|
|
1382
|
+
if result["audience_ok"] is not None:
|
|
1383
|
+
print(f"AUDIENCE: {'OK' if result['audience_ok'] else 'MISMATCH'}")
|
|
1384
|
+
if result["nonce_ok"] is not None:
|
|
1385
|
+
print(f"NONCE: {'OK' if result['nonce_ok'] else 'MISMATCH'}")
|
|
1386
|
+
if result["execution_proven"] is False:
|
|
1387
|
+
print("ASSURANCE: status=executed is self-asserted (no effectDigest/actualActionDigest)")
|
|
1388
|
+
for e in result["errors"]:
|
|
1389
|
+
print(f" - {e}", file=sys.stderr)
|
|
1390
|
+
for w in result["warnings"]:
|
|
1391
|
+
print(f" ! {w}", file=sys.stderr)
|
|
1392
|
+
if result["crypto_ok"]:
|
|
1393
|
+
print("\nThis proves who signed what happened, bound to the referenced decision. It does not prove "
|
|
1394
|
+
"the effect was good, correct or desired.")
|
|
1395
|
+
else:
|
|
1396
|
+
print("\nThis receipt did NOT verify (crypto failure); nothing about the outcome is attested.")
|
|
1397
|
+
# Exit contract mirrors decision verify: 1 crypto · 2 malformed/confusion/binding-mismatch · 3 policyPurpose.
|
|
1398
|
+
if not result["crypto_ok"]:
|
|
1399
|
+
return 1
|
|
1400
|
+
if not result["structure_ok"]:
|
|
1401
|
+
return 2
|
|
1402
|
+
if result["decision_bound"] is False or result["role_separation_ok"] is False:
|
|
1403
|
+
return 2
|
|
1404
|
+
if result["audience_ok"] is False or result["nonce_ok"] is False:
|
|
1405
|
+
return 2
|
|
1406
|
+
return 0
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
def _cmd_outcome_inspect(args: argparse.Namespace) -> int:
|
|
1410
|
+
import base64 # noqa: PLC0415
|
|
1411
|
+
try:
|
|
1412
|
+
with open(args.receipt, encoding="utf-8") as handle:
|
|
1413
|
+
obj = loads_strict(handle.read()) # WP-C1
|
|
1414
|
+
except (ProofBundleError, OSError, ValueError) as exc:
|
|
1415
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
1416
|
+
return 2
|
|
1417
|
+
try:
|
|
1418
|
+
statement = loads_strict(base64.b64decode(obj["payload"])) if isinstance(obj, dict) and "payload" in obj else obj
|
|
1419
|
+
except (ProofBundleError, ValueError, TypeError) as exc:
|
|
1420
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
1421
|
+
return 2
|
|
1422
|
+
predicate = statement.get("predicate", statement) if isinstance(statement, dict) else statement
|
|
1423
|
+
print(json.dumps(predicate, indent=2, ensure_ascii=False))
|
|
1424
|
+
return 0
|
|
1425
|
+
|
|
1426
|
+
|
|
1306
1427
|
def _cmd_policy_explain(args: argparse.Namespace) -> int:
|
|
1307
1428
|
from .policy import PolicyError, explain_policy, load_policy, policy_warnings # noqa: PLC0415
|
|
1308
1429
|
from .policy_profiles import resolve_policy_source # noqa: PLC0415
|
|
@@ -1773,6 +1894,45 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1773
1894
|
d_inspect.add_argument("receipt", help="path to a DSSE receipt or a raw in-toto Statement")
|
|
1774
1895
|
d_inspect.set_defaults(func=_cmd_decision_inspect)
|
|
1775
1896
|
|
|
1897
|
+
# ── Action Outcome Receipt (3.2.0, action-outcome/v0.1, EXPERIMENTAL) ──
|
|
1898
|
+
outcome = sub.add_parser("outcome", help="action-outcome/v0.1 (EXPERIMENTAL): init/emit/verify/inspect an action outcome")
|
|
1899
|
+
osub = outcome.add_subparsers(dest="outcome_command", required=True)
|
|
1900
|
+
|
|
1901
|
+
o_init = osub.add_parser("init", help="print a template outcome predicate (fill in and sign with 'outcome emit')")
|
|
1902
|
+
o_init.add_argument("--out", default=None, help="write the template to a file instead of stdout")
|
|
1903
|
+
o_init.set_defaults(func=_cmd_outcome_init)
|
|
1904
|
+
|
|
1905
|
+
o_emit = osub.add_parser("emit", help="sign an outcome predicate into a DSSE receipt")
|
|
1906
|
+
o_emit.add_argument("predicate", help="path to the outcome predicate JSON")
|
|
1907
|
+
o_emit.add_argument("--out", required=True, help="output path for the signed outcome receipt")
|
|
1908
|
+
o_emit.add_argument("--key", help="load an existing Ed25519 signing key from file")
|
|
1909
|
+
o_emit.add_argument("--new-key", dest="new_key", help="generate a new signing key and write it to file")
|
|
1910
|
+
o_emit.add_argument("--lenient", action="store_true", help="allow non-strict predicates")
|
|
1911
|
+
o_emit.set_defaults(func=_cmd_outcome_emit)
|
|
1912
|
+
|
|
1913
|
+
o_verify = osub.add_parser(
|
|
1914
|
+
"verify", help="verify an outcome receipt (crypto + structure; decision binding + role separation)",
|
|
1915
|
+
description=("Exit codes: 0 OK · 1 crypto/verification failure · 2 malformed / predicateType confusion "
|
|
1916
|
+
"/ decision-binding or role-separation mismatch. --expected-decision-ref binds the outcome "
|
|
1917
|
+
"to a specific decision content root (replay across decisions fails). --decision-maker-id "
|
|
1918
|
+
"enforces role separation (executor must differ from the decision maker). status=executed "
|
|
1919
|
+
"without an effect/actual digest is a self-asserted claim, not proof of the effect."))
|
|
1920
|
+
o_verify.add_argument("envelope", help="path to the DSSE outcome receipt")
|
|
1921
|
+
o_verify.add_argument("--pub", required=True, help="executor Ed25519 public key (base64) to verify against")
|
|
1922
|
+
o_verify.add_argument("--expected-decision-ref", dest="expected_decision_ref", default=None,
|
|
1923
|
+
help="expected decision content root (sha256 hex); a mismatch is a cross-decision replay, fail-closed")
|
|
1924
|
+
o_verify.add_argument("--decision-maker-id", dest="decision_maker_id", default=None,
|
|
1925
|
+
help="the bound decision's maker id; executor==maker violates role separation (fail-closed)")
|
|
1926
|
+
o_verify.add_argument("--json", action="store_true", help="machine readable output")
|
|
1927
|
+
o_verify.add_argument("--strict", action="store_true", help="enforce strict-v0.1 required fields")
|
|
1928
|
+
o_verify.add_argument("--aud", default=None, help="expected audience (checks validity.audience against replay)")
|
|
1929
|
+
o_verify.add_argument("--nonce", default=None, help="expected nonce (checks validity.nonce against replay)")
|
|
1930
|
+
o_verify.set_defaults(func=_cmd_outcome_verify)
|
|
1931
|
+
|
|
1932
|
+
o_inspect = osub.add_parser("inspect", help="print an outcome receipt's predicate (no crypto verification)")
|
|
1933
|
+
o_inspect.add_argument("receipt", help="path to a DSSE receipt or a raw in-toto Statement")
|
|
1934
|
+
o_inspect.set_defaults(func=_cmd_outcome_inspect)
|
|
1935
|
+
|
|
1776
1936
|
return parser
|
|
1777
1937
|
|
|
1778
1938
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""B6 evidence-pack hardening for offline long-term verification (EXPERIMENTAL; ADR 0006).
|
|
2
|
+
|
|
3
|
+
Goal: a pack built from a receipt verifies OFFLINE on a second machine from bundled data — no calendar,
|
|
4
|
+
no network. The OTS anchor already verifies offline from a relying-party Bitcoin block header
|
|
5
|
+
(``anchors_ots.verify_opentimestamps``); this module adds the PACK layer around it:
|
|
6
|
+
|
|
7
|
+
* ``ots_upgraded_proof_is_self_contained`` — a proof with a Bitcoin block-header attestation is upgraded
|
|
8
|
+
and self-contained (the calendar is no longer needed to verify existence-in-Bitcoin). A pending-only or
|
|
9
|
+
malformed proof is not.
|
|
10
|
+
* ``build_evidence_pack`` — assemble the self-contained proof + the canonical root + the set of calendars
|
|
11
|
+
it was submitted to (multi-calendar redundancy metadata) and, optionally, a COPY of the Bitcoin block
|
|
12
|
+
headers as EVIDENCE.
|
|
13
|
+
* ``verify_evidence_pack`` — verify the pack with NO network I/O, delegating to the OTS verifier.
|
|
14
|
+
|
|
15
|
+
WP-A1 boundary (never crossed). The header a pack bundles is producer-controlled EVIDENCE, never trust —
|
|
16
|
+
a producer could bundle a self-committed, backdated header. Confirmation therefore still needs a
|
|
17
|
+
RELYING-PARTY header (``rp_trust.bitcoin_block_headers``). That header can be an OFFLINE trusted Bitcoin
|
|
18
|
+
checkpoint the relying party ships, so "needs a relying-party header" does NOT mean "needs the network".
|
|
19
|
+
A pack with a bundled header but no relying-party trust is honestly ``needs_rp_trust``, not a pass.
|
|
20
|
+
|
|
21
|
+
Honesty on scope. The MECHANISM here is complete and tested. Producing a pack from a REAL confirmed
|
|
22
|
+
receipt (submitting to public calendars, waiting for Bitcoin confirmation, ``ots upgrade``) is an
|
|
23
|
+
Owner-gated external anchor submission and is tracked as the OPEN end-to-end step in ADR 0006 — not
|
|
24
|
+
claimed here.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import base64
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
from .anchors_ots import _classify, verify_opentimestamps
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"ots_upgraded_proof_is_self_contained",
|
|
35
|
+
"build_evidence_pack",
|
|
36
|
+
"verify_evidence_pack",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def ots_upgraded_proof_is_self_contained(proof: bytes) -> bool:
|
|
41
|
+
"""True iff ``proof`` is an UPGRADED OTS proof (a Bitcoin block-header attestation) — self-contained,
|
|
42
|
+
so verifying existence-in-Bitcoin no longer needs a calendar. A pending-only or malformed proof is
|
|
43
|
+
False (fail-closed; never over-claim a pending proof as self-contained)."""
|
|
44
|
+
try:
|
|
45
|
+
from opentimestamps.core.serialize import BytesDeserializationContext # noqa: PLC0415
|
|
46
|
+
from opentimestamps.core.timestamp import DetachedTimestampFile # noqa: PLC0415
|
|
47
|
+
except ImportError:
|
|
48
|
+
return False
|
|
49
|
+
try:
|
|
50
|
+
dtf = DetachedTimestampFile.deserialize(BytesDeserializationContext(proof))
|
|
51
|
+
except Exception:
|
|
52
|
+
return False
|
|
53
|
+
has_bitcoin, _heights, _has_pending = _classify(dtf.timestamp)
|
|
54
|
+
return has_bitcoin
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_evidence_pack(canonical_root: bytes, proof: bytes, *, calendars: list[str],
|
|
58
|
+
bundled_headers: Optional[dict[str, str]] = None) -> dict:
|
|
59
|
+
"""Assemble an offline-verifiable evidence pack around an OTS proof.
|
|
60
|
+
|
|
61
|
+
``calendars`` is the set of calendar servers the stamp was submitted to; recording two or more is the
|
|
62
|
+
multi-calendar redundancy metadata (``calendarRedundancy`` = distinct count). ``bundled_headers`` (a
|
|
63
|
+
``height -> block merkle-root hex`` map) is copied into the pack as EVIDENCE only (``frozen`` block,
|
|
64
|
+
WP-A1: never trusted by the verifier). The pack never contains a secret."""
|
|
65
|
+
distinct = sorted(set(calendars))
|
|
66
|
+
pack: dict = {
|
|
67
|
+
"type": "opentimestamps-evidence-pack",
|
|
68
|
+
"packVersion": "v0.1",
|
|
69
|
+
"canonicalRoot": base64.b64encode(canonical_root).decode(),
|
|
70
|
+
"proof": base64.b64encode(proof).decode(),
|
|
71
|
+
"selfContained": ots_upgraded_proof_is_self_contained(proof),
|
|
72
|
+
"calendars": distinct,
|
|
73
|
+
"calendarRedundancy": len(distinct),
|
|
74
|
+
"bundledHeaderEvidence": bool(bundled_headers),
|
|
75
|
+
}
|
|
76
|
+
if bundled_headers:
|
|
77
|
+
# producer-controlled EVIDENCE, surfaced in the frozen block; verify_opentimestamps treats it as
|
|
78
|
+
# frozenEvidence and never as trust (WP-A1).
|
|
79
|
+
pack["frozen"] = {"bitcoinBlockHeaderMerkleRootsByHeight": dict(bundled_headers)}
|
|
80
|
+
return pack
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def verify_evidence_pack(pack: dict, *, rp_trust: Optional[dict] = None,
|
|
84
|
+
now: Optional[int] = None) -> dict:
|
|
85
|
+
"""Verify an evidence pack OFFLINE (no network I/O). Delegates to the OTS verifier with the pack's
|
|
86
|
+
proof + canonical root; the pack's bundled header is passed as ``frozen`` EVIDENCE, and confirmation
|
|
87
|
+
happens only against a relying-party header (``rp_trust``), which may be an offline checkpoint.
|
|
88
|
+
|
|
89
|
+
Returns the OTS verifier's result dict ({ok, detail, warn, status, …}). Fail-closed on a malformed
|
|
90
|
+
pack (missing/!b64 proof or root)."""
|
|
91
|
+
try:
|
|
92
|
+
proof = base64.b64decode(pack["proof"], validate=True)
|
|
93
|
+
canonical_root = base64.b64decode(pack["canonicalRoot"], validate=True)
|
|
94
|
+
except (KeyError, ValueError, TypeError) as exc:
|
|
95
|
+
return {"ok": False, "warn": False, "status": "malformed_pack",
|
|
96
|
+
"detail": f"evidence pack is missing or has a non-base64 proof/canonicalRoot: {exc}"}
|
|
97
|
+
frozen = pack.get("frozen") or {}
|
|
98
|
+
return verify_opentimestamps(proof, canonical_root, frozen=frozen, now=now, rp_trust=rp_trust)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""B2 hash-agility — an explicit registry of hash algorithms with fail-closed resolution and a
|
|
2
|
+
dual-hash helper (ADR 0006).
|
|
3
|
+
|
|
4
|
+
Why this module exists. proofbundle's surfaces already DECLARE their hash construction per artifact
|
|
5
|
+
(``merkle.hash_alg = "sha256-rfc6962"``, ``contentRootAlg = "jcs-sha256-v1"``, a checkpoint's
|
|
6
|
+
``hashAlg``). What was missing for long-term evidence is a single agility layer: one registry that
|
|
7
|
+
says which hash PRIMITIVES are allowed, which are deprecated, and a resolver that NEVER silently
|
|
8
|
+
defaults a missing/unknown/weak algorithm — the exact place an algorithm-confusion attack would hide
|
|
9
|
+
(RFC 7696 §2.1; ADR 0002 §2 warns of the same at the content-root level). The algorithm id maps to the
|
|
10
|
+
RFC 4998 (ERS) ``digestAlgorithm`` OID, so the B3 renewal chain builds directly on this registry.
|
|
11
|
+
|
|
12
|
+
Model: the IANA Named Information Hash Algorithm registry (RFC 6920) — an id, a status
|
|
13
|
+
(``current`` / ``deprecated``), and enough to compute and size the digest. Deprecation follows the NIST
|
|
14
|
+
transition (SHA-1 retired; SHA-256/384/512 and the SHA-3 family current — NIST FIPS 180-4 / 202,
|
|
15
|
+
SP 800-131A).
|
|
16
|
+
|
|
17
|
+
Fail-closed contract:
|
|
18
|
+
* an absent/empty id is ``MissingHashAlgId`` — there is NO implicit SHA-256;
|
|
19
|
+
* an id not in the registry is ``UnknownHashAlg``;
|
|
20
|
+
* a deprecated id is ``DeprecatedHashAlg`` unless the caller explicitly opts into legacy verification
|
|
21
|
+
(``allow_deprecated=True``) — a verifier may need to read an old receipt, but nothing NEW is ever
|
|
22
|
+
produced with a weak hash, and a dual-hash's PASS requires at least one current algorithm.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import hashlib
|
|
27
|
+
from collections.abc import Mapping, Sequence
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
from .errors import Check, ProofBundleError, VerificationResult
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"HASH_REGISTRY",
|
|
35
|
+
"HashAlg",
|
|
36
|
+
"HashAlgError",
|
|
37
|
+
"MissingHashAlgId",
|
|
38
|
+
"UnknownHashAlg",
|
|
39
|
+
"DeprecatedHashAlg",
|
|
40
|
+
"resolve_hash_alg",
|
|
41
|
+
"compute_digest",
|
|
42
|
+
"compute_dual_hash",
|
|
43
|
+
"verify_dual_hash",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HashAlgError(ProofBundleError):
|
|
48
|
+
"""Base class for every hash-agility failure (a subclass of ``ProofBundleError``)."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MissingHashAlgId(HashAlgError):
|
|
52
|
+
"""No algorithm id was given. Fail-closed: proofbundle never defaults a missing hash to SHA-256."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class UnknownHashAlg(HashAlgError):
|
|
56
|
+
"""The algorithm id is not in ``HASH_REGISTRY``. Fail-closed: an unknown hash is never trusted."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DeprecatedHashAlg(HashAlgError):
|
|
60
|
+
"""The algorithm is known but deprecated and the caller did not opt into legacy verification."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class HashAlg:
|
|
65
|
+
"""One registry entry: how to compute the digest, its size, its status, and its ERS OID.
|
|
66
|
+
|
|
67
|
+
``ers_oid`` is the RFC 4998 ``digestAlgorithm`` ``AlgorithmIdentifier`` OID, so a renewal chain (B3)
|
|
68
|
+
references the same identity this registry defines. ``hashlib_name`` is the ``hashlib.new`` name."""
|
|
69
|
+
|
|
70
|
+
id: str
|
|
71
|
+
hashlib_name: str
|
|
72
|
+
digest_size: int
|
|
73
|
+
status: str # "current" | "deprecated"
|
|
74
|
+
ers_oid: str
|
|
75
|
+
|
|
76
|
+
def new(self) -> "hashlib._Hash":
|
|
77
|
+
return hashlib.new(self.hashlib_name)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# The allowed hash primitives. SHA-256/384/512 (FIPS 180-4) and SHA3-256/512 (FIPS 202) are current;
|
|
81
|
+
# SHA-1 and MD5 are deprecated (kept only so a verifier can READ and clearly reject a legacy receipt).
|
|
82
|
+
# OIDs are the NIST/RFC AlgorithmIdentifier values used by RFC 4998 digestAlgorithm.
|
|
83
|
+
HASH_REGISTRY: dict[str, HashAlg] = {
|
|
84
|
+
"sha256": HashAlg("sha256", "sha256", 32, "current", "2.16.840.1.101.3.4.2.1"),
|
|
85
|
+
"sha384": HashAlg("sha384", "sha384", 48, "current", "2.16.840.1.101.3.4.2.2"),
|
|
86
|
+
"sha512": HashAlg("sha512", "sha512", 64, "current", "2.16.840.1.101.3.4.2.3"),
|
|
87
|
+
"sha3-256": HashAlg("sha3-256", "sha3_256", 32, "current", "2.16.840.1.101.3.4.2.8"),
|
|
88
|
+
"sha3-512": HashAlg("sha3-512", "sha3_512", 64, "current", "2.16.840.1.101.3.4.2.10"),
|
|
89
|
+
"sha1": HashAlg("sha1", "sha1", 20, "deprecated", "1.3.14.3.2.26"),
|
|
90
|
+
"md5": HashAlg("md5", "md5", 16, "deprecated", "1.2.840.113549.2.5"),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def resolve_hash_alg(alg_id: Optional[str], *, allow_deprecated: bool = False) -> HashAlg:
|
|
95
|
+
"""Resolve an algorithm id to its registry entry, fail-closed.
|
|
96
|
+
|
|
97
|
+
Raises ``MissingHashAlgId`` for an absent/empty id (no implicit default), ``UnknownHashAlg`` for an
|
|
98
|
+
id not in the registry, and ``DeprecatedHashAlg`` for a weak algorithm unless ``allow_deprecated``.
|
|
99
|
+
"""
|
|
100
|
+
if not alg_id or not isinstance(alg_id, str):
|
|
101
|
+
raise MissingHashAlgId(
|
|
102
|
+
"a hash algorithm id is required — proofbundle never defaults a missing hash to SHA-256")
|
|
103
|
+
spec = HASH_REGISTRY.get(alg_id)
|
|
104
|
+
if spec is None:
|
|
105
|
+
raise UnknownHashAlg(
|
|
106
|
+
f"unknown hash algorithm {alg_id!r} — not in the allowed registry "
|
|
107
|
+
f"({', '.join(sorted(HASH_REGISTRY))})")
|
|
108
|
+
if spec.status == "deprecated" and not allow_deprecated:
|
|
109
|
+
raise DeprecatedHashAlg(
|
|
110
|
+
f"hash algorithm {alg_id!r} is deprecated and rejected by default; a legacy verifier must "
|
|
111
|
+
"opt in explicitly (allow_deprecated=True)")
|
|
112
|
+
return spec
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def compute_digest(data: bytes, alg_id: str, *, allow_deprecated: bool = False) -> str:
|
|
116
|
+
"""Hex digest of ``data`` under ``alg_id`` (fail-closed on missing/unknown/deprecated)."""
|
|
117
|
+
spec = resolve_hash_alg(alg_id, allow_deprecated=allow_deprecated)
|
|
118
|
+
h = spec.new()
|
|
119
|
+
h.update(data)
|
|
120
|
+
return h.hexdigest()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def compute_dual_hash(data: bytes, alg_ids: Sequence[str]) -> dict[str, str]:
|
|
124
|
+
"""Digests of ``data`` under two or more DISTINCT CURRENT algorithms — for a NEW receipt.
|
|
125
|
+
|
|
126
|
+
A dual hash lets an old receipt survive the deprecation of one hash: as long as a second,
|
|
127
|
+
independent current hash still binds the same bytes, the evidence keeps its force while a renewal
|
|
128
|
+
(B3) migrates it. A new receipt therefore requires at least two distinct current algorithms; a
|
|
129
|
+
deprecated algorithm is rejected outright (nothing new is produced with a weak hash)."""
|
|
130
|
+
seen: dict[str, HashAlg] = {}
|
|
131
|
+
for alg_id in alg_ids:
|
|
132
|
+
spec = resolve_hash_alg(alg_id) # current-only: no allow_deprecated on the produce path
|
|
133
|
+
if spec.id in seen:
|
|
134
|
+
raise HashAlgError(f"dual hash needs DISTINCT algorithms, {spec.id!r} was given twice")
|
|
135
|
+
seen[spec.id] = spec
|
|
136
|
+
if len(seen) < 2:
|
|
137
|
+
raise HashAlgError(
|
|
138
|
+
"a dual hash needs at least two distinct current algorithms "
|
|
139
|
+
"(e.g. sha256 + sha512 or sha256 + sha3-256)")
|
|
140
|
+
return {alg_id: compute_digest(data, alg_id) for alg_id in seen}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def verify_dual_hash(data: bytes, digests: Mapping[str, str]) -> VerificationResult:
|
|
144
|
+
"""Verify that EVERY declared digest binds ``data``, and that at least one is a CURRENT algorithm.
|
|
145
|
+
|
|
146
|
+
Fail-closed: an empty map fails; an unknown algorithm fails; a single mismatching leg fails the
|
|
147
|
+
whole result; a bag of only-deprecated digests fails even when the bytes match (a legacy-only
|
|
148
|
+
receipt has lost its force and must be renewed, not silently accepted). A deprecated leg is checked
|
|
149
|
+
(``allow_deprecated``) so its presence is visible, but it cannot by itself carry a PASS."""
|
|
150
|
+
result = VerificationResult()
|
|
151
|
+
if not isinstance(digests, Mapping) or not digests:
|
|
152
|
+
result.checks.append(Check("hashalg:dual", False,
|
|
153
|
+
"digests must be a non-empty mapping of alg -> hex"))
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
current_ok = 0
|
|
157
|
+
for alg_id, expected in digests.items():
|
|
158
|
+
try:
|
|
159
|
+
spec = resolve_hash_alg(alg_id, allow_deprecated=True)
|
|
160
|
+
except HashAlgError as exc:
|
|
161
|
+
result.checks.append(Check(f"hashalg:{alg_id}", False, str(exc)))
|
|
162
|
+
continue
|
|
163
|
+
actual = compute_digest(data, alg_id, allow_deprecated=True)
|
|
164
|
+
match = isinstance(expected, str) and actual == expected.lower()
|
|
165
|
+
detail = "digest matches" if match else "digest mismatch"
|
|
166
|
+
if match and spec.status == "deprecated":
|
|
167
|
+
detail = "digest matches but algorithm is deprecated (does not carry a PASS on its own)"
|
|
168
|
+
result.checks.append(Check(f"hashalg:{alg_id}", match, detail))
|
|
169
|
+
if match and spec.status == "current":
|
|
170
|
+
current_ok += 1
|
|
171
|
+
|
|
172
|
+
all_match = all(c.ok for c in result.checks)
|
|
173
|
+
if all_match and current_ok == 0:
|
|
174
|
+
result.checks.append(Check(
|
|
175
|
+
"hashalg:current-binding", False,
|
|
176
|
+
"no CURRENT-algorithm digest binds the payload — a legacy-only receipt must be renewed"))
|
|
177
|
+
return result
|