proofbundle 3.2.3__tar.gz → 3.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {proofbundle-3.2.3/src/proofbundle.egg-info → proofbundle-3.3.0}/PKG-INFO +7 -5
- {proofbundle-3.2.3 → proofbundle-3.3.0}/README.md +6 -4
- {proofbundle-3.2.3 → proofbundle-3.3.0}/pyproject.toml +1 -1
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/__init__.py +1 -1
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/automation_verdict.py +6 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/cli.py +66 -4
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/decision.py +75 -4
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/outcome.py +40 -5
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policy.py +27 -1
- proofbundle-3.3.0/src/proofbundle/relation.py +319 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0/src/proofbundle.egg-info}/PKG-INFO +7 -5
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle.egg-info/SOURCES.txt +3 -0
- proofbundle-3.3.0/tests/test_relation_profile.py +736 -0
- proofbundle-3.3.0/tests/test_relation_property.py +110 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_schema_parity.py +61 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/LICENSE +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/setup.cfg +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/_integration.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/_strict_json.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/__init__.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/_provenance.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/eee.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/promptfoo.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/samples.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/anchors.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/anchors_chia.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/anchors_chia_add.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/anchors_markovian.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/anchors_ots.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/anchors_rfc3161.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/assurance.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/beacon.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/budget.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/bundle.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/canonical.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/checkpoint.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/demo.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/dsse.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/emit.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/errors.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/evalcard.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/evalclaim.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/evidence_pack.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/experimental/__init__.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/experimental/enclave.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/hashalg.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/hf_evals.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/intoto.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/kbjwt.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/merkle.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/persample.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policies/decision-receipt-template-v1.json +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policies/research-preview-v1.json +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policies/strict-eval-authenticated-root-template-v1.json +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policies/strict-eval-template-v1.json +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policies/strict-prereg-template-v1.json +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policy_profiles.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/pqsig.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/prereg.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/public_transparency.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/py.typed +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/renewal.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/run_ledger.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/sdjwt.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/sdjwt_issue.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/sdjwt_vc.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/signature.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/statuslist.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/subject_binding.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/tlogproof.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/trust_pack.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/verification_summary.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle.egg-info/requires.txt +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_adapters.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_adversarial.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchor_hardening_321.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchor_longevity_property.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchor_target_trustedtime.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_chia.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_chia_add.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_chia_claims.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_generic.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_markovian.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_ots.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_rfc3161.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_assurance.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_automation_verdict.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_beacon.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_branch_base_check.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_budget.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_bundle.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_canonical.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_checkpoint.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_checkpoint_external_vectors.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_checkpoint_quorum_property.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_claims_hygiene.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_cli.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_cli_eval.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_cli_require_anchor.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_conformance.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_content_root_property.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_cosignature.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_anchors.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_cli.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_evidence.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_fuzz.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_hardening.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_policy.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_schema.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_validator_api.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_verify.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_demo.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_docs_truth.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_dsse_adversarial.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_dup_key_reject.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_ed25519_semantics.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_eee.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_emit.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_enclave_assurance.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_eval_evidence_class.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_evalcard.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_evalclaim.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_evidence_pack.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_examples.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_experimental_enclave.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_fork_pr_secret_isolation.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_fuzz_parsers.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_hashalg.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_hf_entry_verify_eee_digest.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_hf_evals.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_inspect_hook.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_claims_hygiene.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_content_root_migration.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_eval_result.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_examples.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_spec_diff.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_svr.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_kbjwt.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_lens_review_fixes_3_1_3.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_merkle.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_merkle_consistency_property.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_merkle_property.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_mldsa_acvp_vectors.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_official_vectors.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_ok_semantics.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_ots_external_vectors.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_outcome_receiver_corroboration.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_outcome_verify.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_oversized_int_guard.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_persample.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_pinned_key_validation.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_policy_explain_lint.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_policy_lifecycle_purpose.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_policy_profiles.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_policy_templates.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_pqsig.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_predicate_type_enforcement.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_prereg.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_promptfoo.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_provenance.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_public_transparency.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_rekor_interop.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_release_integrity_gate.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_renewal.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_renewal_external_token_glue.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_renewal_policy.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_renewal_signed.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_root_authenticity.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_run_ledger.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_rust_parity_gate.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_schema.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_adversarial.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_duplicate_cnf.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_resource_budget.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_vc.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_verify_binding.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwtvc_external_vectors.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_signature.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_statuslist.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_subject_binding.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_tlogproof.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_tree_context_authenticity.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_trust_pack.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_trust_pack_crypto_agility.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_trust_pack_outcome_receivers_role.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_trust_policy.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_verification_summary.py +0 -0
- {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_verify_matrix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofbundle
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT.
|
|
5
5
|
Author: Konrad Gruszka
|
|
6
6
|
License: MIT
|
|
@@ -72,7 +72,7 @@ Merkle, one file, no server, no network.
|
|
|
72
72
|
[](https://pypi.org/project/proofbundle/)
|
|
73
73
|
[](https://pypi.org/project/proofbundle/)
|
|
74
74
|
[](https://github.com/b7n0de/proofbundle/blob/main/LICENSE)
|
|
75
|
-
[](https://doi.org/10.5281/zenodo.21110642)
|
|
76
76
|
[](https://github.com/astral-sh/ruff)
|
|
77
77
|
[](https://github.com/b7n0de/proofbundle/blob/main/scripts/mutation_check.py)
|
|
78
78
|
[](https://pypi.org/project/proofbundle/#files)
|
|
@@ -211,9 +211,11 @@ a preprint.
|
|
|
211
211
|
## Cite this work
|
|
212
212
|
|
|
213
213
|
If proofbundle helped your evaluation pipeline, please cite it. Machine-readable metadata is in
|
|
214
|
-
[`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff)
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
[`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff). The archival software record
|
|
215
|
+
is on Zenodo under concept DOI [10.5281/zenodo.21110642](https://doi.org/10.5281/zenodo.21110642); the
|
|
216
|
+
Technical Note (design write-up) under concept DOI
|
|
217
|
+
[10.5281/zenodo.21230466](https://doi.org/10.5281/zenodo.21230466), also linked from
|
|
218
|
+
[b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
|
|
217
219
|
|
|
218
220
|
## Post-quantum posture (honest, two layers)
|
|
219
221
|
|
|
@@ -18,7 +18,7 @@ Merkle, one file, no server, no network.
|
|
|
18
18
|
[](https://pypi.org/project/proofbundle/)
|
|
19
19
|
[](https://pypi.org/project/proofbundle/)
|
|
20
20
|
[](https://github.com/b7n0de/proofbundle/blob/main/LICENSE)
|
|
21
|
-
[](https://doi.org/10.5281/zenodo.21110642)
|
|
22
22
|
[](https://github.com/astral-sh/ruff)
|
|
23
23
|
[](https://github.com/b7n0de/proofbundle/blob/main/scripts/mutation_check.py)
|
|
24
24
|
[](https://pypi.org/project/proofbundle/#files)
|
|
@@ -157,9 +157,11 @@ a preprint.
|
|
|
157
157
|
## Cite this work
|
|
158
158
|
|
|
159
159
|
If proofbundle helped your evaluation pipeline, please cite it. Machine-readable metadata is in
|
|
160
|
-
[`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff)
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
[`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff). The archival software record
|
|
161
|
+
is on Zenodo under concept DOI [10.5281/zenodo.21110642](https://doi.org/10.5281/zenodo.21110642); the
|
|
162
|
+
Technical Note (design write-up) under concept DOI
|
|
163
|
+
[10.5281/zenodo.21230466](https://doi.org/10.5281/zenodo.21230466), also linked from
|
|
164
|
+
[b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
|
|
163
165
|
|
|
164
166
|
## Post-quantum posture (honest, two layers)
|
|
165
167
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proofbundle"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.3.0"
|
|
8
8
|
description = "Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
15
|
|
|
16
|
-
__version__ = "3.
|
|
16
|
+
__version__ = "3.3.0"
|
|
17
17
|
|
|
18
18
|
# The `proofbundle/v0.1` normative spec revision this build implements — kept in sync with the
|
|
19
19
|
# `Revision:` line at the top of SPEC.md by tests/test_docs_truth.py (WP-B1, closes #28). Bump
|
|
@@ -33,6 +33,12 @@ AUTOMATION_BLOCKER_REASONS = {
|
|
|
33
33
|
"POLICY_FAILED": "The supplied trust policy / authorization gate was not satisfied",
|
|
34
34
|
"REFERENCES_NOT_RESOLVED": "One or more referenced/bound artifacts did not resolve (see the "
|
|
35
35
|
"predicate's own *_ok / *_bound / *_intact fields for which)",
|
|
36
|
+
# relation/v0.1 (EXPERIMENTAL): raised by the DECISION verify path when the trust
|
|
37
|
+
# policy's relations section is violated (the outcome-path policy gate is a documented
|
|
38
|
+
# follow-up — verify_outcome_receipt has no policy parameter today) (a named relation did not resolve, or an attached,
|
|
39
|
+
# verified successor supersedes the receipt under rejectSuperseded). LIVE, not dormant.
|
|
40
|
+
"LINEAGE_REQUIREMENT_FAILED": "The trust policy's lineage requirement was not met (relations "
|
|
41
|
+
"section: unresolved required relation or superseded receipt)",
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
|
|
@@ -1207,6 +1207,39 @@ def _cmd_decision_emit(args: argparse.Namespace) -> int:
|
|
|
1207
1207
|
return 0
|
|
1208
1208
|
|
|
1209
1209
|
|
|
1210
|
+
def _load_related(paths, pub: bytes) -> tuple[dict, list[str]]:
|
|
1211
|
+
"""relation/v0.1 (--with-related): load DSSE envelopes of RELATED receipts, verify each one
|
|
1212
|
+
STANDALONE under the SAME relying-party key, and key it by its content root. Same-key is the
|
|
1213
|
+
honest v0.1 CLI contract (it matches the strictest relationSigner policy value); per-target
|
|
1214
|
+
keys are a documented follow-up. A file that fails to load is a usage error (exit 2 at the
|
|
1215
|
+
caller) — an envelope that loads but does NOT verify is kept with verified=False, so an
|
|
1216
|
+
attached-but-wrong target FAILS lineage instead of vanishing."""
|
|
1217
|
+
from . import anchors as _anchors_mod # noqa: PLC0415
|
|
1218
|
+
from . import dsse as _dsse # noqa: PLC0415
|
|
1219
|
+
related: dict = {}
|
|
1220
|
+
errs: list[str] = []
|
|
1221
|
+
for path in paths or []:
|
|
1222
|
+
try:
|
|
1223
|
+
with open(path, encoding="utf-8") as handle:
|
|
1224
|
+
env = loads_strict(handle.read()) # WP-C1: duplicate keys rejected
|
|
1225
|
+
body = _dsse.load_payload(env)
|
|
1226
|
+
root_hex = _anchors_mod.statement_content_root(body).hex()
|
|
1227
|
+
# L3-audit fix: inside the try so a malformed-envelope error names the offending file too.
|
|
1228
|
+
verified = bool(_dsse.verify_envelope(env, pub, payload_type="application/vnd.in-toto+json"))
|
|
1229
|
+
except (ProofBundleError, OSError, ValueError) as exc:
|
|
1230
|
+
errs.append(f"cannot read --with-related {path}: {exc}")
|
|
1231
|
+
continue
|
|
1232
|
+
rels = None
|
|
1233
|
+
try:
|
|
1234
|
+
stmt = loads_strict(body.decode("utf-8"))
|
|
1235
|
+
if isinstance(stmt, dict) and isinstance(stmt.get("predicate"), dict):
|
|
1236
|
+
rels = stmt["predicate"].get("relationships")
|
|
1237
|
+
except (ProofBundleError, ValueError):
|
|
1238
|
+
rels = None
|
|
1239
|
+
related[root_hex] = {"verified": verified, "relationships": rels}
|
|
1240
|
+
return related, errs
|
|
1241
|
+
|
|
1242
|
+
|
|
1210
1243
|
def _cmd_decision_verify(args: argparse.Namespace) -> int:
|
|
1211
1244
|
import base64 # noqa: PLC0415
|
|
1212
1245
|
from .decision import verify_decision_receipt # noqa: PLC0415
|
|
@@ -1246,10 +1279,16 @@ def _cmd_decision_verify(args: argparse.Namespace) -> int:
|
|
|
1246
1279
|
merged = dict(pol_trust)
|
|
1247
1280
|
merged.update(rp_trust or {})
|
|
1248
1281
|
rp_trust = merged
|
|
1282
|
+
related, rel_errs = _load_related(getattr(args, "with_related", None), pub)
|
|
1283
|
+
if rel_errs:
|
|
1284
|
+
for e in rel_errs:
|
|
1285
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1286
|
+
return 2
|
|
1249
1287
|
result = verify_decision_receipt(env, pub, strict=args.strict, expected_audience=args.aud,
|
|
1250
1288
|
expected_nonce=args.nonce, policy=policy, anchors=anchors,
|
|
1251
1289
|
rp_trust=rp_trust,
|
|
1252
|
-
require_derived_subject=args.require_derived_subject
|
|
1290
|
+
require_derived_subject=args.require_derived_subject,
|
|
1291
|
+
related=related or None)
|
|
1253
1292
|
except (ProofBundleError, OSError, ValueError) as exc:
|
|
1254
1293
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
1255
1294
|
return 2
|
|
@@ -1265,7 +1304,7 @@ def _cmd_decision_verify(args: argparse.Namespace) -> int:
|
|
|
1265
1304
|
# Findings 01/03 (crypto-review X2): the uniform automation verdict + EvidenceLevel ladder are
|
|
1266
1305
|
# part of the library result; a pipeline doing `... --json | jq .automation.safeForAutomation`
|
|
1267
1306
|
# must not get null (indistinguishable from a real "not evaluated"). Emit them here too.
|
|
1268
|
-
"automation", "evidence_levels", "warnings", "errors",
|
|
1307
|
+
"automation", "evidence_levels", "lineage", "warnings", "errors",
|
|
1269
1308
|
) if k in result}
|
|
1270
1309
|
print(json.dumps(report, indent=2, default=str))
|
|
1271
1310
|
else:
|
|
@@ -1315,6 +1354,11 @@ def _cmd_decision_verify(args: argparse.Namespace) -> int:
|
|
|
1315
1354
|
# signal — fail-closed, never a silent exit 0 (fix-review Finding 3). None = no anchor supplied.
|
|
1316
1355
|
if result["anchors_ok"] is False:
|
|
1317
1356
|
return 1
|
|
1357
|
+
# relation/v0.1: a REQUESTED lineage check (edges present or --with-related supplied) that FAILs
|
|
1358
|
+
# (cycle, depth, malformed ancestor, attached-but-unverified target) is a tamper/structure signal —
|
|
1359
|
+
# fail-closed exit 2, never a silent exit 0. DECLARED_UNRESOLVED stays exit 0 (declared-only is honest).
|
|
1360
|
+
if isinstance(result.get("lineage"), dict) and result["lineage"].get("lineage") == "FAIL":
|
|
1361
|
+
return 2
|
|
1318
1362
|
if result["policy_ok"] is False:
|
|
1319
1363
|
return 3
|
|
1320
1364
|
return 0
|
|
@@ -1422,11 +1466,16 @@ def _cmd_outcome_verify(args: argparse.Namespace) -> int:
|
|
|
1422
1466
|
with open(args.envelope, encoding="utf-8") as handle:
|
|
1423
1467
|
env = loads_strict(handle.read()) # WP-C1: duplicate keys rejected
|
|
1424
1468
|
pub = base64.b64decode(args.pub)
|
|
1469
|
+
related, rel_errs = _load_related(getattr(args, "with_related", None), pub)
|
|
1470
|
+
if rel_errs:
|
|
1471
|
+
for e in rel_errs:
|
|
1472
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1473
|
+
return 2
|
|
1425
1474
|
result = verify_outcome_receipt(
|
|
1426
1475
|
env, pub, strict=args.strict,
|
|
1427
1476
|
expected_decision_ref=args.expected_decision_ref, decision_maker_id=args.decision_maker_id,
|
|
1428
1477
|
expected_audience=args.aud, expected_nonce=args.nonce,
|
|
1429
|
-
require_derived_subject=args.require_derived_subject)
|
|
1478
|
+
require_derived_subject=args.require_derived_subject, related=related or None)
|
|
1430
1479
|
except (ProofBundleError, OSError, ValueError) as exc:
|
|
1431
1480
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
1432
1481
|
return 2
|
|
@@ -1439,7 +1488,7 @@ def _cmd_outcome_verify(args: argparse.Namespace) -> int:
|
|
|
1439
1488
|
# is not silently blind to them (a jq filter on an absent key is indistinguishable from a real
|
|
1440
1489
|
# None). executor_role_trusted/receiver_* are None unless the caller supplied a trust_pack.
|
|
1441
1490
|
"automation", "evidence_levels", "executor_role_trusted", "receiver_bound", "receiver_role_trusted",
|
|
1442
|
-
"warnings", "errors",
|
|
1491
|
+
"lineage", "warnings", "errors",
|
|
1443
1492
|
) if k in result}
|
|
1444
1493
|
print(json.dumps(report, indent=2, default=str))
|
|
1445
1494
|
else:
|
|
@@ -1481,6 +1530,10 @@ def _cmd_outcome_verify(args: argparse.Namespace) -> int:
|
|
|
1481
1530
|
# fail-closed binding as --aud/--nonce — an EXTERNAL_ATTESTED subject then must not exit 0.
|
|
1482
1531
|
if result["subject_derived_ok"] is False:
|
|
1483
1532
|
return 2
|
|
1533
|
+
# relation/v0.1 (adversarial-review finding, 2026-07-16): mirror of the decision-verify gate —
|
|
1534
|
+
# a REQUESTED lineage check (--with-related / edges present) that FAILs must never exit 0.
|
|
1535
|
+
if isinstance(result.get("lineage"), dict) and result["lineage"].get("lineage") == "FAIL":
|
|
1536
|
+
return 2
|
|
1484
1537
|
return 0
|
|
1485
1538
|
|
|
1486
1539
|
|
|
@@ -1983,6 +2036,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1983
2036
|
d_verify.add_argument("--bitcoin-header", action="append", default=None, metavar="HEIGHT:MERKLEROOT_HEX",
|
|
1984
2037
|
help="WP-A1: a relying-party-supplied Bitcoin block header (internal byte order) "
|
|
1985
2038
|
"that confirms an OpenTimestamps statement anchor — frozen is never trusted")
|
|
2039
|
+
d_verify.add_argument("--with-related", dest="with_related", action="append", default=None, metavar="PATH",
|
|
2040
|
+
help="relation/v0.1 (EXPERIMENTAL): attach a RELATED receipt's DSSE envelope for "
|
|
2041
|
+
"offline lineage resolution (repeatable). Each target is verified standalone "
|
|
2042
|
+
"under the SAME --pub (same-key contract); an attached target that does not "
|
|
2043
|
+
"verify FAILS lineage — relationship declared by issuer, not a statement of "
|
|
2044
|
+
"correctness")
|
|
1986
2045
|
d_verify.add_argument("--require-derived-subject", dest="require_derived_subject", action="store_true",
|
|
1987
2046
|
help="Finding 05: fail closed (exit 2) unless the Statement subject is a DERIVED "
|
|
1988
2047
|
"commitment to the predicate (subject_binding.classify_subject) — rejects a "
|
|
@@ -2027,6 +2086,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2027
2086
|
o_verify.add_argument("--strict", action="store_true", help="enforce strict-v0.1 required fields")
|
|
2028
2087
|
o_verify.add_argument("--aud", default=None, help="expected audience (checks validity.audience against replay)")
|
|
2029
2088
|
o_verify.add_argument("--nonce", default=None, help="expected nonce (checks validity.nonce against replay)")
|
|
2089
|
+
o_verify.add_argument("--with-related", dest="with_related", action="append", default=None, metavar="PATH",
|
|
2090
|
+
help="relation/v0.1 (EXPERIMENTAL): attach a RELATED receipt's DSSE envelope for "
|
|
2091
|
+
"offline lineage resolution (repeatable; same-key contract as decision verify)")
|
|
2030
2092
|
o_verify.add_argument("--require-derived-subject", dest="require_derived_subject", action="store_true",
|
|
2031
2093
|
help="Finding 05: fail closed (exit 2) unless the Statement subject is a DERIVED "
|
|
2032
2094
|
"commitment to the predicate (subject_binding.classify_subject) — rejects a "
|
|
@@ -48,7 +48,7 @@ _REQUIRED_STRICT = _REQUIRED_ALWAYS + ("notChecked", "decisionChangeConditions",
|
|
|
48
48
|
# prereg anchor) are referenced indirectly via `evidenceRefs`. An `anchors` field inside the predicate is
|
|
49
49
|
# therefore a fail-closed unknown-field error.
|
|
50
50
|
_OPTIONAL = ("recordedAt", "delegationRefs", "actionOutcome", "traceContext", "validity",
|
|
51
|
-
"notChecked", "decisionChangeConditions", "privacy")
|
|
51
|
+
"notChecked", "decisionChangeConditions", "privacy", "relationships")
|
|
52
52
|
_ALLOWED_TOP = set(_REQUIRED_ALWAYS) | set(_OPTIONAL)
|
|
53
53
|
|
|
54
54
|
# Time-bearing paths that, if present, MUST be RFC3339-Z.
|
|
@@ -254,6 +254,16 @@ def validate_decision_predicate(predicate: Any, *, strict: bool = False) -> list
|
|
|
254
254
|
elif strict and isinstance(priv, dict) and not isinstance(priv.get("rawInputsIncluded"), bool):
|
|
255
255
|
errors.append("privacy.rawInputsIncluded (boolean) is required in strict mode")
|
|
256
256
|
|
|
257
|
+
# relationships (optional, relation/v0.1 EXPERIMENTAL): typed, SIGNED lineage edges to earlier
|
|
258
|
+
# receipts — inside the predicate so the existing DSSE signature covers them (unlike detached
|
|
259
|
+
# `anchors`). Edge closure/enums/digest shape are enforced by relation.validate_relationships
|
|
260
|
+
# (fail-closed, incl. per-edge additionalProperties:false), so the block is NOT walked by the
|
|
261
|
+
# generic nested-closure below.
|
|
262
|
+
if "relationships" in predicate:
|
|
263
|
+
from .relation import validate_relationships
|
|
264
|
+
errors.extend(f"relationships: {e}" if not e.startswith("relationships") else e
|
|
265
|
+
for e in validate_relationships(predicate.get("relationships")))
|
|
266
|
+
|
|
257
267
|
# Nested schema closure (Finding 04): additionalProperties:false at the TOP level does not, by itself,
|
|
258
268
|
# close these nested objects — an undeclared key inside decision/policyBoundary/proposedAction/
|
|
259
269
|
# decisionMaker/evidenceRefs[] (and their sub-objects) previously rode along silently.
|
|
@@ -395,6 +405,9 @@ def _empty_result() -> dict:
|
|
|
395
405
|
# (assurance.classify_digest_evidence) computed at the end of verify — never gate anything on
|
|
396
406
|
# these two here, the old ok/*_proven/evidence_bound fields above are UNCHANGED for compat.
|
|
397
407
|
"automation": None, "evidence_levels": None,
|
|
408
|
+
# relation/v0.1 (EXPERIMENTAL, additive): lineage verdict over the predicate's OPTIONAL
|
|
409
|
+
# relationships edges — None until computed over AUTHENTICATED bytes; never gates `ok`.
|
|
410
|
+
"lineage": None, "lineage_requirement_failed": None, "lineage_ok": None,
|
|
398
411
|
"warnings": [], "errors": [],
|
|
399
412
|
}
|
|
400
413
|
|
|
@@ -403,7 +416,8 @@ def verify_decision_receipt(envelope: dict, public_key: bytes, *, strict: bool =
|
|
|
403
416
|
expected_audience: str | None = None, expected_nonce: str | None = None,
|
|
404
417
|
policy: dict | None = None, anchors: list | None = None,
|
|
405
418
|
rp_trust: dict | None = None, require_derived_subject: bool = False,
|
|
406
|
-
evidence_resolver: Callable[[dict], bool] | None = None
|
|
419
|
+
evidence_resolver: Callable[[dict], bool] | None = None,
|
|
420
|
+
related: dict | None = None) -> dict:
|
|
407
421
|
"""Verify a DSSE-signed Decision Receipt. Crypto first, then structure over the EXACT signed bytes (never
|
|
408
422
|
re-serialized). Returns the snake_case structured result; each check independent, non-applicable = None.
|
|
409
423
|
|
|
@@ -535,6 +549,33 @@ def verify_decision_receipt(envelope: dict, public_key: bytes, *, strict: bool =
|
|
|
535
549
|
"actionOutcome.outcomeRef": _outcome_level,
|
|
536
550
|
"evidenceRefs": _evref_levels,
|
|
537
551
|
}
|
|
552
|
+
|
|
553
|
+
# relation/v0.1 (EXPERIMENTAL, additive): evaluate the OPTIONAL relationships edges against
|
|
554
|
+
# caller-attached targets (`related`, offline — the CLI's --with-related). Computed ONLY over
|
|
555
|
+
# authenticated bytes (this block), NEVER feeds `ok`/crypto (lattice monotonicity); a lineage
|
|
556
|
+
# FAIL surfaces via errors[] and the policy layer, not by flipping the crypto verdict.
|
|
557
|
+
if "relationships" in predicate or related:
|
|
558
|
+
from . import anchors as _anchors_for_rel # noqa: PLC0415
|
|
559
|
+
from .relation import successor_warning, verify_relationship_edges # noqa: PLC0415
|
|
560
|
+
try:
|
|
561
|
+
_subject_hex = _anchors_for_rel.statement_content_root(body).hex()
|
|
562
|
+
except Exception:
|
|
563
|
+
_subject_hex = None
|
|
564
|
+
r["lineage"] = verify_relationship_edges(
|
|
565
|
+
predicate.get("relationships"), related, subject_hex=_subject_hex)
|
|
566
|
+
# Advisory by default; the policy's reject_superseded turns it into a blocker below.
|
|
567
|
+
_sw = successor_warning(predicate.get("relationships"), related, subject_hex=_subject_hex)
|
|
568
|
+
r["lineage"]["supersededByAttached"] = _sw
|
|
569
|
+
if _sw:
|
|
570
|
+
r["warnings"].append(f"lineage: {_sw}")
|
|
571
|
+
if r["lineage"]["lineage"] == "FAIL":
|
|
572
|
+
r["errors"].extend(r["lineage"]["errors"] or ["relation: lineage verification FAILED"])
|
|
573
|
+
# No-Fake (6-Linsen-Audit L2, 2026-07-16): a REQUESTED lineage check that FAILs must be
|
|
574
|
+
# visible in the aggregate `ok` and `automation` — like every other error category in this
|
|
575
|
+
# result — not only at the CLI exit code. lineage_ok is False ONLY on FAIL (DECLARED_UNRESOLVED
|
|
576
|
+
# and NOT_EVALUATED are honest non-fails, stay None). This tightens the verdict downward; it
|
|
577
|
+
# NEVER lifts it (crypto_ok is untouched, lattice monotonicity preserved).
|
|
578
|
+
r["lineage_ok"] = False if r["lineage"]["lineage"] == "FAIL" else None
|
|
538
579
|
# 3.1.2 fail-closed fix (audit 2026-07-13, sibling of the eval-path F4 hardening and the decision
|
|
539
580
|
# template/expiry gates): a relying party who supplies expected_audience/expected_nonce is ASKING for
|
|
540
581
|
# RFC-9901-§7.3-style replay/audience binding. If the receipt carries NO validity object (or a
|
|
@@ -662,6 +703,27 @@ def verify_decision_receipt(envelope: dict, public_key: bytes, *, strict: bool =
|
|
|
662
703
|
"decision_receipt.trusted_decision_makers) — POLICY: OK then proves integrity by an "
|
|
663
704
|
"UNKNOWN signer. Pin the expected decision-maker key(s).")
|
|
664
705
|
|
|
706
|
+
# relation/v0.1 policy gate (relations section, LIVE): a violation fails policy_ok (exit-3 class)
|
|
707
|
+
# and raises the dedicated automation blocker LINEAGE_REQUIREMENT_FAILED below — it NEVER touches
|
|
708
|
+
# the crypto verdict (lattice monotonicity). require_relation_resolution is conditional on
|
|
709
|
+
# presence: a named relation that appears as an edge MUST be VERIFIED (attached + standalone-
|
|
710
|
+
# verified); an absent relation is no violation.
|
|
711
|
+
if policy is not None and isinstance(policy.get("relations"), dict) and r["crypto_ok"]:
|
|
712
|
+
_rel_pol = policy["relations"]
|
|
713
|
+
_lin = r.get("lineage") or {}
|
|
714
|
+
_viol = []
|
|
715
|
+
_req = _rel_pol.get("require_relation_resolution") or []
|
|
716
|
+
for _e in (_lin.get("edges") or []):
|
|
717
|
+
if _e.get("relation") in _req and _e.get("resolution") != "VERIFIED":
|
|
718
|
+
_viol.append(f"relation {_e.get('relation')!r} must resolve (target attached and "
|
|
719
|
+
f"verified), got {_e.get('resolution')}")
|
|
720
|
+
if _rel_pol.get("reject_superseded") and _lin.get("supersededByAttached"):
|
|
721
|
+
_viol.append(f"reject_superseded: {_lin['supersededByAttached']}")
|
|
722
|
+
if _viol:
|
|
723
|
+
r["policy_ok"] = False
|
|
724
|
+
r["lineage_requirement_failed"] = True
|
|
725
|
+
r["errors"].extend("LINEAGE_REQUIREMENT_FAILED: " + v for v in _viol)
|
|
726
|
+
|
|
665
727
|
# Aggregate verdict: authenticated AND well-structured AND no applicable trust check FAILED.
|
|
666
728
|
# None means not-applicable (passes); only an explicit False fails the aggregate.
|
|
667
729
|
r["ok"] = bool(
|
|
@@ -669,13 +731,22 @@ def verify_decision_receipt(envelope: dict, public_key: bytes, *, strict: bool =
|
|
|
669
731
|
and r["policy_ok"] is not False and r["signer_trusted"] is not False
|
|
670
732
|
and r["audience_ok"] is not False and r["nonce_ok"] is not False
|
|
671
733
|
and r["evidence_bound"] is not False and r["anchors_ok"] is not False
|
|
672
|
-
and r["subject_derived_ok"] is not False)
|
|
734
|
+
and r["subject_derived_ok"] is not False and r["lineage_ok"] is not False)
|
|
673
735
|
|
|
674
736
|
# Finding 01 (additive): the STRICTER automation-safety verdict — policy_ok must be True, never merely
|
|
675
737
|
# "not False" — never changes `ok` above.
|
|
676
738
|
from .automation_verdict import automation_summary # noqa: PLC0415
|
|
677
739
|
r["automation"] = automation_summary(r, required_checks={
|
|
678
740
|
"crypto": "crypto_ok", "structure": "structure_ok", "policy": "policy_ok",
|
|
679
|
-
"references": ["evidence_bound", "audience_ok", "nonce_ok", "anchors_ok", "subject_derived_ok"
|
|
741
|
+
"references": ["evidence_bound", "audience_ok", "nonce_ok", "anchors_ok", "subject_derived_ok",
|
|
742
|
+
"lineage_ok"],
|
|
680
743
|
})
|
|
744
|
+
# relation/v0.1: the dedicated blocker name (LIVE, not dormant) — POLICY_FAILED already fires via
|
|
745
|
+
# policy_ok=False above; this names the REASON so a consumer can distinguish a lineage gate from
|
|
746
|
+
# any other policy failure. Only ever ADDS a blocker (never turns safeForAutomation true).
|
|
747
|
+
if r.get("lineage_requirement_failed") and isinstance(r.get("automation"), dict):
|
|
748
|
+
blockers = r["automation"].setdefault("automationBlockers", [])
|
|
749
|
+
if "LINEAGE_REQUIREMENT_FAILED" not in blockers:
|
|
750
|
+
blockers.append("LINEAGE_REQUIREMENT_FAILED")
|
|
751
|
+
r["automation"]["safeForAutomation"] = False
|
|
681
752
|
return r
|
|
@@ -45,7 +45,10 @@ _OPTIONAL = ("actualActionDigest", "responseDigest", "effectDigest", "recordedAt
|
|
|
45
45
|
"traceContext", "limitations", "validity",
|
|
46
46
|
# Finding 16 (self-fixable part, additive): receiverRefs / sequence — see the module-level
|
|
47
47
|
# note above `_OUTCOME_RECEIVER_ROLE` for what each closes and its honest, documented limit.
|
|
48
|
-
"receiverRefs", "sequence"
|
|
48
|
+
"receiverRefs", "sequence",
|
|
49
|
+
# relation/v0.1 (EXPERIMENTAL, 3.3.0): typed signed lineage edges — validated fail-closed
|
|
50
|
+
# by relation.validate_relationships (incl. per-edge closure), not by the generic walker.
|
|
51
|
+
"relationships")
|
|
49
52
|
_ALLOWED_TOP = set(_REQUIRED_ALWAYS) | set(_OPTIONAL)
|
|
50
53
|
|
|
51
54
|
_DIGEST_FIELDS = ("requestedActionDigest", "actualActionDigest", "responseDigest", "effectDigest")
|
|
@@ -186,6 +189,14 @@ def validate_outcome_predicate(predicate: Any, *, strict: bool = False) -> list[
|
|
|
186
189
|
if "validity" in predicate and not isinstance(val, dict):
|
|
187
190
|
errors.append("validity must be an object")
|
|
188
191
|
|
|
192
|
+
# relationships (optional, relation/v0.1 EXPERIMENTAL): typed, SIGNED lineage edges — inside the
|
|
193
|
+
# predicate so the DSSE signature covers them; closure/enums/digests enforced fail-closed by the
|
|
194
|
+
# relation module itself.
|
|
195
|
+
if "relationships" in predicate:
|
|
196
|
+
from .relation import validate_relationships
|
|
197
|
+
errors.extend(f"relationships: {e}" if not e.startswith("relationships") else e
|
|
198
|
+
for e in validate_relationships(predicate.get("relationships")))
|
|
199
|
+
|
|
189
200
|
# Nested schema closure (Finding 04): additionalProperties:false at the TOP level does not, by itself,
|
|
190
201
|
# close traceContext/validity — an undeclared key inside either previously rode along silently.
|
|
191
202
|
errors.extend(nested_closure_violations(predicate, _NESTED_ALLOWED))
|
|
@@ -422,7 +433,7 @@ def _empty_result() -> dict:
|
|
|
422
433
|
# a non-empty receiverRefs are supplied. Neither is wired into the aggregate `ok` (receiverRefs is
|
|
423
434
|
# OPTIONAL supplementary evidence, not core to the outcome's own validity — see verify docstring).
|
|
424
435
|
"receiver_bound": None, "receiver_role_trusted": None,
|
|
425
|
-
"warnings": [], "errors": [],
|
|
436
|
+
"lineage": None, "lineage_ok": None, "warnings": [], "errors": [],
|
|
426
437
|
}
|
|
427
438
|
|
|
428
439
|
|
|
@@ -431,7 +442,8 @@ def verify_outcome_receipt(envelope: dict, public_key: bytes, *, strict: bool =
|
|
|
431
442
|
expected_audience: str | None = None, expected_nonce: str | None = None,
|
|
432
443
|
require_derived_subject: bool = False, trust_pack: dict | None = None,
|
|
433
444
|
evidence_resolver: Callable[[dict], bool] | None = None,
|
|
434
|
-
receiver_attestation_resolver: Callable[[dict], bool] | None = None
|
|
445
|
+
receiver_attestation_resolver: Callable[[dict], bool] | None = None,
|
|
446
|
+
related: dict | None = None) -> dict:
|
|
435
447
|
"""Verify a DSSE-signed Outcome Receipt. Crypto first, then structure over the EXACT signed bytes.
|
|
436
448
|
|
|
437
449
|
Outcome-specific fail-closed checks (each applies only after crypto passes; non-applicable = None):
|
|
@@ -547,6 +559,28 @@ def verify_outcome_receipt(envelope: dict, public_key: bytes, *, strict: bool =
|
|
|
547
559
|
"role separation violated — executor.id equals the decisionMaker id; whoever decides "
|
|
548
560
|
"must not witness their own execution (fail-closed)")
|
|
549
561
|
|
|
562
|
+
# relation/v0.1 (EXPERIMENTAL, additive): evaluate the OPTIONAL relationships edges against
|
|
563
|
+
# caller-attached targets (offline --with-related). Only over AUTHENTICATED bytes; NEVER feeds
|
|
564
|
+
# the crypto verdict (lattice monotonicity) — a lineage FAIL surfaces via errors[] + policy.
|
|
565
|
+
if "relationships" in predicate or related:
|
|
566
|
+
from . import anchors as _anchors_for_rel # noqa: PLC0415
|
|
567
|
+
from .relation import successor_warning, verify_relationship_edges # noqa: PLC0415
|
|
568
|
+
try:
|
|
569
|
+
_subject_hex = _anchors_for_rel.statement_content_root(body).hex()
|
|
570
|
+
except Exception:
|
|
571
|
+
_subject_hex = None
|
|
572
|
+
r["lineage"] = verify_relationship_edges(
|
|
573
|
+
predicate.get("relationships"), related, subject_hex=_subject_hex)
|
|
574
|
+
_sw = successor_warning(predicate.get("relationships"), related, subject_hex=_subject_hex)
|
|
575
|
+
r["lineage"]["supersededByAttached"] = _sw
|
|
576
|
+
if _sw:
|
|
577
|
+
r["warnings"].append(f"lineage: {_sw}")
|
|
578
|
+
if r["lineage"]["lineage"] == "FAIL":
|
|
579
|
+
r["errors"].extend(r["lineage"]["errors"] or ["relation: lineage verification FAILED"])
|
|
580
|
+
# No-Fake (6-Linsen-Audit L2): mirror of the decision path — a REQUESTED lineage FAIL is
|
|
581
|
+
# visible in `ok`/`automation`, not only at the CLI exit code. FAIL->False, else None.
|
|
582
|
+
r["lineage_ok"] = False if r["lineage"]["lineage"] == "FAIL" else None
|
|
583
|
+
|
|
550
584
|
# execution proof (honesty limit, warning not hard-fail).
|
|
551
585
|
r["execution_proven"] = outcome_execution_proven(predicate)
|
|
552
586
|
if r["execution_proven"] is False:
|
|
@@ -675,7 +709,8 @@ def verify_outcome_receipt(envelope: dict, public_key: bytes, *, strict: bool =
|
|
|
675
709
|
# Finding 01 (additive, backward compatible): None (no trust_pack supplied, the pre-existing
|
|
676
710
|
# default for every caller) passes exactly like every other optional check above; only an explicit
|
|
677
711
|
# False (a trust_pack WAS supplied and the executor is not a trusted role member) fails ok.
|
|
678
|
-
and r["executor_role_trusted"] is not False
|
|
712
|
+
and r["executor_role_trusted"] is not False
|
|
713
|
+
and r["lineage_ok"] is not False)
|
|
679
714
|
|
|
680
715
|
# Finding 01 (additive): the STRICTER automation-safety verdict — never changes `ok` above. Outcome has
|
|
681
716
|
# no separate trust-policy layer (yet); executor_role_trusted (the outcomeExecutors role gate) is the
|
|
@@ -684,6 +719,6 @@ def verify_outcome_receipt(envelope: dict, public_key: bytes, *, strict: bool =
|
|
|
684
719
|
r["automation"] = automation_summary(r, required_checks={
|
|
685
720
|
"crypto": "crypto_ok", "structure": "structure_ok", "policy": "executor_role_trusted",
|
|
686
721
|
"references": ["decision_bound", "role_separation_ok", "audience_ok", "nonce_ok",
|
|
687
|
-
"subject_derived_ok"],
|
|
722
|
+
"subject_derived_ok", "lineage_ok"],
|
|
688
723
|
})
|
|
689
724
|
return r
|
|
@@ -105,7 +105,7 @@ def _pinned_key_forgeable(b64: str) -> bool:
|
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
_TOP_KEYS = {"schema", "policy_id", "allowed_schema_versions", "allowed_issuers", "signature",
|
|
108
|
-
"merkle", "sd_jwt", "status", "assurance", "decision_receipt", "anchors",
|
|
108
|
+
"merkle", "sd_jwt", "status", "assurance", "decision_receipt", "anchors", "relations",
|
|
109
109
|
"deploymentReady", "requiresIdentityOverlay", # AP-2 §6.2 template metadata
|
|
110
110
|
"valid_until", # AP-2 §6.4 lifecycle expiry
|
|
111
111
|
"valid_from", # A-P0-2 §6 lifecycle not-before
|
|
@@ -135,6 +135,11 @@ def _parse_iso_utc(s):
|
|
|
135
135
|
_ANCHORS_KEYS = {"require_anchor", "require_anchor_target", "allow_pending",
|
|
136
136
|
"trusted_tsa_roots", "bitcoin_block_headers", "trusted_tsa_policy_oids"}
|
|
137
137
|
_ANCHOR_TARGETS = ("receipt", "preRegistration", "statement")
|
|
138
|
+
# relation/v0.1 (EXPERIMENTAL, 3.3.0): the lineage policy section. relation_signer is a documented
|
|
139
|
+
# FOLLOW-UP (the CLI --with-related contract is same-key today; a pinned-set needs per-target key
|
|
140
|
+
# plumbing) — an unknown key here is fail-closed, so a future field cannot silently no-op.
|
|
141
|
+
_RELATIONS_KEYS = {"require_relation_resolution", "reject_superseded"}
|
|
142
|
+
_RELATION_NAMES = ("supersedes", "revises", "corrects", "retracts", "renews", "derivedFrom", "amends")
|
|
138
143
|
_DECISION_KEYS = {"trusted_decision_makers", "allowed_decision_types", "allowed_verdicts",
|
|
139
144
|
"required_evidence_relations", "accepted_predicate_types", "require_policy_digest",
|
|
140
145
|
"require_external_anchor", "allow_pending", "require_audience", "require_nonce",
|
|
@@ -410,6 +415,19 @@ def load_policy(source: Union[str, dict]) -> dict:
|
|
|
410
415
|
for k, v in hdrs.items()):
|
|
411
416
|
raise PolicyError("anchors.bitcoin_block_headers must map a decimal height string to a "
|
|
412
417
|
"64-char hex merkle root")
|
|
418
|
+
if "relations" in policy:
|
|
419
|
+
# relation/v0.1 lineage requirements (v0.2-gated like decision_receipt/anchors).
|
|
420
|
+
if policy.get("schema") != POLICY_SCHEMA_V0_2:
|
|
421
|
+
raise PolicyError("relations section requires schema proofbundle/trust-policy/v0.2")
|
|
422
|
+
rel = _require_dict(policy["relations"], "relations")
|
|
423
|
+
_reject_unknown(rel, _RELATIONS_KEYS, "relations")
|
|
424
|
+
if "require_relation_resolution" in rel:
|
|
425
|
+
rr = rel["require_relation_resolution"]
|
|
426
|
+
if (not isinstance(rr, list) or not rr
|
|
427
|
+
or not all(isinstance(x, str) and x in _RELATION_NAMES for x in rr)):
|
|
428
|
+
raise PolicyError("relations.require_relation_resolution must be a non-empty list of "
|
|
429
|
+
f"relation names out of {list(_RELATION_NAMES)}")
|
|
430
|
+
_require_bool(rel, "reject_superseded", "relations")
|
|
413
431
|
if "decision_receipt" in policy:
|
|
414
432
|
dr = _require_dict(policy["decision_receipt"], "decision_receipt")
|
|
415
433
|
_reject_unknown(dr, _DECISION_KEYS, "decision_receipt")
|
|
@@ -977,6 +995,14 @@ def explain_policy(policy: dict) -> list:
|
|
|
977
995
|
if anc.get("allow_pending"):
|
|
978
996
|
detail += " (pending accepted)"
|
|
979
997
|
lines.append(f"external time anchor required ({detail})")
|
|
998
|
+
# relation/v0.1: the relations section is enforced by the decision verify path (a
|
|
999
|
+
# violation fails policy_ok, exit 3; the outcome-path policy gate is a documented follow-up) — listed here for explain⟺enforce parity, same rule as anchors.
|
|
1000
|
+
rel = policy.get("relations") or {}
|
|
1001
|
+
if rel.get("require_relation_resolution"):
|
|
1002
|
+
lines.append("lineage relations must resolve (target attached + verified): "
|
|
1003
|
+
+ ", ".join(rel["require_relation_resolution"]))
|
|
1004
|
+
if rel.get("reject_superseded"):
|
|
1005
|
+
lines.append("superseded receipt rejected (an attached, verified successor blocks automation)")
|
|
980
1006
|
dr = policy.get("decision_receipt") or {}
|
|
981
1007
|
if dr:
|
|
982
1008
|
active = [k for k in dr if dr.get(k)]
|