proofbundle 3.2.3__tar.gz → 3.6.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.
Files changed (218) hide show
  1. {proofbundle-3.2.3/src/proofbundle.egg-info → proofbundle-3.6.0}/PKG-INFO +11 -6
  2. {proofbundle-3.2.3 → proofbundle-3.6.0}/README.md +6 -4
  3. {proofbundle-3.2.3 → proofbundle-3.6.0}/pyproject.toml +13 -5
  4. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/__init__.py +1 -1
  5. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/anchors_chia.py +4 -0
  6. proofbundle-3.6.0/src/proofbundle/anchors_ots.py +273 -0
  7. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/automation_verdict.py +13 -0
  8. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/cli.py +625 -4
  9. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/decision.py +86 -4
  10. proofbundle-3.6.0/src/proofbundle/evidence_pack.py +162 -0
  11. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/outcome.py +67 -4
  12. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/policy.py +99 -1
  13. proofbundle-3.6.0/src/proofbundle/relation.py +452 -0
  14. proofbundle-3.6.0/src/proofbundle/relation_statement.py +319 -0
  15. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/trust_pack.py +18 -0
  16. {proofbundle-3.2.3 → proofbundle-3.6.0/src/proofbundle.egg-info}/PKG-INFO +11 -6
  17. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle.egg-info/SOURCES.txt +12 -0
  18. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle.egg-info/requires.txt +5 -1
  19. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_anchors_markovian.py +11 -3
  20. proofbundle-3.6.0/tests/test_anchors_ots.py +275 -0
  21. proofbundle-3.6.0/tests/test_audit_candidate_360.py +364 -0
  22. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_claims_hygiene.py +106 -0
  23. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_evidence_pack.py +71 -22
  24. proofbundle-3.6.0/tests/test_fuzz_soak_regression.py +114 -0
  25. proofbundle-3.6.0/tests/test_ots_calendar_hardening.py +548 -0
  26. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_policy_explain_lint.py +13 -0
  27. proofbundle-3.6.0/tests/test_relation_profile.py +736 -0
  28. proofbundle-3.6.0/tests/test_relation_property.py +110 -0
  29. proofbundle-3.6.0/tests/test_relation_signer_target_340.py +357 -0
  30. proofbundle-3.6.0/tests/test_relation_statement.py +327 -0
  31. proofbundle-3.6.0/tests/test_relation_statement_rust_parity.py +53 -0
  32. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_renewal_external_token_glue.py +12 -4
  33. proofbundle-3.6.0/tests/test_roadmap_frontload_foundations.py +159 -0
  34. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_schema_parity.py +61 -0
  35. proofbundle-3.6.0/tests/test_trust_pack_payloadtype_negatives.py +96 -0
  36. proofbundle-3.2.3/src/proofbundle/anchors_ots.py +0 -115
  37. proofbundle-3.2.3/src/proofbundle/evidence_pack.py +0 -98
  38. proofbundle-3.2.3/tests/test_anchors_ots.py +0 -132
  39. {proofbundle-3.2.3 → proofbundle-3.6.0}/LICENSE +0 -0
  40. {proofbundle-3.2.3 → proofbundle-3.6.0}/setup.cfg +0 -0
  41. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/_inspect_registry.py +0 -0
  42. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/_integration.py +0 -0
  43. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/_strict_json.py +0 -0
  44. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/adapters/__init__.py +0 -0
  45. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/adapters/_provenance.py +0 -0
  46. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/adapters/eee.py +0 -0
  47. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
  48. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/adapters/lm_eval.py +0 -0
  49. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/adapters/promptfoo.py +0 -0
  50. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/adapters/samples.py +0 -0
  51. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/anchors.py +0 -0
  52. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/anchors_chia_add.py +0 -0
  53. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/anchors_markovian.py +0 -0
  54. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/anchors_rfc3161.py +0 -0
  55. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/assurance.py +0 -0
  56. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/beacon.py +0 -0
  57. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/budget.py +0 -0
  58. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/bundle.py +0 -0
  59. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/canonical.py +0 -0
  60. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/checkpoint.py +0 -0
  61. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/demo.py +0 -0
  62. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/dsse.py +0 -0
  63. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/eee_eval_schema.json +0 -0
  64. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/emit.py +0 -0
  65. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/errors.py +0 -0
  66. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/evalcard.py +0 -0
  67. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/evalclaim.py +0 -0
  68. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/experimental/__init__.py +0 -0
  69. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/experimental/enclave.py +0 -0
  70. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/hashalg.py +0 -0
  71. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/hf_evals.py +0 -0
  72. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/inspect_hook.py +0 -0
  73. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/intoto.py +0 -0
  74. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/kbjwt.py +0 -0
  75. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/merkle.py +0 -0
  76. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/persample.py +0 -0
  77. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/policies/decision-receipt-template-v1.json +0 -0
  78. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/policies/research-preview-v1.json +0 -0
  79. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/policies/strict-eval-authenticated-root-template-v1.json +0 -0
  80. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/policies/strict-eval-template-v1.json +0 -0
  81. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/policies/strict-prereg-template-v1.json +0 -0
  82. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/policy_profiles.py +0 -0
  83. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/pqsig.py +0 -0
  84. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/prereg.py +0 -0
  85. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/public_transparency.py +0 -0
  86. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/py.typed +0 -0
  87. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/pytest_plugin.py +0 -0
  88. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/renewal.py +0 -0
  89. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/run_ledger.py +0 -0
  90. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/sdjwt.py +0 -0
  91. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/sdjwt_issue.py +0 -0
  92. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/sdjwt_vc.py +0 -0
  93. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/signature.py +0 -0
  94. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/statuslist.py +0 -0
  95. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/subject_binding.py +0 -0
  96. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/tlogproof.py +0 -0
  97. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle/verification_summary.py +0 -0
  98. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
  99. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
  100. {proofbundle-3.2.3 → proofbundle-3.6.0}/src/proofbundle.egg-info/top_level.txt +0 -0
  101. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_adapters.py +0 -0
  102. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_adversarial.py +0 -0
  103. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_anchor_hardening_321.py +0 -0
  104. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_anchor_longevity_property.py +0 -0
  105. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_anchor_target_trustedtime.py +0 -0
  106. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_anchors_chia.py +0 -0
  107. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_anchors_chia_add.py +0 -0
  108. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_anchors_chia_claims.py +0 -0
  109. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_anchors_generic.py +0 -0
  110. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_anchors_rfc3161.py +0 -0
  111. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_assurance.py +0 -0
  112. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_automation_verdict.py +0 -0
  113. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_beacon.py +0 -0
  114. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_branch_base_check.py +0 -0
  115. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_budget.py +0 -0
  116. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_bundle.py +0 -0
  117. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_bundle_robustness.py +0 -0
  118. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_canonical.py +0 -0
  119. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_checkpoint.py +0 -0
  120. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_checkpoint_external_vectors.py +0 -0
  121. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_checkpoint_quorum_property.py +0 -0
  122. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_cli.py +0 -0
  123. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_cli_eval.py +0 -0
  124. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_cli_require_anchor.py +0 -0
  125. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_conformance.py +0 -0
  126. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_content_root_property.py +0 -0
  127. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_cosignature.py +0 -0
  128. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_cosignature_mldsa.py +0 -0
  129. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_decision_anchors.py +0 -0
  130. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_decision_cli.py +0 -0
  131. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_decision_evidence.py +0 -0
  132. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_decision_fuzz.py +0 -0
  133. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_decision_hardening.py +0 -0
  134. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_decision_policy.py +0 -0
  135. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_decision_schema.py +0 -0
  136. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_decision_validator_api.py +0 -0
  137. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_decision_verify.py +0 -0
  138. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_demo.py +0 -0
  139. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_docs_truth.py +0 -0
  140. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_dsse_adversarial.py +0 -0
  141. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_dup_key_reject.py +0 -0
  142. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_ed25519_semantics.py +0 -0
  143. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_eee.py +0 -0
  144. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_emit.py +0 -0
  145. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_enclave_assurance.py +0 -0
  146. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_eval_claim_schema.py +0 -0
  147. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_eval_evidence_class.py +0 -0
  148. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_evalcard.py +0 -0
  149. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_evalclaim.py +0 -0
  150. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_examples.py +0 -0
  151. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_experimental_enclave.py +0 -0
  152. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_fork_pr_secret_isolation.py +0 -0
  153. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_fuzz_parsers.py +0 -0
  154. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_hashalg.py +0 -0
  155. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_hf_entry_verify_eee_digest.py +0 -0
  156. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_hf_evals.py +0 -0
  157. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_inspect_hook.py +0 -0
  158. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_intoto.py +0 -0
  159. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_intoto_claims_hygiene.py +0 -0
  160. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_intoto_content_root_migration.py +0 -0
  161. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_intoto_dsse.py +0 -0
  162. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_intoto_eval_result.py +0 -0
  163. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_intoto_examples.py +0 -0
  164. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_intoto_spec_diff.py +0 -0
  165. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_intoto_svr.py +0 -0
  166. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_kbjwt.py +0 -0
  167. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_lens_review_fixes_3_1_3.py +0 -0
  168. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_merkle.py +0 -0
  169. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_merkle_consistency_property.py +0 -0
  170. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_merkle_property.py +0 -0
  171. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_mldsa_acvp_vectors.py +0 -0
  172. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_official_vectors.py +0 -0
  173. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_ok_semantics.py +0 -0
  174. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_ots_external_vectors.py +0 -0
  175. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_outcome_receiver_corroboration.py +0 -0
  176. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_outcome_verify.py +0 -0
  177. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_oversized_int_guard.py +0 -0
  178. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_persample.py +0 -0
  179. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_pinned_key_validation.py +0 -0
  180. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_policy_lifecycle_purpose.py +0 -0
  181. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_policy_profiles.py +0 -0
  182. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_policy_templates.py +0 -0
  183. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_pqsig.py +0 -0
  184. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_predicate_type_enforcement.py +0 -0
  185. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_prereg.py +0 -0
  186. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_promptfoo.py +0 -0
  187. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_provenance.py +0 -0
  188. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_public_transparency.py +0 -0
  189. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_pytest_plugin.py +0 -0
  190. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_rekor_interop.py +0 -0
  191. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_release_integrity_gate.py +0 -0
  192. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_renewal.py +0 -0
  193. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_renewal_policy.py +0 -0
  194. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_renewal_signed.py +0 -0
  195. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_rfc6962_external_vectors.py +0 -0
  196. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_root_authenticity.py +0 -0
  197. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_run_ledger.py +0 -0
  198. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_rust_parity_gate.py +0 -0
  199. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_schema.py +0 -0
  200. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_sdjwt_adversarial.py +0 -0
  201. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_sdjwt_duplicate_cnf.py +0 -0
  202. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_sdjwt_issue.py +0 -0
  203. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_sdjwt_reference.py +0 -0
  204. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_sdjwt_resource_budget.py +0 -0
  205. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_sdjwt_vc.py +0 -0
  206. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_sdjwt_verify_binding.py +0 -0
  207. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_sdjwtvc_external_vectors.py +0 -0
  208. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_signature.py +0 -0
  209. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_statuslist.py +0 -0
  210. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_subject_binding.py +0 -0
  211. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_tlogproof.py +0 -0
  212. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_tree_context_authenticity.py +0 -0
  213. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_trust_pack.py +0 -0
  214. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_trust_pack_crypto_agility.py +0 -0
  215. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_trust_pack_outcome_receivers_role.py +0 -0
  216. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_trust_policy.py +0 -0
  217. {proofbundle-3.2.3 → proofbundle-3.6.0}/tests/test_verification_summary.py +0 -0
  218. {proofbundle-3.2.3 → proofbundle-3.6.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.2.3
3
+ Version: 3.6.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
@@ -29,13 +29,15 @@ Provides-Extra: eval
29
29
  Requires-Dist: rfc8785>=0.1.4; extra == "eval"
30
30
  Provides-Extra: anchors
31
31
  Requires-Dist: rfc3161-client>=1.0.6; extra == "anchors"
32
- Requires-Dist: opentimestamps>=0.4.5; extra == "anchors"
32
+ Requires-Dist: opentimestamps<0.5,>=0.4.5; extra == "anchors"
33
33
  Requires-Dist: rfc8785>=0.1.4; extra == "anchors"
34
34
  Provides-Extra: adapters
35
35
  Provides-Extra: chia
36
36
  Provides-Extra: experimental
37
37
  Provides-Extra: pq
38
38
  Requires-Dist: cryptography>=48; extra == "pq"
39
+ Provides-Extra: formal
40
+ Requires-Dist: z3-solver>=4.12; extra == "formal"
39
41
  Provides-Extra: pytest
40
42
  Requires-Dist: pytest>=7; extra == "pytest"
41
43
  Provides-Extra: inspect
@@ -49,6 +51,7 @@ Requires-Dist: build>=1; extra == "dev"
49
51
  Requires-Dist: hypothesis>=6; extra == "dev"
50
52
  Requires-Dist: rfc8785>=0.1.4; extra == "dev"
51
53
  Requires-Dist: sd-jwt>=0.10; extra == "dev"
54
+ Requires-Dist: PyYAML>=6; extra == "dev"
52
55
  Requires-Dist: inspect_ai<0.4,>=0.3.112; extra == "dev"
53
56
  Dynamic: license-file
54
57
 
@@ -72,7 +75,7 @@ Merkle, one file, no server, no network.
72
75
  [![PyPI](https://img.shields.io/pypi/v/proofbundle.svg)](https://pypi.org/project/proofbundle/)
73
76
  [![Python](https://img.shields.io/pypi/pyversions/proofbundle.svg)](https://pypi.org/project/proofbundle/)
74
77
  [![License: MIT](https://img.shields.io/badge/license-MIT-D6248A.svg)](https://github.com/b7n0de/proofbundle/blob/main/LICENSE)
75
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21230467.svg)](https://doi.org/10.5281/zenodo.21230467)
78
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21110642.svg)](https://doi.org/10.5281/zenodo.21110642)
76
79
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
77
80
  [![Mutation tested](https://img.shields.io/badge/tests-mutation_gated-D6248A.svg)](https://github.com/b7n0de/proofbundle/blob/main/scripts/mutation_check.py)
78
81
  [![PyPI attestations](https://img.shields.io/badge/PyPI-PEP_740_attested-D6248A.svg)](https://pypi.org/project/proofbundle/#files)
@@ -211,9 +214,11 @@ a preprint.
211
214
  ## Cite this work
212
215
 
213
216
  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); the archival record and
215
- concept DOI are on Zenodo ([10.5281/zenodo.21230467](https://doi.org/10.5281/zenodo.21230467)), and the
216
- current design write-up is the Technical Note linked from [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
217
+ [`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff). The archival software record
218
+ is on Zenodo under concept DOI [10.5281/zenodo.21110642](https://doi.org/10.5281/zenodo.21110642); the
219
+ Technical Note (design write-up) under concept DOI
220
+ [10.5281/zenodo.21230466](https://doi.org/10.5281/zenodo.21230466), also linked from
221
+ [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
217
222
 
218
223
  ## Post-quantum posture (honest, two layers)
219
224
 
@@ -18,7 +18,7 @@ Merkle, one file, no server, no network.
18
18
  [![PyPI](https://img.shields.io/pypi/v/proofbundle.svg)](https://pypi.org/project/proofbundle/)
19
19
  [![Python](https://img.shields.io/pypi/pyversions/proofbundle.svg)](https://pypi.org/project/proofbundle/)
20
20
  [![License: MIT](https://img.shields.io/badge/license-MIT-D6248A.svg)](https://github.com/b7n0de/proofbundle/blob/main/LICENSE)
21
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21230467.svg)](https://doi.org/10.5281/zenodo.21230467)
21
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21110642.svg)](https://doi.org/10.5281/zenodo.21110642)
22
22
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
23
23
  [![Mutation tested](https://img.shields.io/badge/tests-mutation_gated-D6248A.svg)](https://github.com/b7n0de/proofbundle/blob/main/scripts/mutation_check.py)
24
24
  [![PyPI attestations](https://img.shields.io/badge/PyPI-PEP_740_attested-D6248A.svg)](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); the archival record and
161
- concept DOI are on Zenodo ([10.5281/zenodo.21230467](https://doi.org/10.5281/zenodo.21230467)), and the
162
- current design write-up is the Technical Note linked from [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
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.2.3"
7
+ version = "3.6.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"
@@ -50,9 +50,12 @@ eval = ["rfc8785>=0.1.4"]
50
50
  # EXPERIMENTAL: external time anchors (proofbundle.anchors) — RFC 3161 TSA + OpenTimestamps. Opt-in and
51
51
  # clearly experimental (API/format may change). rfc3161-client (Trail of Bits) is deliberately offline —
52
52
  # it verifies an RFC 3161 token against bundled roots, no network in the verify path. opentimestamps is
53
- # the OTS proof library. rfc8785 gives the receipt its canonical (JCS) root. The base install stays
54
- # anchor-free (only cryptography); a receipt with no anchors verifies unchanged.
55
- anchors = ["rfc3161-client>=1.0.6", "opentimestamps>=0.4.5", "rfc8785>=0.1.4"]
53
+ # the OTS proof LIBRARY (python-opentimestamps, the consensus-critical opentimestamps.core modules), NOT
54
+ # the `opentimestamps-client` CLI tool (a SEPARATE PyPI package on the 0.7.x line; do not confuse the
55
+ # two). The library line is 0.4.x; it is upper-bounded so a future 0.5 wire/API change is a deliberate
56
+ # opt-in, not a silent break (WP-D2 reproducibility). rfc8785 gives the receipt its canonical (JCS) root.
57
+ # The base install stays anchor-free (only cryptography); a receipt with no anchors verifies unchanged.
58
+ anchors = ["rfc3161-client>=1.0.6", "opentimestamps>=0.4.5,<0.5", "rfc8785>=0.1.4"]
56
59
  # The lm-eval adapter reads exported results.json (no import) → pure stdlib.
57
60
  adapters = []
58
61
  # chia-datalayer/v1 anchor WRITER (anchors_chia_add). Needs no extra Python package — it drives the
@@ -69,6 +72,11 @@ experimental = []
69
72
  # cryptography build with OpenSSL 3.5+ (default PyPI wheels since 48.0.0). Verify-only by default;
70
73
  # the emit helper needs the same. Ed25519 paths never require this.
71
74
  pq = ["cryptography>=48"]
75
+ # EXPERIMENTAL: the versioned formal model of the relation-lineage ladder (Fundament F3, formal/model.py).
76
+ # z3-solver enables the z3-EXTENDED symbolic proof (higher symbolic arity than bounded enumeration can
77
+ # reach). Without it the model still runs a complete bounded-enumeration proof — the extra only adds the
78
+ # symbolic layer, never gates the model. Verify/emit paths never touch z3.
79
+ formal = ["z3-solver>=4.12"]
72
80
  # The pytest plugin (opt-in test-run receipt via the pytest11 entry-point) — pytest is an optional extra,
73
81
  # never a core dependency. Floor 7.0: the hooks/stats API is signature-stable across pytest 7/8/9.
74
82
  pytest = ["pytest>=7"]
@@ -78,7 +86,7 @@ pytest = ["pytest>=7"]
78
86
  # marker is needed here (inspect_ai's own >=3.10 requirement is always satisfiable on a supported base).
79
87
  inspect = ['inspect_ai>=0.3.112,<0.4']
80
88
  dev = ["pytest>=7", "ruff>=0.5", "jsonschema>=4", "mypy>=1.8", "build>=1", "hypothesis>=6",
81
- "rfc8785>=0.1.4", "sd-jwt>=0.10", 'inspect_ai>=0.3.112,<0.4']
89
+ "rfc8785>=0.1.4", "sd-jwt>=0.10", "PyYAML>=6", 'inspect_ai>=0.3.112,<0.4']
82
90
 
83
91
  [project.urls]
84
92
  Homepage = "https://b7n0de.com"
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  from typing import TYPE_CHECKING
15
15
 
16
- __version__ = "3.2.3"
16
+ __version__ = "3.6.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
@@ -122,6 +122,10 @@ def verify_offline_merkle(proof_obj: dict, canonical_root: bytes) -> dict:
122
122
  Proves ONLY that ``canonical_root`` is a key included under ``published_root``; NOT that ``published_root``
123
123
  is on-chain (that is level ii/iii, needs Chia software — see docs/ANCHORS.md).
124
124
  """
125
+ # Fail-closed type guard (F4 type-confusion): a non-object proof is malformed input, reported in
126
+ # the function's own {ok, detail} contract — never a raw AttributeError on `.get`.
127
+ if not isinstance(proof_obj, dict):
128
+ return {"ok": False, "detail": "malformed chia-datalayer proof: expected a JSON object"}
125
129
  try:
126
130
  key_clvm = _hexbytes(proof_obj.get("key_clvm_hash"), "key_clvm_hash")
127
131
  value_clvm = _hexbytes(proof_obj.get("value_clvm_hash"), "value_clvm_hash")
@@ -0,0 +1,273 @@
1
+ """OpenTimestamps anchor (EXPERIMENTAL; the `[anchors]` extra).
2
+
3
+ Honest lifecycle (this is where OTS is easy to over-claim):
4
+
5
+ * A fresh stamp is submitted to public calendars and is **PENDING** — it commits your root but is not
6
+ yet anchored in Bitcoin. A pending proof is a WARN, never a full-strength anchor.
7
+ * ``ots upgrade`` embeds the Bitcoin block-header path; only then is the proof **upgraded** and
8
+ self-contained (no calendar needed to verify).
9
+ * Verifying an upgraded proof still needs the Bitcoin **block header** for the attested height — per the
10
+ documented client path, a local (pruned) Bitcoin node. There is no documented "header file instead of
11
+ a node" mode, and we do not claim one. If the caller supplies the block header (its Merkle root) in the
12
+ anchor's ``frozen`` block, we verify against it offline; otherwise we report, honestly, that the proof
13
+ is upgraded but Bitcoin verification needs a node/header — we never silently PASS it.
14
+
15
+ ``proof`` is the serialized detached OTS proof; ``canonicalRoot`` is the exact bytes that were stamped.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Optional
20
+
21
+
22
+ def _classify(timestamp):
23
+ """Return (has_bitcoin, bitcoin_heights, has_pending) over all attestations in the proof."""
24
+ from opentimestamps.core.notary import ( # noqa: PLC0415
25
+ BitcoinBlockHeaderAttestation, PendingAttestation,
26
+ )
27
+ heights, has_pending = [], False
28
+ for _msg, att in timestamp.all_attestations():
29
+ if isinstance(att, BitcoinBlockHeaderAttestation):
30
+ heights.append(att.height)
31
+ elif isinstance(att, PendingAttestation):
32
+ has_pending = True
33
+ return (bool(heights), heights, has_pending)
34
+
35
+
36
+ def _bitcoin_confirmations(timestamp):
37
+ """Yield ``(attested_msg, height, has_hash_op)`` for every ``BitcoinBlockHeaderAttestation`` in the
38
+ proof tree — ONLY a Bitcoin attestation (a Litecoin or any other chain's attestation with a colliding
39
+ integer height is NOT a Bitcoin confirmation) — where ``has_hash_op`` is True iff at least one
40
+ CRYPTOGRAPHIC hash operation (``CryptOp``, e.g. ``OpSHA256``) lies on the op path from the root
41
+ ``file_digest`` down to the attestation.
42
+
43
+ WP-A1.c (Null-Op hardening, 2026-07-17). A genuine Bitcoin timestamp always descends through the
44
+ block's SHA-256 merkle path, so ``has_hash_op`` is always True for it. A SELF-FABRICATED pack can plant
45
+ a ``BitcoinBlockHeaderAttestation`` DIRECTLY on the file digest (leaf == root, zero ops) or under a
46
+ hash-free append/prepend-only chain — then the producer freely set ``file_digest == canonicalRoot ==
47
+ the attested block merkle root`` with NO hashing at all, which is not a timestamp. Such a branch yields
48
+ ``has_hash_op=False`` and the caller MUST NOT treat it as a confirmation (fail-closed). ``getattr`` on
49
+ the height is deliberately avoided (the WP-A1.b confirm loop used to read ``getattr(att, 'height')``,
50
+ which a Litecoin attestation also carries — that colliding-height branch is closed here by the
51
+ ``isinstance`` filter)."""
52
+ from opentimestamps.core.notary import BitcoinBlockHeaderAttestation # noqa: PLC0415
53
+ from opentimestamps.core.op import CryptOp # noqa: PLC0415
54
+ stack = [(timestamp, False)]
55
+ while stack:
56
+ ts, seen_hash = stack.pop()
57
+ for att in ts.attestations:
58
+ if isinstance(att, BitcoinBlockHeaderAttestation):
59
+ yield (ts.msg, att.height, seen_hash)
60
+ for op, child in ts.ops.items():
61
+ stack.append((child, seen_hash or isinstance(op, CryptOp)))
62
+
63
+
64
+ def verify_opentimestamps(proof: bytes, canonical_root: bytes, *, frozen: dict,
65
+ now: Optional[int] = None, rp_trust: Optional[dict] = None) -> dict:
66
+ """Fail-closed OTS verify. Returns {ok, detail, warn, status}. A pending proof is warn (status
67
+ 'pending'); an upgraded proof with no RELYING-PARTY block header is not-ok-but-not-warn honest report
68
+ (status 'needs_rp_trust'); an upgraded proof verified against an RP-supplied header is ok (status
69
+ 'confirmed').
70
+
71
+ WP-A1 (Owner-GO, trust from the relying party): the Bitcoin block header that turns an upgraded proof
72
+ into a CONFIRMED anchor is trust material and MUST come from the relying party (``rp_trust`` — CLI
73
+ ``--bitcoin-header`` / policy ``anchors.bitcoin_block_headers``), NEVER from the bundle's own ``frozen``
74
+ block (which the producer controls and could backdate with a self-committed header). A frozen header is
75
+ reported as EVIDENCE (``frozenEvidence``) but is never trusted. Without RP trust material an upgraded
76
+ proof is honestly ``needs_rp_trust`` (ok=False), so ``--require-anchor`` is unmet → exit 3."""
77
+ try:
78
+ from opentimestamps.core.serialize import BytesDeserializationContext # noqa: PLC0415
79
+ from opentimestamps.core.timestamp import DetachedTimestampFile # noqa: PLC0415
80
+ except ImportError:
81
+ return {"ok": False, "warn": False, "status": "no_lib",
82
+ "detail": "opentimestamps anchor needs proofbundle[anchors] (opentimestamps)"}
83
+ try:
84
+ dtf = DetachedTimestampFile.deserialize(BytesDeserializationContext(proof))
85
+ except Exception as exc: # any malformed proof → FAIL (fail-closed)
86
+ return {"ok": False, "warn": False, "status": "malformed",
87
+ "detail": f"OTS proof did not deserialize: {exc}"}
88
+ # structural binding: the proof must commit to EXACTLY the canonical root
89
+ if dtf.file_digest != canonical_root:
90
+ return {"ok": False, "warn": False, "status": "unbound",
91
+ "detail": "OTS proof does not commit to the target canonical root"}
92
+ has_bitcoin, heights, has_pending = _classify(dtf.timestamp)
93
+ if not has_bitcoin:
94
+ if has_pending:
95
+ return {"ok": False, "warn": True, "status": "pending",
96
+ "detail": "OTS proof is PENDING (submitted to calendars, not yet on Bitcoin) — "
97
+ "run `ots upgrade`; not a full anchor yet"}
98
+ return {"ok": False, "warn": False, "status": "empty",
99
+ "detail": "OTS proof has no Bitcoin or pending attestation"}
100
+ # upgraded: to verify offline we need the block's Merkle root for the attested height. WP-A1: that root
101
+ # is TRUST material — it must come from the RELYING PARTY (rp_trust.bitcoin_block_headers, i.e. their
102
+ # own trusted/pruned Bitcoin node), NEVER from the producer-controlled `frozen` block. A frozen header
103
+ # is surfaced as evidence only. BitcoinBlockHeaderAttestation's own check is exactly
104
+ # `attestation_message == block_header.hashMerkleRoot`; we do that comparison directly against the
105
+ # RP-supplied root (equivalent, and avoids reconstructing a full CBlockHeader).
106
+ rp_headers = (rp_trust or {}).get("bitcoin_block_headers") or {}
107
+ frozen_headers = frozen.get("bitcoinBlockHeaderMerkleRootsByHeight") or {}
108
+ if not rp_headers:
109
+ # no RP trust material → cannot confirm. Frozen alone is NOT trust (it could be a self-committed
110
+ # backdated header). Honest not-ok report; --require-anchor stays unmet (exit 3).
111
+ return {"ok": False, "warn": False, "status": "needs_rp_trust", "needs_rp_trust": True,
112
+ "frozenEvidence": bool(frozen_headers),
113
+ "detail": f"OTS proof is upgraded (Bitcoin height {heights}) but confirming it needs a "
114
+ "relying-party-supplied Bitcoin block header (--bitcoin-header / policy "
115
+ "anchors.bitcoin_block_headers). The bundle's own frozen header is producer-"
116
+ "controlled evidence, not trust; not claiming a pass"}
117
+ # WP-A1.b (Berkeley audit, 2026-07-16): a proof can carry SEVERAL Bitcoin attestations (independent
118
+ # calendar branches, a reorg-era re-anchor, a bad/tampered branch alongside a good one). Because the
119
+ # structural binding above pins EVERY attestation's message to the same canonical root (the walk starts
120
+ # at `file_digest`, which must equal `canonical_root`), confirming on ANY branch whose attested block
121
+ # matches the relying-party header is sound — the branch cannot commit an unrelated root. So we scan ALL
122
+ # RP-covered heights and confirm as soon as one matches; a single wrong/tampered branch must NEVER
123
+ # short-circuit and mask a genuinely confirmable one (that would be a False-REJECT / DoS).
124
+ #
125
+ # WP-A1.c (Null-Op hardening, 2026-07-17): a confirmable Bitcoin branch must also sit at the END of a
126
+ # REAL op chain (>=1 cryptographic hash op below the file digest). `_bitcoin_confirmations` filters to
127
+ # BitcoinBlockHeaderAttestation only (a Litecoin attestation with a colliding height is not a Bitcoin
128
+ # confirmation) and reports `has_hash_op`; a branch attested DIRECTLY on the file digest (leaf==root, no
129
+ # ops) is a self-fabricated Null-Op pack, not a timestamp, and is refused (fail-closed) even if its
130
+ # attested value equals the relying-party header. This is defense-in-depth only: the canonical
131
+ # `verify --require-anchor` path is unaffected — it independently binds the anchor to the receipt's
132
+ # recomputed root, so it never trusts a self-declared canonicalRoot.
133
+ #
134
+ # Per-branch diagnostics are retained: if no branch confirms we surface whether a covered branch was a
135
+ # fabricated Null-Op (null_op), present-and-wrong (block_mismatch, a tamper signal), carried bad
136
+ # relying-party hex (bad_header), or was simply uncovered (upgraded_unverified).
137
+ mismatch_heights: list = []
138
+ bad_header_heights: list = []
139
+ null_op_heights: list = []
140
+ for att_msg, height, has_hash_op in _bitcoin_confirmations(dtf.timestamp):
141
+ merkle_root_hex = rp_headers.get(str(height))
142
+ if not merkle_root_hex:
143
+ continue
144
+ try:
145
+ expected = bytes.fromhex(merkle_root_hex)
146
+ except ValueError:
147
+ # a malformed RP header for THIS height must not short-circuit — another branch may confirm.
148
+ bad_header_heights.append(height)
149
+ continue
150
+ if att_msg != expected:
151
+ mismatch_heights.append(height)
152
+ continue
153
+ if not has_hash_op:
154
+ # leaf==root / hash-free chain: a self-fabricated Null-Op pack, never a confirmation. Record it
155
+ # but KEEP scanning — a genuine branch (if any) must still be able to confirm (WP-A1.b).
156
+ null_op_heights.append(height)
157
+ continue
158
+ return {"ok": True, "warn": False, "status": "confirmed", "rp_trusted": True,
159
+ # WP-A2: a Bitcoin HEIGHT is the proof's native trusted-time unit — reported
160
+ # structured, never converted to a wall-clock guess (the header time is not
161
+ # part of the supplied material).
162
+ "trustedTime": {"source": "bitcoin_block", "height": height},
163
+ "detail": f"OTS proof confirmed: committed in the Bitcoin block at height {height} "
164
+ "(merkle root supplied by the relying party)"}
165
+ # No genuine RP-covered branch matched. Fall through with an honest, tamper-visible diagnostic; the
166
+ # fabricated Null-Op case is the most severe (it matched but was never a real timestamp) — surface it
167
+ # first, but keep the other per-branch signals for the relying party.
168
+ if null_op_heights:
169
+ return {"ok": False, "warn": False, "status": "null_op", "rp_trusted": True,
170
+ "nullOpHeights": sorted(null_op_heights),
171
+ "mismatchHeights": sorted(mismatch_heights),
172
+ "badHeaderHeights": sorted(bad_header_heights),
173
+ "detail": f"OTS proof's Bitcoin attestation(s) at height(s) {sorted(null_op_heights)} sit "
174
+ "directly on the canonical root with no cryptographic op chain (leaf==root) — a "
175
+ "self-fabricated Null-Op pack, not a real Bitcoin timestamp; not confirmed. The "
176
+ "relying party must bind the anchor independently (verify --require-anchor)"}
177
+ if mismatch_heights:
178
+ return {"ok": False, "warn": False, "status": "block_mismatch", "rp_trusted": True,
179
+ "mismatchHeights": sorted(mismatch_heights),
180
+ "badHeaderHeights": sorted(bad_header_heights),
181
+ "detail": f"no OTS Bitcoin attestation matched the relying-party block merkle root: "
182
+ f"present-and-wrong at height(s) {sorted(mismatch_heights)}"
183
+ + (f", invalid relying-party hex at height(s) {sorted(bad_header_heights)}"
184
+ if bad_header_heights else "")}
185
+ if bad_header_heights:
186
+ return {"ok": False, "warn": False, "status": "bad_header", "rp_trusted": True,
187
+ "badHeaderHeights": sorted(bad_header_heights),
188
+ "detail": f"relying-party Bitcoin block merkle root is not valid hex for height(s) "
189
+ f"{sorted(bad_header_heights)}, and no other covered branch confirmed"}
190
+ return {"ok": False, "warn": False, "status": "upgraded_unverified", "needs_rp_trust": True,
191
+ "frozenEvidence": bool(frozen_headers),
192
+ "detail": f"OTS proof is upgraded (Bitcoin height {heights}) but the relying party supplied no "
193
+ "block header for any attested height; not claiming a pass"}
194
+
195
+
196
+ # ── Calendar transparency (WP-B1) ──────────────────────────────────────────────────────────────────
197
+ # A stamp is created with the aid of MULTIPLE remote calendars — the OpenTimestamps client submits to
198
+ # three default endpoints across at least two independent operators (a/b.pool.opentimestamps.org run by
199
+ # OpenTimestamps, a.pool.eternitywall.com run by Eternity Wall), and requires at least two to reply.
200
+ # Surfacing WHICH calendars carry a proof is an embedded-but-UNVERIFIED transparency hint (a
201
+ # PendingAttestation URI is unauthenticated and offline-constructible), NOT cryptographic redundancy
202
+ # evidence: an outage or defunding of any one calendar removes none of the others' PendingAttestations,
203
+ # and verifying an UPGRADED proof needs no calendar at all. These helpers are pure transparency: they
204
+ # read a proof, never trust it, never raise.
205
+
206
+ _KNOWN_CALENDAR_OPERATORS = (
207
+ ("opentimestamps.org", "opentimestamps"),
208
+ ("eternitywall.com", "eternitywall"),
209
+ ("catallaxy.com", "catallaxy"),
210
+ )
211
+
212
+
213
+ def calendar_operator(uri: str) -> str:
214
+ """Best-effort operator label for a calendar URI. Operator redundancy (distinct OPERATORS), not URL
215
+ count, is what tolerates an outage or a defunding — two URLs on one operator are one point of
216
+ failure. Maps a known calendar host to its operator; an unknown host falls back to its last two
217
+ host labels so a distinct third-party or self-hosted calendar still counts as a distinct operator.
218
+ Never raises (a transparency helper must not break a verify path).
219
+
220
+ BLIND SPOT (documented, not hidden — Berkeley audit 2026-07-16): this is a bare-hostname heuristic,
221
+ NOT a verified-independent-entity claim. The last-two-labels fallback does not know the public-suffix
222
+ boundary, so a ccSLD host like ``cal.example.co.uk`` collapses to ``co.uk`` (and ``example.com.au`` to
223
+ ``com.au``): two genuinely independent operators under the same ccSLD would be counted as one, and the
224
+ label itself is a registrable-domain guess, not an attestation of who runs the calendar. It is a
225
+ transparency hint only; for a real independence claim, pin the operators you trust. An optional
226
+ ``tldextract`` dependency would resolve the public-suffix boundary; it is deliberately not added here,
227
+ so this stays a heuristic."""
228
+ if not isinstance(uri, str) or not uri:
229
+ return "unknown"
230
+ from urllib.parse import urlparse # noqa: PLC0415
231
+ host = (urlparse(uri if "://" in uri else "https://" + uri).hostname or "").lower()
232
+ if not host:
233
+ return "unknown"
234
+ for needle, label in _KNOWN_CALENDAR_OPERATORS:
235
+ if host == needle or host.endswith("." + needle):
236
+ return label
237
+ parts = [p for p in host.split(".") if p]
238
+ return ".".join(parts[-2:]) if len(parts) >= 2 else host
239
+
240
+
241
+ def calendar_uris(proof: bytes) -> list[str]:
242
+ """The distinct calendar URIs whose PendingAttestations carry ``proof`` (WP-B1 transparency).
243
+ Fail-closed: without the ``[anchors]`` extra, or on a malformed proof, returns ``[]`` (never raises).
244
+ An UPGRADED proof that no longer retains pending attestations honestly returns ``[]`` — its calendar
245
+ dependency is already discharged, which is precisely the calendar-independence being surfaced."""
246
+ try:
247
+ from opentimestamps.core.notary import PendingAttestation # noqa: PLC0415
248
+ from opentimestamps.core.serialize import BytesDeserializationContext # noqa: PLC0415
249
+ from opentimestamps.core.timestamp import DetachedTimestampFile # noqa: PLC0415
250
+ except ImportError:
251
+ return []
252
+ try:
253
+ dtf = DetachedTimestampFile.deserialize(BytesDeserializationContext(proof))
254
+ except Exception: # malformed → no calendars, never raise (fail-closed transparency)
255
+ return []
256
+ uris: set[str] = set()
257
+ for _msg, att in dtf.timestamp.all_attestations():
258
+ if isinstance(att, PendingAttestation):
259
+ uri = getattr(att, "uri", None)
260
+ if isinstance(uri, bytes):
261
+ try:
262
+ uri = uri.decode("utf-8")
263
+ except Exception:
264
+ continue
265
+ if isinstance(uri, str) and uri:
266
+ uris.add(uri)
267
+ return sorted(uris)
268
+
269
+
270
+ def calendar_operators(uris) -> list[str]:
271
+ """The distinct, sorted operator labels behind a list of calendar URIs (WP-B1). ``len(...)`` is the
272
+ OPERATOR redundancy — the number that survives an outage, unlike a raw URL count."""
273
+ return sorted({calendar_operator(u) for u in (uris or [])})
@@ -33,6 +33,19 @@ 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)",
42
+ # relation/v0.1 3.4.0 (WP-A / WP-A2): raised on BOTH the decision and outcome verify paths.
43
+ "RELATION_SIGNER_UNAUTHORIZED": "The successor's issuer key is not authorized by the trust "
44
+ "policy's relation_signer pin (WHO may replace — set membership "
45
+ "under the verifier's pins, not a proof of authority)",
46
+ "RELATION_TARGET_MISMATCH": "A supersedes-like edge resolves to a parent NOT in the trust "
47
+ "policy's require_relation_target pin (WHICH parent — a valid but "
48
+ "wrong/decoy parent, rejected only in the policy verdict)",
36
49
  }
37
50
 
38
51