proofbundle 3.2.2__tar.gz → 3.2.3__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.2.2/src/proofbundle.egg-info → proofbundle-3.2.3}/PKG-INFO +9 -4
- {proofbundle-3.2.2 → proofbundle-3.2.3}/README.md +8 -3
- {proofbundle-3.2.2 → proofbundle-3.2.3}/pyproject.toml +1 -1
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/__init__.py +24 -1
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/_strict_json.py +43 -3
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/_provenance.py +30 -2
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/anchors_markovian.py +6 -0
- proofbundle-3.2.3/src/proofbundle/assurance.py +201 -0
- proofbundle-3.2.3/src/proofbundle/automation_verdict.py +101 -0
- proofbundle-3.2.3/src/proofbundle/budget.py +103 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/bundle.py +13 -1
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/cli.py +83 -7
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/decision.py +53 -3
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/dsse.py +17 -0
- proofbundle-3.2.3/src/proofbundle/evalcard.py +62 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/evalclaim.py +49 -2
- proofbundle-3.2.3/src/proofbundle/outcome.py +689 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policy.py +48 -1
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/renewal.py +133 -2
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/run_ledger.py +18 -1
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/sdjwt.py +43 -13
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/sdjwt_vc.py +3 -2
- proofbundle-3.2.3/src/proofbundle/signature.py +69 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/trust_pack.py +53 -6
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/verification_summary.py +16 -1
- {proofbundle-3.2.2 → proofbundle-3.2.3/src/proofbundle.egg-info}/PKG-INFO +9 -4
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle.egg-info/SOURCES.txt +13 -0
- proofbundle-3.2.3/tests/test_assurance.py +193 -0
- proofbundle-3.2.3/tests/test_automation_verdict.py +216 -0
- proofbundle-3.2.3/tests/test_budget.py +240 -0
- proofbundle-3.2.3/tests/test_bundle.py +132 -0
- proofbundle-3.2.3/tests/test_enclave_assurance.py +247 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_eval_claim_schema.py +10 -0
- proofbundle-3.2.3/tests/test_evalcard.py +136 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_evalclaim.py +19 -0
- proofbundle-3.2.3/tests/test_outcome_receiver_corroboration.py +436 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_outcome_verify.py +81 -0
- proofbundle-3.2.3/tests/test_provenance.py +177 -0
- proofbundle-3.2.3/tests/test_renewal_external_token_glue.py +269 -0
- proofbundle-3.2.3/tests/test_rust_parity_gate.py +391 -0
- proofbundle-3.2.3/tests/test_sdjwtvc_external_vectors.py +266 -0
- proofbundle-3.2.3/tests/test_signature.py +106 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_trust_pack.py +22 -0
- proofbundle-3.2.3/tests/test_trust_pack_outcome_receivers_role.py +75 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_trust_policy.py +80 -3
- proofbundle-3.2.2/src/proofbundle/outcome.py +0 -380
- proofbundle-3.2.2/src/proofbundle/signature.py +0 -31
- proofbundle-3.2.2/tests/test_bundle.py +0 -67
- proofbundle-3.2.2/tests/test_provenance.py +0 -77
- proofbundle-3.2.2/tests/test_sdjwtvc_external_vectors.py +0 -162
- proofbundle-3.2.2/tests/test_signature.py +0 -36
- {proofbundle-3.2.2 → proofbundle-3.2.3}/LICENSE +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/setup.cfg +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/_integration.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/__init__.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/eee.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/promptfoo.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/samples.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/anchors.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/anchors_chia.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/anchors_chia_add.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/anchors_ots.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/anchors_rfc3161.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/beacon.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/canonical.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/checkpoint.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/demo.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/emit.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/errors.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/evidence_pack.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/experimental/__init__.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/experimental/enclave.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/hashalg.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/hf_evals.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/intoto.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/kbjwt.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/merkle.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/persample.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policies/decision-receipt-template-v1.json +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policies/research-preview-v1.json +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policies/strict-eval-authenticated-root-template-v1.json +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policies/strict-eval-template-v1.json +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policies/strict-prereg-template-v1.json +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policy_profiles.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/pqsig.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/prereg.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/public_transparency.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/py.typed +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/sdjwt_issue.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/statuslist.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/subject_binding.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/tlogproof.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle.egg-info/requires.txt +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_adapters.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_adversarial.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchor_hardening_321.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchor_longevity_property.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchor_target_trustedtime.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_chia.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_chia_add.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_chia_claims.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_generic.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_markovian.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_ots.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_rfc3161.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_beacon.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_branch_base_check.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_canonical.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_checkpoint.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_checkpoint_external_vectors.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_checkpoint_quorum_property.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_claims_hygiene.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_cli.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_cli_eval.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_cli_require_anchor.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_conformance.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_content_root_property.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_cosignature.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_anchors.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_cli.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_evidence.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_fuzz.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_hardening.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_policy.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_schema.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_validator_api.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_verify.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_demo.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_docs_truth.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_dsse_adversarial.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_dup_key_reject.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_ed25519_semantics.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_eee.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_emit.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_eval_evidence_class.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_evidence_pack.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_examples.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_experimental_enclave.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_fork_pr_secret_isolation.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_fuzz_parsers.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_hashalg.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_hf_entry_verify_eee_digest.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_hf_evals.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_inspect_hook.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_claims_hygiene.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_content_root_migration.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_eval_result.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_examples.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_spec_diff.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_svr.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_kbjwt.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_lens_review_fixes_3_1_3.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_merkle.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_merkle_consistency_property.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_merkle_property.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_mldsa_acvp_vectors.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_official_vectors.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_ok_semantics.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_ots_external_vectors.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_oversized_int_guard.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_persample.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_pinned_key_validation.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_policy_explain_lint.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_policy_lifecycle_purpose.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_policy_profiles.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_policy_templates.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_pqsig.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_predicate_type_enforcement.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_prereg.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_promptfoo.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_public_transparency.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_rekor_interop.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_release_integrity_gate.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_renewal.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_renewal_policy.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_renewal_signed.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_root_authenticity.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_run_ledger.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_schema.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_schema_parity.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_adversarial.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_duplicate_cnf.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_resource_budget.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_vc.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_verify_binding.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_statuslist.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_subject_binding.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_tlogproof.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_tree_context_authenticity.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_trust_pack_crypto_agility.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_verification_summary.py +0 -0
- {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_verify_matrix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofbundle
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.3
|
|
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
|
|
@@ -205,7 +205,7 @@ a preprint.
|
|
|
205
205
|
| **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 |
|
|
206
206
|
| **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 |
|
|
207
207
|
| **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 |
|
|
208
|
-
| **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 |
|
|
208
|
+
| **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 (`evaluation_card_sha256`, EVAL_CLAIM.md), not its quality |
|
|
209
209
|
| 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 |
|
|
210
210
|
|
|
211
211
|
## Cite this work
|
|
@@ -341,12 +341,17 @@ is anchored to external RFC 6962 vectors and a real Rekor proof, not just its ow
|
|
|
341
341
|
by itself — it is the small, offline, standards-native receipt layer between them. Security policy:
|
|
342
342
|
[SECURITY.md](https://github.com/b7n0de/proofbundle/blob/main/SECURITY.md).
|
|
343
343
|
|
|
344
|
-
**Roadmap (stated honestly,
|
|
344
|
+
**Roadmap (stated honestly, not yet built):**
|
|
345
345
|
|
|
346
346
|
- A post-quantum *payload* signature (crypto-agility for the receipt itself) is on the roadmap; today the
|
|
347
347
|
post-quantum coverage is witness-side ML-DSA-44 cosignatures only.
|
|
348
348
|
- A CLI flag to select the content-root algorithm is still deferred (`jcs-sha256-v1` is the signed default).
|
|
349
|
-
|
|
349
|
+
|
|
350
|
+
**Already shipped, preview/experimental maturity (not on the roadmap above — built, tested, install
|
|
351
|
+
extra `[experimental]`, but API/wire-format may still change without deprecation):**
|
|
352
|
+
|
|
353
|
+
- A TEE-attestation bridge (RATS/EAT, RFC 9334 + RFC 9711) that makes `assurance_level =
|
|
354
|
+
enclave_attested` independently verifiable — see [docs/EXPERIMENTAL_ENCLAVE.md](https://github.com/b7n0de/proofbundle/blob/main/docs/EXPERIMENTAL_ENCLAVE.md).
|
|
350
355
|
|
|
351
356
|
## Contributing
|
|
352
357
|
|
|
@@ -151,7 +151,7 @@ a preprint.
|
|
|
151
151
|
| **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 |
|
|
152
152
|
| **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 |
|
|
153
153
|
| **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 |
|
|
154
|
-
| **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 |
|
|
154
|
+
| **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 (`evaluation_card_sha256`, EVAL_CLAIM.md), not its quality |
|
|
155
155
|
| 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 |
|
|
156
156
|
|
|
157
157
|
## Cite this work
|
|
@@ -287,12 +287,17 @@ is anchored to external RFC 6962 vectors and a real Rekor proof, not just its ow
|
|
|
287
287
|
by itself — it is the small, offline, standards-native receipt layer between them. Security policy:
|
|
288
288
|
[SECURITY.md](https://github.com/b7n0de/proofbundle/blob/main/SECURITY.md).
|
|
289
289
|
|
|
290
|
-
**Roadmap (stated honestly,
|
|
290
|
+
**Roadmap (stated honestly, not yet built):**
|
|
291
291
|
|
|
292
292
|
- A post-quantum *payload* signature (crypto-agility for the receipt itself) is on the roadmap; today the
|
|
293
293
|
post-quantum coverage is witness-side ML-DSA-44 cosignatures only.
|
|
294
294
|
- A CLI flag to select the content-root algorithm is still deferred (`jcs-sha256-v1` is the signed default).
|
|
295
|
-
|
|
295
|
+
|
|
296
|
+
**Already shipped, preview/experimental maturity (not on the roadmap above — built, tested, install
|
|
297
|
+
extra `[experimental]`, but API/wire-format may still change without deprecation):**
|
|
298
|
+
|
|
299
|
+
- A TEE-attestation bridge (RATS/EAT, RFC 9334 + RFC 9711) that makes `assurance_level =
|
|
300
|
+
enclave_attested` independently verifiable — see [docs/EXPERIMENTAL_ENCLAVE.md](https://github.com/b7n0de/proofbundle/blob/main/docs/EXPERIMENTAL_ENCLAVE.md).
|
|
296
301
|
|
|
297
302
|
## Contributing
|
|
298
303
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proofbundle"
|
|
7
|
-
version = "3.2.
|
|
7
|
+
version = "3.2.3"
|
|
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.2.
|
|
16
|
+
__version__ = "3.2.3"
|
|
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
|
|
@@ -44,6 +44,8 @@ __all__ = [
|
|
|
44
44
|
"audit_challenge",
|
|
45
45
|
"prereg_hash",
|
|
46
46
|
"verify_prereg",
|
|
47
|
+
"evaluation_card_hash",
|
|
48
|
+
"verify_evaluation_card",
|
|
47
49
|
"beacon_audit_challenge",
|
|
48
50
|
"canonicalize_statement",
|
|
49
51
|
"statement_content_root",
|
|
@@ -64,6 +66,15 @@ __all__ = [
|
|
|
64
66
|
"verify_mldsa",
|
|
65
67
|
"verify_slhdsa",
|
|
66
68
|
"verify_hybrid",
|
|
69
|
+
"EvidenceLevel",
|
|
70
|
+
"classify_digest_evidence",
|
|
71
|
+
"classify_receiver_corroboration",
|
|
72
|
+
"evidence_ladder_summary",
|
|
73
|
+
"evidence_ladder_best",
|
|
74
|
+
"VerificationBudget",
|
|
75
|
+
"DEFAULT_BUDGET",
|
|
76
|
+
"BudgetExceeded",
|
|
77
|
+
"automation_summary",
|
|
67
78
|
"VerificationResult",
|
|
68
79
|
"Check",
|
|
69
80
|
"ProofBundleError",
|
|
@@ -89,6 +100,8 @@ _LAZY = {
|
|
|
89
100
|
"audit_challenge": ".persample",
|
|
90
101
|
"prereg_hash": ".prereg",
|
|
91
102
|
"verify_prereg": ".prereg",
|
|
103
|
+
"evaluation_card_hash": ".evalcard",
|
|
104
|
+
"verify_evaluation_card": ".evalcard",
|
|
92
105
|
"beacon_audit_challenge": ".beacon",
|
|
93
106
|
"canonicalize_statement": ".canonical",
|
|
94
107
|
"statement_content_root": ".canonical",
|
|
@@ -109,6 +122,11 @@ _LAZY = {
|
|
|
109
122
|
"verify_mldsa": ".pqsig",
|
|
110
123
|
"verify_slhdsa": ".pqsig",
|
|
111
124
|
"verify_hybrid": ".pqsig",
|
|
125
|
+
"EvidenceLevel": ".assurance", "classify_digest_evidence": ".assurance",
|
|
126
|
+
"classify_receiver_corroboration": ".assurance", "evidence_ladder_summary": ".assurance",
|
|
127
|
+
"evidence_ladder_best": ".assurance",
|
|
128
|
+
"VerificationBudget": ".budget", "DEFAULT_BUDGET": ".budget", "BudgetExceeded": ".budget",
|
|
129
|
+
"automation_summary": ".automation_verdict",
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime stays lazy
|
|
@@ -129,7 +147,12 @@ if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime s
|
|
|
129
147
|
evaluate_renewal_policy, last_ats, renew_hashtree, renew_timestamp,
|
|
130
148
|
verify_sequence)
|
|
131
149
|
from .pqsig import verify_hybrid, verify_mldsa, verify_slhdsa
|
|
150
|
+
from .assurance import (EvidenceLevel, classify_digest_evidence, classify_receiver_corroboration,
|
|
151
|
+
evidence_ladder_best, evidence_ladder_summary)
|
|
152
|
+
from .budget import DEFAULT_BUDGET, BudgetExceeded, VerificationBudget
|
|
153
|
+
from .automation_verdict import automation_summary
|
|
132
154
|
from .prereg import prereg_hash, verify_prereg
|
|
155
|
+
from .evalcard import evaluation_card_hash, verify_evaluation_card
|
|
133
156
|
from .statuslist import verify_status_snapshot
|
|
134
157
|
from .tlogproof import verify_tlog_proof
|
|
135
158
|
from .merkle import verify_consistency, verify_inclusion
|
|
@@ -49,7 +49,30 @@ def _reject_duplicate_keys(pairs: list) -> dict:
|
|
|
49
49
|
return obj
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def
|
|
52
|
+
def _enforce_node_budget(obj: Any, json_nodes: int) -> None:
|
|
53
|
+
"""Bounded iterative walk (crypto-review 2026-07-15): refuse a PARSED structure whose combined
|
|
54
|
+
dict-key + list-item count exceeds ``json_nodes`` — a wide-but-small-bytes document that slips under the
|
|
55
|
+
raw ``input_bytes`` cap. Aborts the moment the ceiling is passed, so it never walks the whole of an
|
|
56
|
+
over-budget document; the raw cap already bounds this walk's own worst case. Raises
|
|
57
|
+
:class:`proofbundle.budget.BudgetExceeded` (a ``ProofBundleError`` subclass), fail-closed."""
|
|
58
|
+
from .budget import BudgetExceeded # noqa: PLC0415 - local import avoids an import cycle
|
|
59
|
+
count = 0
|
|
60
|
+
stack = [obj]
|
|
61
|
+
while stack:
|
|
62
|
+
cur = stack.pop()
|
|
63
|
+
if isinstance(cur, dict):
|
|
64
|
+
count += len(cur)
|
|
65
|
+
if count > json_nodes:
|
|
66
|
+
raise BudgetExceeded("json_nodes", count, json_nodes)
|
|
67
|
+
stack.extend(cur.values())
|
|
68
|
+
elif isinstance(cur, list):
|
|
69
|
+
count += len(cur)
|
|
70
|
+
if count > json_nodes:
|
|
71
|
+
raise BudgetExceeded("json_nodes", count, json_nodes)
|
|
72
|
+
stack.extend(cur)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def loads_strict(text: Union[str, bytes], *, budget: Any = None) -> Any:
|
|
53
76
|
"""``json.loads`` that rejects duplicate object keys at any nesting depth.
|
|
54
77
|
|
|
55
78
|
Raises :class:`BundleFormatError` for a duplicate key (fail-closed, clear message), maps
|
|
@@ -58,9 +81,24 @@ def loads_strict(text: Union[str, bytes]) -> Any:
|
|
|
58
81
|
with more than ``sys.get_int_max_str_digits()`` digits (CWE-674 / CVE-2020-10735) to it too —
|
|
59
82
|
never a raw traceback — mirroring :func:`proofbundle.bundle.load_bundle`. Ordinary JSON syntax
|
|
60
83
|
errors keep raising ``ValueError`` (``json.JSONDecodeError``) so existing ``except (ValueError,
|
|
61
|
-
...)`` handling at the call sites stays correct.
|
|
84
|
+
...)`` handling at the call sites stays correct.
|
|
85
|
+
|
|
86
|
+
DoS backstop (Finding 15b, crypto-review 2026-07-15): this is the ONE parse chokepoint every verify
|
|
87
|
+
path funnels through, so the resource caps live here rather than at each of ~10 call sites. The raw
|
|
88
|
+
``text`` is refused BEFORE parsing when it exceeds ``budget.input_bytes`` (``json.loads`` cost scales
|
|
89
|
+
with input size, so an unbounded parse of a 50 MB envelope is a real pre-loop DoS the downstream
|
|
90
|
+
signature/list caps cannot reach), and the PARSED structure is refused when its combined dict-key +
|
|
91
|
+
list-item count exceeds ``budget.json_nodes``. Both raise :class:`proofbundle.budget.BudgetExceeded`
|
|
92
|
+
(a ``ProofBundleError`` subclass, so existing ``except (ProofBundleError, ...)`` sites treat it as
|
|
93
|
+
fail-closed malformed/over-limit input). ``budget`` defaults to ``DEFAULT_BUDGET``; pass a tighter one
|
|
94
|
+
to test the guard."""
|
|
95
|
+
from .budget import DEFAULT_BUDGET # noqa: PLC0415 - local import avoids an import cycle
|
|
96
|
+
b = budget if budget is not None else DEFAULT_BUDGET
|
|
97
|
+
if len(text) > b.input_bytes:
|
|
98
|
+
from .budget import BudgetExceeded # noqa: PLC0415
|
|
99
|
+
raise BudgetExceeded("input_bytes", len(text), b.input_bytes)
|
|
62
100
|
try:
|
|
63
|
-
|
|
101
|
+
obj = json.loads(text, object_pairs_hook=_reject_duplicate_keys)
|
|
64
102
|
except RecursionError as exc:
|
|
65
103
|
raise BundleFormatError("JSON nesting is too deep") from exc
|
|
66
104
|
except ValueError as exc:
|
|
@@ -70,3 +108,5 @@ def loads_strict(text: Union[str, bytes]) -> Any:
|
|
|
70
108
|
if "integer string conversion" in str(exc):
|
|
71
109
|
raise BundleFormatError("JSON integer literal is implausibly long (fail-closed)") from exc
|
|
72
110
|
raise
|
|
111
|
+
_enforce_node_budget(obj, b.json_nodes)
|
|
112
|
+
return obj
|
|
@@ -14,6 +14,17 @@ Design notes (verified against framework source, 2026-07):
|
|
|
14
14
|
- The hash is over the config's JSON, prefixed with a domain tag, hex sha256. It is provenance
|
|
15
15
|
metadata (traceability), NOT a security commitment — it is not salted and reveals structure;
|
|
16
16
|
it exists so two receipts from the same config are linkable and a changed config is visible.
|
|
17
|
+
|
|
18
|
+
**Benchmark-hacking VISIBILITY (additive).** `run_attempts`/`aborted_runs`/`methodology_sha256`/
|
|
19
|
+
`benchjack_audit_report_sha256` make retry/best-of-many patterns and the methodology behind a
|
|
20
|
+
result *visible* in the signed claim. They are honest metadata, NEVER a cryptographic guarantee
|
|
21
|
+
against a gamed benchmark — `eval_evidence_class` already separates `methodology` from
|
|
22
|
+
`score_evidence` (a receipt never judges whether the suite is well designed), and THREAT_MODEL.md
|
|
23
|
+
states the same boundary for benchmark-hacking explicitly (BenchJack, arXiv:2605.12673): crypto
|
|
24
|
+
cannot prove benchmark truth, only that these numbers are what was signed. The two digest fields
|
|
25
|
+
are plain sha256 references an auditor re-hashes by hand — same mechanism, same epistemic
|
|
26
|
+
strength as `prereg_sha256`/`evaluation_card_sha256` (a match proves only "this is the document
|
|
27
|
+
the issuer pointed at", never that the document is honest or complete).
|
|
17
28
|
"""
|
|
18
29
|
|
|
19
30
|
from __future__ import annotations
|
|
@@ -48,11 +59,19 @@ def config_hash(config) -> Optional[str]:
|
|
|
48
59
|
|
|
49
60
|
|
|
50
61
|
def add_provenance(provenance: dict, *, run_id=None, config=None, log_timestamp=None,
|
|
51
|
-
config_hash_value: Optional[str] = None
|
|
62
|
+
config_hash_value: Optional[str] = None,
|
|
63
|
+
run_attempts: Optional[int] = None, aborted_runs: Optional[int] = None,
|
|
64
|
+
methodology_sha256: Optional[str] = None,
|
|
65
|
+
benchjack_audit_report_sha256: Optional[str] = None) -> dict:
|
|
52
66
|
"""Merge the standard traceability fields into a provenance dict, skipping absent ones.
|
|
53
67
|
|
|
54
68
|
``config_hash_value`` lets a caller pass a precomputed hash (e.g. over already-canonical
|
|
55
|
-
material) instead of a config object; otherwise ``config`` is hashed here.
|
|
69
|
+
material) instead of a config object; otherwise ``config`` is hashed here.
|
|
70
|
+
|
|
71
|
+
``run_attempts``/``aborted_runs`` (non-negative integers) and ``methodology_sha256``/
|
|
72
|
+
``benchjack_audit_report_sha256`` (plain sha256 hex references) are additive VISIBILITY-only
|
|
73
|
+
fields for benchmark-hacking transparency — see the module docstring; they carry no
|
|
74
|
+
verification semantics here (no gate calls this a "guarantee")."""
|
|
56
75
|
if run_id:
|
|
57
76
|
provenance["run_id"] = str(run_id)
|
|
58
77
|
if log_timestamp is not None:
|
|
@@ -60,4 +79,13 @@ def add_provenance(provenance: dict, *, run_id=None, config=None, log_timestamp=
|
|
|
60
79
|
ch = config_hash_value if config_hash_value is not None else config_hash(config)
|
|
61
80
|
if ch:
|
|
62
81
|
provenance["config_hash"] = ch
|
|
82
|
+
for name, value in (("run_attempts", run_attempts), ("aborted_runs", aborted_runs)):
|
|
83
|
+
if value is not None:
|
|
84
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
85
|
+
raise ValueError(f"{name} must be a non-negative integer, got {value!r}")
|
|
86
|
+
provenance[name] = value
|
|
87
|
+
if methodology_sha256 is not None:
|
|
88
|
+
provenance["methodology_sha256"] = str(methodology_sha256)
|
|
89
|
+
if benchjack_audit_report_sha256 is not None:
|
|
90
|
+
provenance["benchjack_audit_report_sha256"] = str(benchjack_audit_report_sha256)
|
|
63
91
|
return provenance
|
|
@@ -57,6 +57,12 @@ def verify_markovian(proof: bytes, canonical_root: bytes, *, frozen: dict,
|
|
|
57
57
|
# which wallet/merkle_root was committed; BundleFormatError keeps the never-raise contract)
|
|
58
58
|
try:
|
|
59
59
|
from ._strict_json import loads_strict # noqa: PLC0415
|
|
60
|
+
from .budget import DEFAULT_BUDGET, BudgetExceeded # noqa: PLC0415
|
|
61
|
+
# DoS (crypto-review 2026-07-15): cap the raw proof BEFORE .decode(), mirroring anchors_chia's
|
|
62
|
+
# len(proof) > _MAX_PROOF_BYTES guard (loads_strict caps the decoded str, but the decode itself is
|
|
63
|
+
# otherwise unbounded). BudgetExceeded is caught below and reported as a clean _fail, never raised.
|
|
64
|
+
if len(proof) > DEFAULT_BUDGET.input_bytes:
|
|
65
|
+
raise BudgetExceeded("input_bytes", len(proof), DEFAULT_BUDGET.input_bytes)
|
|
60
66
|
env = loads_strict(proof.decode("utf-8"))
|
|
61
67
|
if not isinstance(env, dict):
|
|
62
68
|
raise ValueError("envelope is not a JSON object")
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""EvidenceLevel — a uniform, orderable strength ladder over the digest-presence-only 'proven'/'bound'
|
|
2
|
+
verify-time checks (2026-07 verify-layer hardening, Finding 03, additive/non-breaking).
|
|
3
|
+
|
|
4
|
+
WURZEL: ``action_outcome_proven`` (decision.py), ``outcome_execution_proven`` (outcome.py), and the
|
|
5
|
+
``evidence_bound`` shape check in decision.py all stop at "does a syntactically valid sha256 digest
|
|
6
|
+
OBJECT exist at this field" (``_is_digest``) — a 64-hex string of AN ATTACKER'S CHOOSING satisfies it as
|
|
7
|
+
readily as the digest of the real referenced artifact; none of them checks the digest against actual
|
|
8
|
+
resolved bytes. ``decision.resolve_evidence_ref`` already exists to go further (it checks a digest against
|
|
9
|
+
ACTUAL bytes an offline caller supplies), but no ``verify_*`` path ever calls it — the deeper evidence
|
|
10
|
+
primitive is built, never wired.
|
|
11
|
+
|
|
12
|
+
This module makes the STRENGTH of a 'proven'/'bound' claim explicit and orderable, WITHOUT changing the
|
|
13
|
+
existing boolean ``*_proven``/``evidence_bound`` fields (they stay, unchanged, for backward compatibility)
|
|
14
|
+
— each ``verify_*`` function gains ADDITIVE, more precise field(s) that classify a claim onto this ladder.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import enum
|
|
19
|
+
import re
|
|
20
|
+
from typing import Any, Callable, Optional
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"EvidenceLevel", "EVIDENCE_LEVEL_NAMES", "classify_digest_evidence",
|
|
24
|
+
"classify_receiver_corroboration",
|
|
25
|
+
"evidence_ladder_summary", "evidence_ladder_best", "EFFECT_OBSERVED_NOT_IMPLEMENTED",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
_SHA256_HEX = re.compile(r"^[0-9a-f]{64}$")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EvidenceLevel(enum.IntEnum):
|
|
32
|
+
"""Ordered ladder from a bare claim to full effect observation (No-Overclaim, Finding 03). Higher is
|
|
33
|
+
strictly stronger; compare/sort with the plain ``int``/``IntEnum`` ordering."""
|
|
34
|
+
|
|
35
|
+
CLAIMED = 0 # a value is asserted but not even shaped as a digest
|
|
36
|
+
REFERENCE_WELL_FORMED = 1 # a syntactically valid sha256 digest OBJECT is present (the old
|
|
37
|
+
# *_proven==True / evidence_bound==True bar — attacker-choosable content)
|
|
38
|
+
CONTENT_RESOLVED = 2 # the digest was checked against ACTUALLY RESOLVED bytes
|
|
39
|
+
# (mirrors decision.resolve_evidence_ref's content_root_ok)
|
|
40
|
+
RECEIPT_CRYPTO_VERIFIED = 3 # the resolved content is ITSELF a cryptographically verified receipt
|
|
41
|
+
POLICY_AUTHORIZED = 4 # a trust policy additionally authorizes the claim (signer/role pinned)
|
|
42
|
+
INDEPENDENTLY_ATTESTED = 5 # a THIRD PARTY (not the original claimant) attests the same content
|
|
43
|
+
EFFECT_OBSERVED = 6 # the real-world EFFECT itself was observed, not merely a receipt about it
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
EVIDENCE_LEVEL_NAMES: tuple[str, ...] = tuple(level.name for level in EvidenceLevel)
|
|
47
|
+
|
|
48
|
+
# EFFECT_OBSERVED is structurally UNREACHABLE from this module alone (Finding 16 — real-world effect
|
|
49
|
+
# observation, e.g. a monitored side channel confirming the outcome actually happened in the world, not
|
|
50
|
+
# merely that a receipt about it was signed/resolved). No verify_* path in this repo can compute it today.
|
|
51
|
+
# Making that explicit here — rather than silently never emitting it — is itself the honest No-Fake point;
|
|
52
|
+
# a caller grepping for "EFFECT_OBSERVED" finds this marker, not silence.
|
|
53
|
+
#
|
|
54
|
+
# Finding 16 UPDATE (receiver/observer corroboration, self-fixable part): the SELF-FIXABLE portion of
|
|
55
|
+
# Finding 16 IS now built — outcome.py's optional `receiverRefs` + `classify_receiver_corroboration` below
|
|
56
|
+
# make INDEPENDENTLY_ATTESTED (level 5, "a THIRD PARTY attests the same content") reachable when a
|
|
57
|
+
# receiver/observer's own signed acknowledgement is resolved and verified as coming from a party distinct
|
|
58
|
+
# from the executor. EFFECT_OBSERVED (level 6) stays UNREACHABLE even then — a signed receiver receipt is
|
|
59
|
+
# still a RECEIPT ABOUT the effect, never a live-monitored observation of the real-world effect itself; that
|
|
60
|
+
# is Finding 16's honestly-documented INHERENT limit (proofbundle cannot itself make a third-party system
|
|
61
|
+
# sign anything — real-world side-channel monitoring is ecosystem adoption outside this repo).
|
|
62
|
+
EFFECT_OBSERVED_NOT_IMPLEMENTED = (
|
|
63
|
+
"EvidenceLevel.EFFECT_OBSERVED is not reachable by any verify_* path in this repo (Finding 16's "
|
|
64
|
+
"self-fixable receiver-corroboration part now reaches INDEPENDENTLY_ATTESTED; EFFECT_OBSERVED itself "
|
|
65
|
+
"still needs a real-world effect-observation channel, which is an inherent, not-yet-built limit outside "
|
|
66
|
+
"proofbundle's own control) — TODO, tracked, not silently absent."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_digest(obj: Any) -> bool:
|
|
71
|
+
return isinstance(obj, dict) and isinstance(obj.get("sha256"), str) and bool(_SHA256_HEX.match(obj["sha256"]))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def classify_digest_evidence(digest_obj: Any, *, applicable: bool = True,
|
|
75
|
+
evidence_resolver: Optional[Callable[[Any], bool]] = None) -> dict:
|
|
76
|
+
"""Classify ONE digest-bound field (e.g. an ``effectDigest``, a ``decisionRef``, one
|
|
77
|
+
``evidenceRefs[]`` entry) onto the :class:`EvidenceLevel` ladder. Never raises; a malformed input
|
|
78
|
+
classifies as ``CLAIMED``, it never crashes the caller.
|
|
79
|
+
|
|
80
|
+
``applicable=False`` (e.g. ``status != 'executed'``) -> ``level=None`` (not applicable, mirrors the
|
|
81
|
+
existing ``*_proven=None`` convention: a non-applicable claim is not a WEAK claim, it is not a claim
|
|
82
|
+
at all).
|
|
83
|
+
|
|
84
|
+
``evidence_resolver``, when supplied, is called with ``digest_obj`` and must return True iff the
|
|
85
|
+
digest was checked against the ACTUAL resolved bytes (mirrors ``resolve_evidence_ref``'s
|
|
86
|
+
``content_root_ok``); on True the level reaches ``CONTENT_RESOLVED``, never higher —
|
|
87
|
+
``RECEIPT_CRYPTO_VERIFIED``/``POLICY_AUTHORIZED``/``INDEPENDENTLY_ATTESTED`` are each a STRONGER claim
|
|
88
|
+
this classifier does not itself verify (conflating "checked against real bytes" with "the real bytes'
|
|
89
|
+
OWN signature was checked" would be exactly the kind of unearned strength bump No-Overclaim forbids).
|
|
90
|
+
A raising/exception-throwing ``evidence_resolver`` is treated as False (fail-closed: an exception is
|
|
91
|
+
not evidence, never silently promoted).
|
|
92
|
+
"""
|
|
93
|
+
if not applicable:
|
|
94
|
+
return {"level": None, "level_name": None, "detail": "not applicable"}
|
|
95
|
+
if not _is_digest(digest_obj):
|
|
96
|
+
return {"level": EvidenceLevel.CLAIMED, "level_name": EvidenceLevel.CLAIMED.name,
|
|
97
|
+
"detail": "no well-formed sha256 digest object present"}
|
|
98
|
+
level = EvidenceLevel.REFERENCE_WELL_FORMED
|
|
99
|
+
detail = "a well-formed sha256 digest object is present (attacker-choosable content, not content-checked)"
|
|
100
|
+
if evidence_resolver is not None:
|
|
101
|
+
try:
|
|
102
|
+
resolved = bool(evidence_resolver(digest_obj))
|
|
103
|
+
except Exception: # noqa: BLE001 - fail-closed: a raising resolver proves nothing
|
|
104
|
+
resolved = False
|
|
105
|
+
if resolved:
|
|
106
|
+
level = EvidenceLevel.CONTENT_RESOLVED
|
|
107
|
+
detail = "digest checked against actually-resolved content bytes"
|
|
108
|
+
return {"level": level, "level_name": level.name, "detail": detail}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def classify_receiver_corroboration(digest_obj: Any, *, applicable: bool = True,
|
|
112
|
+
evidence_resolver: Optional[Callable[[Any], bool]] = None,
|
|
113
|
+
independent_attestation_resolver: Optional[Callable[[Any], bool]] = None,
|
|
114
|
+
executor_key_id: Optional[str] = None,
|
|
115
|
+
receiver_key_id: Optional[str] = None,
|
|
116
|
+
) -> dict:
|
|
117
|
+
"""Classify a receiver/observer corroboration ref (Finding 16, additive) ONE STEP BEYOND
|
|
118
|
+
:func:`classify_digest_evidence` — reaches ``EvidenceLevel.INDEPENDENTLY_ATTESTED`` when
|
|
119
|
+
``independent_attestation_resolver`` confirms the referenced content is ITSELF a validly-signed
|
|
120
|
+
statement from a party DISTINCT from the original claimant (e.g. a receiver's or observer's own
|
|
121
|
+
DSSE-signed acknowledgement of an Action Outcome) — never merely a resolved digest, which is
|
|
122
|
+
:func:`classify_digest_evidence`'s own documented ceiling (its docstring: "RECEIPT_CRYPTO_VERIFIED /
|
|
123
|
+
POLICY_AUTHORIZED / INDEPENDENTLY_ATTESTED are each a STRONGER claim this classifier does not itself
|
|
124
|
+
verify").
|
|
125
|
+
|
|
126
|
+
The three-tier informal ladder a caller might reach for here — SELF_ASSERTED / DIGEST_REFERENCED /
|
|
127
|
+
RECEIVER_CORROBORATED — maps onto this module's EXISTING orderable :class:`EvidenceLevel` rather than
|
|
128
|
+
a new competing enum (CLAIMED/REFERENCE_WELL_FORMED ≈ SELF_ASSERTED/DIGEST_REFERENCED,
|
|
129
|
+
INDEPENDENTLY_ATTESTED ≈ RECEIVER_CORROBORATED — "a THIRD PARTY attests the same content" is exactly
|
|
130
|
+
what a receiver/observer corroboration IS).
|
|
131
|
+
|
|
132
|
+
Never raises: a raising ``independent_attestation_resolver`` is fail-closed (treated as False, the base
|
|
133
|
+
``classify_digest_evidence`` level is kept — never silently promoted, mirrors the existing
|
|
134
|
+
``evidence_resolver`` contract). The resolver is only ever consulted once the digest has ALREADY reached
|
|
135
|
+
at least ``CONTENT_RESOLVED`` — an attacker-choosable digest that was never resolved cannot be promoted
|
|
136
|
+
straight to INDEPENDENTLY_ATTESTED by a permissive attestation resolver alone.
|
|
137
|
+
|
|
138
|
+
STRUCTURAL independence (crypto-review, 2026-07-15): "INDEPENDENTLY_ATTESTED" means the corroborating
|
|
139
|
+
statement is from a party DISTINCT from the executor/claimant. proofbundle asserts this only when it can
|
|
140
|
+
PROVE it: a receiver reaches INDEPENDENTLY_ATTESTED ONLY IF BOTH ``executor_key_id`` AND
|
|
141
|
+
``receiver_key_id`` are present AND they differ. An ABSENT ``executor_key_id`` blocks promotion just as
|
|
142
|
+
an absent/equal receiver key id does — the executor authors and signs its own outcome predicate and
|
|
143
|
+
``executor.keyId`` is schema-optional, so a one-sided check (fire only when executor_key_id is supplied)
|
|
144
|
+
would be trivially evaded by simply omitting one's own keyId. Without knowing BOTH parties' key ids
|
|
145
|
+
proofbundle cannot show they differ, so it does not claim independence (fail-closed to the base level).
|
|
146
|
+
|
|
147
|
+
INHERENT limit (honestly not closed here): two DISTINCT key ids can still belong to the SAME real-world
|
|
148
|
+
principal (an executor using a second key it also controls). proofbundle cannot bind a key id to a
|
|
149
|
+
real-world identity on its own — that is exactly what the ``outcomeReceivers`` Trust Pack role provides
|
|
150
|
+
(``outcome.receiver_trusted_by_role``: a curated list of trusted, genuinely-independent receiver keys).
|
|
151
|
+
So key-id distinctness here is the STRUCTURAL floor; principal-level independence needs that out-of-band
|
|
152
|
+
trust binding."""
|
|
153
|
+
base = classify_digest_evidence(digest_obj, applicable=applicable, evidence_resolver=evidence_resolver)
|
|
154
|
+
if base["level"] is None or base["level"] < EvidenceLevel.CONTENT_RESOLVED or independent_attestation_resolver is None:
|
|
155
|
+
return base
|
|
156
|
+
# Provable distinctness: to ASSERT independence, BOTH key ids must be present, be STRINGS, AND differ.
|
|
157
|
+
# The isinstance(str) guards close a type-confusion evasion (crypto-review 2026-07-15): a non-str
|
|
158
|
+
# receiver_key_id (e.g. ["kid-exec"]) is `!= "kid-exec"` in Python, so a bare `==` distinctness check
|
|
159
|
+
# would read a wrapped copy of the executor's OWN id as "distinct". An absent/non-str/equal key id is
|
|
160
|
+
# self-corroboration that cannot be shown independent -> fail-closed, no promotion.
|
|
161
|
+
if not isinstance(executor_key_id, str) or not isinstance(receiver_key_id, str) or receiver_key_id == executor_key_id:
|
|
162
|
+
return {**base, "detail": base["detail"] + " (independence not provable: executor and receiver key "
|
|
163
|
+
"ids must both be present and differ; an absent/equal key id is self-corroboration — "
|
|
164
|
+
"principal-level independence for two distinct keys needs the outcomeReceivers trust role)"}
|
|
165
|
+
try:
|
|
166
|
+
attested = bool(independent_attestation_resolver(digest_obj))
|
|
167
|
+
except Exception: # noqa: BLE001 - fail-closed: a raising resolver proves nothing
|
|
168
|
+
attested = False
|
|
169
|
+
if not attested:
|
|
170
|
+
return base
|
|
171
|
+
return {"level": EvidenceLevel.INDEPENDENTLY_ATTESTED,
|
|
172
|
+
"level_name": EvidenceLevel.INDEPENDENTLY_ATTESTED.name,
|
|
173
|
+
"detail": "the referenced content is itself a validly-signed statement from a party distinct "
|
|
174
|
+
"from the original claimant (receiver/observer corroboration)"}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def evidence_ladder_summary(*fields: dict) -> dict:
|
|
178
|
+
"""Roll several :func:`classify_digest_evidence` results into ONE summary using AND semantics: a chain
|
|
179
|
+
of evidence is only as strong as its WEAKEST applicable link (e.g. ``decision.py``'s
|
|
180
|
+
``evidenceRefs[]`` — ``evidence_bound`` is only meaningful when EVERY ref is bound). Non-applicable
|
|
181
|
+
(``level=None``) fields are ignored, never silently counted as CLAIMED. When no field is applicable,
|
|
182
|
+
returns ``level=None`` (mirrors the existing ``evidence_bound=None`` "nothing to bind" convention —
|
|
183
|
+
never a vacuous strong verdict over an empty set)."""
|
|
184
|
+
applicable = [f for f in fields if f.get("level") is not None]
|
|
185
|
+
if not applicable:
|
|
186
|
+
return {"level": None, "level_name": None, "fields": list(fields)}
|
|
187
|
+
weakest = min(applicable, key=lambda f: f["level"])
|
|
188
|
+
return {"level": weakest["level"], "level_name": weakest["level_name"], "fields": list(fields)}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def evidence_ladder_best(*fields: dict) -> dict:
|
|
192
|
+
"""Roll several :func:`classify_digest_evidence` results into ONE summary using OR semantics: only ONE
|
|
193
|
+
of several alternative digest fields needs to hold for the claim to be satisfied (e.g.
|
|
194
|
+
``outcome.py``'s ``effectDigest`` OR ``actualActionDigest`` — the existing boolean
|
|
195
|
+
``outcome_execution_proven`` is exactly this OR). Picks the STRONGEST applicable field. When no field
|
|
196
|
+
is applicable, returns ``level=None``."""
|
|
197
|
+
applicable = [f for f in fields if f.get("level") is not None]
|
|
198
|
+
if not applicable:
|
|
199
|
+
return {"level": None, "level_name": None, "fields": list(fields)}
|
|
200
|
+
strongest = max(applicable, key=lambda f: f["level"])
|
|
201
|
+
return {"level": strongest["level"], "level_name": strongest["level_name"], "fields": list(fields)}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""automation_summary — a uniform, additive automation-safety verdict layered on top of any proofbundle
|
|
2
|
+
``verify_*`` result dict (2026-07 verify-layer hardening, Finding 01).
|
|
3
|
+
|
|
4
|
+
WURZEL: ``bundle.py::root_authenticity_summary`` already computes a ``safeForAutomation`` /
|
|
5
|
+
``automationBlockers`` verdict for the core evidence bundle — it exists ONLY there. The other five
|
|
6
|
+
receipt-chain predicates (``decision.py``, ``outcome.py``, ``trust_pack.py``, ``verification_summary.py``,
|
|
7
|
+
``run_ledger.py``) each compute their OWN aggregate ``ok`` using an ``is not False`` pattern over their
|
|
8
|
+
optional/not-applicable checks (documented, intentional: ``None`` = "not requested, passes"). That is the
|
|
9
|
+
RIGHT default for ``ok`` (a caller who never asked for a policy check should not be told the receipt is
|
|
10
|
+
somehow invalid) — but it is the WRONG bar for an AUTOMATION decision: a caller who filters on ``ok`` alone
|
|
11
|
+
can walk away believing a receipt was policy-authorized when ``policy_ok`` was actually ``None``
|
|
12
|
+
(never evaluated), because ``None is not False`` is ``True``.
|
|
13
|
+
|
|
14
|
+
``automation_summary`` does NOT change any existing ``ok`` field (additive, no breaking default flip — see
|
|
15
|
+
CHANGELOG "Unreleased" for the ONE deliberately-deferred breaking piece, the ``bundle.py`` CLI exit-code
|
|
16
|
+
default). It computes a SEPARATE, stricter verdict: ``safeForAutomation`` is true only when policy IS
|
|
17
|
+
``True`` (never merely "not False"), mirroring ``bundle.py``'s own ``policy_ok is True`` bar (P0-B, audit
|
|
18
|
+
2026-07-13). Each of the five ``verify_*`` functions stashes this at ``result["automation"]`` — the old
|
|
19
|
+
``result["ok"]`` field is untouched.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
24
|
+
|
|
25
|
+
__all__ = ["automation_summary", "AUTOMATION_BLOCKER_REASONS"]
|
|
26
|
+
|
|
27
|
+
# The human-legible reason for each automationBlockers enum value (mirrors bundle.py's
|
|
28
|
+
# AUTOMATION_BLOCKER_REASONS — kept here, next to the blocker logic, so the two can never drift apart).
|
|
29
|
+
AUTOMATION_BLOCKER_REASONS = {
|
|
30
|
+
"CRYPTO_NOT_OK": "The cryptographic (DSSE / threshold-signature) verdict is not true",
|
|
31
|
+
"STRUCTURE_NOT_OK": "The predicate structure did not fully validate",
|
|
32
|
+
"POLICY_NOT_EVALUATED": "No trust policy / authorization gate was evaluated for this predicate type",
|
|
33
|
+
"POLICY_FAILED": "The supplied trust policy / authorization gate was not satisfied",
|
|
34
|
+
"REFERENCES_NOT_RESOLVED": "One or more referenced/bound artifacts did not resolve (see the "
|
|
35
|
+
"predicate's own *_ok / *_bound / *_intact fields for which)",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _tri(result: Mapping[str, Any], key: Optional[str]) -> Optional[bool]:
|
|
40
|
+
if key is None:
|
|
41
|
+
return None
|
|
42
|
+
value = result.get(key)
|
|
43
|
+
return None if value is None else bool(value)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def automation_summary(result: Mapping[str, Any], *, required_checks: Mapping[str, Any]) -> dict:
|
|
47
|
+
"""Build a uniform automation-safety verdict from a ``verify_*`` result dict.
|
|
48
|
+
|
|
49
|
+
``required_checks`` maps the four canonical automation dimensions to the ACTUAL field name(s) in
|
|
50
|
+
``result`` that decide them for THIS predicate type (field names differ per ``verify_*`` function):
|
|
51
|
+
|
|
52
|
+
``"crypto"`` -- str, the crypto/signature verdict field (e.g. ``"crypto_ok"``,
|
|
53
|
+
``"root_threshold_met"``).
|
|
54
|
+
``"structure"`` -- str, the structural verdict field (e.g. ``"structure_ok"``).
|
|
55
|
+
``"policy"`` -- str or ``None``. When a str, the field is treated the SAME way
|
|
56
|
+
``bundle.py::root_authenticity_summary`` treats ``policy_ok``: safe requires the
|
|
57
|
+
field to be ``True`` EXACTLY (``is True``), never merely ``is not False`` --
|
|
58
|
+
``None`` (not evaluated) yields ``POLICY_NOT_EVALUATED``, never a silent pass.
|
|
59
|
+
When ``None``, this predicate type carries no policy/authorization layer at all
|
|
60
|
+
-- the policy dimension is reported ``None`` (not applicable) and never blocks
|
|
61
|
+
``safeForAutomation``.
|
|
62
|
+
``"references"`` -- a sequence of field names whose values, when EXPLICITLY ``False``, mean a
|
|
63
|
+
referenced/bound artifact did not resolve (e.g. ``decision_bound``,
|
|
64
|
+
``evidence_bound``, ``chain_intact``). ``None`` entries (not applicable / not
|
|
65
|
+
requested) never block.
|
|
66
|
+
|
|
67
|
+
Returns ``{"cryptoValid", "structureValid", "policyAuthorized", "referencesResolved",
|
|
68
|
+
"safeForAutomation", "automationBlockers"}``. This function is PURE (no side effects on ``result``);
|
|
69
|
+
the caller is responsible for stashing the return value at ``result["automation"]``.
|
|
70
|
+
"""
|
|
71
|
+
crypto_key = required_checks.get("crypto")
|
|
72
|
+
structure_key = required_checks.get("structure")
|
|
73
|
+
policy_key = required_checks.get("policy")
|
|
74
|
+
reference_keys: Sequence[str] = required_checks.get("references") or ()
|
|
75
|
+
|
|
76
|
+
crypto_ok = _tri(result, crypto_key)
|
|
77
|
+
structure_ok = _tri(result, structure_key)
|
|
78
|
+
policy_val = result.get(policy_key) if policy_key is not None else None
|
|
79
|
+
unresolved = [name for name in reference_keys if result.get(name) is False]
|
|
80
|
+
|
|
81
|
+
blockers: list[str] = []
|
|
82
|
+
if crypto_ok is not True:
|
|
83
|
+
blockers.append("CRYPTO_NOT_OK")
|
|
84
|
+
if structure_ok is not True:
|
|
85
|
+
blockers.append("STRUCTURE_NOT_OK")
|
|
86
|
+
if policy_key is not None:
|
|
87
|
+
if policy_val is None:
|
|
88
|
+
blockers.append("POLICY_NOT_EVALUATED")
|
|
89
|
+
elif policy_val is not True:
|
|
90
|
+
blockers.append("POLICY_FAILED")
|
|
91
|
+
if unresolved:
|
|
92
|
+
blockers.append("REFERENCES_NOT_RESOLVED")
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
"cryptoValid": crypto_ok,
|
|
96
|
+
"structureValid": structure_ok,
|
|
97
|
+
"policyAuthorized": None if policy_key is None else (policy_val is True),
|
|
98
|
+
"referencesResolved": None if not reference_keys else not unresolved,
|
|
99
|
+
"safeForAutomation": not blockers,
|
|
100
|
+
"automationBlockers": blockers,
|
|
101
|
+
}
|