proofbundle 3.1.2__tar.gz → 3.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {proofbundle-3.1.2/src/proofbundle.egg-info → proofbundle-3.1.3}/PKG-INFO +6 -4
- {proofbundle-3.1.2 → proofbundle-3.1.3}/README.md +5 -3
- {proofbundle-3.1.2 → proofbundle-3.1.3}/pyproject.toml +1 -1
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/__init__.py +2 -2
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/bundle.py +56 -7
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/cli.py +188 -13
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/decision.py +5 -1
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/policies/decision-receipt-template-v1.json +1 -0
- proofbundle-3.1.3/src/proofbundle/policies/research-preview-v1.json +16 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/policies/strict-eval-authenticated-root-template-v1.json +1 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/policies/strict-eval-template-v1.json +1 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/policies/strict-prereg-template-v1.json +1 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/policy.py +302 -18
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/policy_profiles.py +30 -6
- {proofbundle-3.1.2 → proofbundle-3.1.3/src/proofbundle.egg-info}/PKG-INFO +6 -4
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle.egg-info/SOURCES.txt +3 -0
- proofbundle-3.1.3/tests/test_lens_review_fixes_3_1_3.py +252 -0
- proofbundle-3.1.3/tests/test_policy_lifecycle_purpose.py +464 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_root_authenticity.py +44 -13
- proofbundle-3.1.3/tests/test_tree_context_authenticity.py +329 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_trust_policy.py +4 -2
- proofbundle-3.1.2/src/proofbundle/policies/research-preview-v1.json +0 -7
- {proofbundle-3.1.2 → proofbundle-3.1.3}/LICENSE +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/setup.cfg +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/_integration.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/_strict_json.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/adapters/__init__.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/adapters/_provenance.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/adapters/eee.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/adapters/promptfoo.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/adapters/samples.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/anchors.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/anchors_chia.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/anchors_chia_add.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/anchors_markovian.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/anchors_ots.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/anchors_rfc3161.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/beacon.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/canonical.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/checkpoint.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/demo.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/dsse.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/emit.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/errors.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/evalclaim.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/experimental/__init__.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/experimental/enclave.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/hf_evals.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/intoto.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/kbjwt.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/merkle.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/persample.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/prereg.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/py.typed +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/sdjwt.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/sdjwt_issue.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/signature.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/statuslist.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle/tlogproof.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle.egg-info/requires.txt +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_adapters.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_adversarial.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_anchor_target_trustedtime.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_anchors_chia.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_anchors_chia_add.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_anchors_chia_claims.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_anchors_generic.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_anchors_markovian.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_anchors_ots.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_anchors_rfc3161.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_beacon.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_branch_base_check.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_bundle.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_canonical.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_checkpoint.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_claims_hygiene.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_cli.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_cli_eval.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_cli_require_anchor.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_conformance.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_cosignature.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_decision_anchors.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_decision_cli.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_decision_evidence.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_decision_fuzz.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_decision_hardening.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_decision_policy.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_decision_schema.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_decision_validator_api.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_decision_verify.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_demo.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_docs_truth.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_dup_key_reject.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_ed25519_semantics.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_eee.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_emit.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_eval_evidence_class.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_evalclaim.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_examples.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_experimental_enclave.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_fork_pr_secret_isolation.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_fuzz_parsers.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_hf_entry_verify_eee_digest.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_hf_evals.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_inspect_hook.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_intoto.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_intoto_claims_hygiene.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_intoto_content_root_migration.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_intoto_eval_result.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_intoto_examples.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_intoto_spec_diff.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_intoto_svr.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_kbjwt.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_merkle.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_merkle_property.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_ok_semantics.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_oversized_int_guard.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_persample.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_pinned_key_validation.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_policy_explain_lint.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_policy_profiles.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_policy_templates.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_predicate_type_enforcement.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_prereg.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_promptfoo.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_provenance.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_rekor_interop.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_release_integrity_gate.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_schema.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_sdjwt_duplicate_cnf.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_sdjwt_verify_binding.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_signature.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_statuslist.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_tlogproof.py +0 -0
- {proofbundle-3.1.2 → proofbundle-3.1.3}/tests/test_verify_matrix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofbundle
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.3
|
|
4
4
|
Summary: Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT.
|
|
5
5
|
Author: Konrad Gruszka
|
|
6
6
|
License: MIT
|
|
@@ -127,9 +127,11 @@ proofbundle verify receipt.json --policy trust_policy.json # POLICY: OK | FAIL
|
|
|
127
127
|
# start from a shipped TEMPLATE and pin your own signer — offline, no network:
|
|
128
128
|
proofbundle policy instantiate strict-eval-template-v1 \
|
|
129
129
|
--issuer-key org-eval.pub --policy-id org/strict-eval-v1 --output org.json
|
|
130
|
-
proofbundle verify receipt.json --json --policy org.json
|
|
131
|
-
|
|
132
|
-
#
|
|
130
|
+
proofbundle verify receipt.json --json --policy org.json \
|
|
131
|
+
--expected-root <b64> --expected-tree-size <n> # or: --trusted-checkpoint note.txt --checkpoint-vkey <vkey>
|
|
132
|
+
# → root_authenticity.safeForAutomation: true only when the policy pins a trusted signer AND root
|
|
133
|
+
# plus tree size are authenticated ATOMICALLY from one source (a root-bytes-only pin is
|
|
134
|
+
# rootTrustLevel: ROOT_BYTES_ONLY — never automation-safe; automationBlockers names every reason)
|
|
133
135
|
```
|
|
134
136
|
|
|
135
137
|
## Inspect-native? (METR Task Standard, UK-AISI ecosystem)
|
|
@@ -73,9 +73,11 @@ proofbundle verify receipt.json --policy trust_policy.json # POLICY: OK | FAIL
|
|
|
73
73
|
# start from a shipped TEMPLATE and pin your own signer — offline, no network:
|
|
74
74
|
proofbundle policy instantiate strict-eval-template-v1 \
|
|
75
75
|
--issuer-key org-eval.pub --policy-id org/strict-eval-v1 --output org.json
|
|
76
|
-
proofbundle verify receipt.json --json --policy org.json
|
|
77
|
-
|
|
78
|
-
#
|
|
76
|
+
proofbundle verify receipt.json --json --policy org.json \
|
|
77
|
+
--expected-root <b64> --expected-tree-size <n> # or: --trusted-checkpoint note.txt --checkpoint-vkey <vkey>
|
|
78
|
+
# → root_authenticity.safeForAutomation: true only when the policy pins a trusted signer AND root
|
|
79
|
+
# plus tree size are authenticated ATOMICALLY from one source (a root-bytes-only pin is
|
|
80
|
+
# rootTrustLevel: ROOT_BYTES_ONLY — never automation-safe; automationBlockers names every reason)
|
|
79
81
|
```
|
|
80
82
|
|
|
81
83
|
## Inspect-native? (METR Task Standard, UK-AISI ecosystem)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proofbundle"
|
|
7
|
-
version = "3.1.
|
|
7
|
+
version = "3.1.3"
|
|
8
8
|
description = "Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -13,12 +13,12 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
15
|
|
|
16
|
-
__version__ = "3.1.
|
|
16
|
+
__version__ = "3.1.3"
|
|
17
17
|
|
|
18
18
|
# The `proofbundle/v0.1` normative spec revision this build implements — kept in sync with the
|
|
19
19
|
# `Revision:` line at the top of SPEC.md by tests/test_docs_truth.py (WP-B1, closes #28). Bump
|
|
20
20
|
# both together whenever SPEC.md's normative text changes (not on every package release).
|
|
21
|
-
SPEC_REVISION = "2026-07-
|
|
21
|
+
SPEC_REVISION = "2026-07-13"
|
|
22
22
|
|
|
23
23
|
__all__ = [
|
|
24
24
|
"__version__",
|
|
@@ -43,12 +43,18 @@ AUTOMATION_BLOCKER_REASONS = {
|
|
|
43
43
|
"CRYPTO_FAILED": "Cryptographic verification did not pass",
|
|
44
44
|
"ROOT_NOT_AUTHENTICATED": "The Merkle root was not authenticated against a relying-party value "
|
|
45
45
|
"(--expected-root or a policy trusted_roots entry)",
|
|
46
|
+
"TREE_CONTEXT_NOT_AUTHENTICATED": "Root and tree size were not authenticated ATOMICALLY from one "
|
|
47
|
+
"source (a signed checkpoint via --trusted-checkpoint / policy "
|
|
48
|
+
"trusted_checkpoints, or an --expected-root + "
|
|
49
|
+
"--expected-tree-size pair) — a root-bytes-only pin cannot "
|
|
50
|
+
"detect a tree-size/leaf-index relabel (A-P0-1)",
|
|
46
51
|
"POLICY_NOT_EVALUATED": "No trust policy was evaluated (supply --policy to authorise a signer)",
|
|
47
52
|
"POLICY_FAILED": "The supplied trust policy was not satisfied",
|
|
48
53
|
"SIGNER_NOT_PINNED": "The trust policy pins no trusted signer identity (attributes to nobody)",
|
|
49
54
|
"TEMPLATE_NOT_INSTANTIATED": "The trust policy is a raw template (requiresIdentityOverlay:true) — "
|
|
50
55
|
"instantiate it with a signer identity before depending on it for automation",
|
|
51
56
|
"POLICY_EXPIRED": "The trust policy has expired (its valid_until is in the past)",
|
|
57
|
+
"POLICY_NOT_YET_VALID": "The trust policy is not yet valid (its valid_from is in the future)",
|
|
52
58
|
"POLICY_WARNINGS_PRESENT": "The trust policy carries a warning that blocks automation",
|
|
53
59
|
"ANCHOR_REQUIRED_FAILED": "A required external time anchor did not verify",
|
|
54
60
|
"PUBLIC_TRANSPARENCY_REQUIRED_FAILED": "A required public-transparency proof did not verify",
|
|
@@ -433,9 +439,12 @@ def root_authenticity_summary(result: VerificationResult, *,
|
|
|
433
439
|
signer_trusted: Optional[bool] = None,
|
|
434
440
|
policy_warnings: Optional[list] = None,
|
|
435
441
|
policy_expired: Optional[bool] = None,
|
|
442
|
+
policy_not_yet_valid: Optional[bool] = None,
|
|
436
443
|
requires_identity_overlay: Optional[bool] = None,
|
|
437
444
|
public_transparency_ok: Optional[bool] = None,
|
|
438
|
-
replay_ok: Optional[bool] = None
|
|
445
|
+
replay_ok: Optional[bool] = None,
|
|
446
|
+
tree_context_authenticated: Optional[bool] = None,
|
|
447
|
+
checkpoint_authenticity: Optional[str] = None) -> dict:
|
|
439
448
|
"""Structured root-authenticity verdicts (P0-A §6.3), derived from a completed VerificationResult.
|
|
440
449
|
|
|
441
450
|
Separates what Merkle inclusion actually proves from what it does NOT, as three-state strings so a
|
|
@@ -473,6 +482,28 @@ def root_authenticity_summary(result: VerificationResult, *,
|
|
|
473
482
|
root_auth = "FAIL"
|
|
474
483
|
else:
|
|
475
484
|
root_auth = "NOT_EVALUATED"
|
|
485
|
+
|
|
486
|
+
# A-P0-1 §5.3: the differentiated tree-context verdicts. `root_auth` above is the root-BYTES
|
|
487
|
+
# verdict; TREE_CONTEXT_AUTHENTICITY additionally requires that root and tree_size were
|
|
488
|
+
# authenticated ATOMICALLY from one source (a signed checkpoint, a policy trusted_checkpoints
|
|
489
|
+
# match, or an RP-supplied root+size PAIR). A naked root pin reaches at most
|
|
490
|
+
# ROOT_BYTES_AUTHENTICITY: PASS — never TREE_CONTEXT_AUTHENTICITY: PASS (§5.5).
|
|
491
|
+
if tree_context_authenticated is True:
|
|
492
|
+
tree_context = "PASS"
|
|
493
|
+
elif tree_context_authenticated is False:
|
|
494
|
+
tree_context = "FAIL"
|
|
495
|
+
else:
|
|
496
|
+
tree_context = "NOT_EVALUATED"
|
|
497
|
+
cp_auth = checkpoint_authenticity if checkpoint_authenticity in ("PASS", "FAIL") \
|
|
498
|
+
else "NOT_EVALUATED"
|
|
499
|
+
if tree_context == "PASS" and cp_auth == "PASS":
|
|
500
|
+
root_trust_level = "CHECKPOINT"
|
|
501
|
+
elif tree_context == "PASS":
|
|
502
|
+
root_trust_level = "ROOT_AND_TREE_SIZE_PINNED"
|
|
503
|
+
elif root_auth == "PASS":
|
|
504
|
+
root_trust_level = "ROOT_BYTES_ONLY"
|
|
505
|
+
else:
|
|
506
|
+
root_trust_level = "NONE"
|
|
476
507
|
# P0-B (audit 2026-07-13): the former `policy_ok is not False` let policy_ok=None (no policy
|
|
477
508
|
# evaluated) through → a crypto-valid, root-pinned receipt looked automation-safe though NO trusted
|
|
478
509
|
# signer was ever authorised. safeForAutomation is now a GLOBAL trust verdict: policy_ok must be True
|
|
@@ -492,24 +523,35 @@ def root_authenticity_summary(result: VerificationResult, *,
|
|
|
492
523
|
blockers.append("CRYPTO_FAILED")
|
|
493
524
|
if root_auth != "PASS":
|
|
494
525
|
blockers.append("ROOT_NOT_AUTHENTICATED")
|
|
526
|
+
if tree_context != "PASS":
|
|
527
|
+
# A-P0-1 §5.4: safeForAutomation requires the ATOMIC (root, tree_size) authentication —
|
|
528
|
+
# root bytes alone cannot detect the 2→3-leaf relabel, so they never authorise automation.
|
|
529
|
+
blockers.append("TREE_CONTEXT_NOT_AUTHENTICATED")
|
|
495
530
|
if policy_ok is None:
|
|
496
531
|
blockers.append("POLICY_NOT_EVALUATED")
|
|
497
532
|
elif policy_ok is False:
|
|
498
533
|
blockers.append("POLICY_FAILED")
|
|
499
|
-
elif requires_identity_overlay:
|
|
534
|
+
elif signer_trusted is not True and not requires_identity_overlay:
|
|
535
|
+
blockers.append("SIGNER_NOT_PINNED") # policy passed but pins no trusted identity (attributes to nobody)
|
|
536
|
+
elif signer_trusted is True and policy_warnings:
|
|
537
|
+
blockers.append("POLICY_WARNINGS_PRESENT") # signer pinned yet the policy still warns (forward-compat)
|
|
538
|
+
if requires_identity_overlay:
|
|
500
539
|
# AP-2 §6.2 (L2 pre-land audit): a RAW template (requiresIdentityOverlay:true) is never automation-safe
|
|
501
540
|
# — reported as its OWN blocker, not SIGNER_NOT_PINNED, which would be factually wrong when the template
|
|
502
|
-
# actually does match a signer (the real reason is the un-cleared template-lifecycle flag).
|
|
541
|
+
# actually does match a signer (the real reason is the un-cleared template-lifecycle flag). Independent
|
|
542
|
+
# of the policy_ok chain since A-P0-2: the eval path now FAILS a raw template (policy:not_template →
|
|
543
|
+
# POLICY_FAILED), and the honest template blocker must still be reported alongside it.
|
|
503
544
|
blockers.append("TEMPLATE_NOT_INSTANTIATED")
|
|
504
|
-
elif signer_trusted is not True:
|
|
505
|
-
blockers.append("SIGNER_NOT_PINNED") # policy passed but pins no trusted identity (attributes to nobody)
|
|
506
|
-
elif policy_warnings:
|
|
507
|
-
blockers.append("POLICY_WARNINGS_PRESENT") # signer pinned yet the policy still warns (forward-compat)
|
|
508
545
|
# AP-2 §6.4 lifecycle: an EXPIRED policy is unsafe to automate on even if it otherwise passed (a stale
|
|
509
546
|
# signer pin the relying party has since rotated away from). Independent of the signer/warning chain so
|
|
510
547
|
# it is reported alongside, never in place of, another reason.
|
|
511
548
|
if policy_expired is True:
|
|
512
549
|
blockers.append("POLICY_EXPIRED")
|
|
550
|
+
# A-P0-2 not-before mirror (Lens-2/3/4/6 review): a policy whose valid_from is in the FUTURE at the
|
|
551
|
+
# real current time is not in force, so a crypto-valid receipt under it is not automation-safe even
|
|
552
|
+
# when historically verified — the symmetric case to POLICY_EXPIRED. Reported at current time.
|
|
553
|
+
if policy_not_yet_valid is True:
|
|
554
|
+
blockers.append("POLICY_NOT_YET_VALID")
|
|
513
555
|
if anchor_ok is False:
|
|
514
556
|
blockers.append("ANCHOR_REQUIRED_FAILED")
|
|
515
557
|
if public_transparency_ok is False:
|
|
@@ -519,7 +561,14 @@ def root_authenticity_summary(result: VerificationResult, *,
|
|
|
519
561
|
return {
|
|
520
562
|
"payloadSignature": _tri("ed25519-signature"),
|
|
521
563
|
"merkleConsistency": _tri("merkle-inclusion"),
|
|
564
|
+
# A-P0-1 §5.3: differentiated statuses. `rootAuthenticity` is kept as a WIRE-COMPAT ALIAS of
|
|
565
|
+
# the root-BYTES verdict (it never asserted more than bytes); the undifferentiated reading
|
|
566
|
+
# of it as full root trust is retired — automation keys off treeContextAuthenticity.
|
|
522
567
|
"rootAuthenticity": root_auth,
|
|
568
|
+
"rootBytesAuthenticity": root_auth,
|
|
569
|
+
"treeContextAuthenticity": tree_context,
|
|
570
|
+
"checkpointAuthenticity": cp_auth,
|
|
571
|
+
"rootTrustLevel": root_trust_level,
|
|
523
572
|
"publicTransparency": "NOT_EVALUATED",
|
|
524
573
|
"safeForAutomation": not blockers,
|
|
525
574
|
"automationBlockers": blockers,
|
|
@@ -458,6 +458,65 @@ def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
458
458
|
# --trusted-tsa-root / --bitcoin-header is a clean exit 2 (not a raw traceback), like every other
|
|
459
459
|
# malformed flag. Built once, used at the anchor-requirement site (which is outside this try).
|
|
460
460
|
rp_trust_material = _build_rp_trust(args)
|
|
461
|
+
# A-P0-2 §6.3: historical verification only via an EXPLICIT instant — never silent backdating.
|
|
462
|
+
verification_time = None
|
|
463
|
+
if getattr(args, "verification_time", None) is not None:
|
|
464
|
+
from .policy import _parse_iso_utc # noqa: PLC0415
|
|
465
|
+
if not getattr(args, "policy", None):
|
|
466
|
+
raise ValueError("--verification-time only applies together with --policy (it sets "
|
|
467
|
+
"the instant the policy lifecycle is evaluated at)")
|
|
468
|
+
verification_time = _parse_iso_utc(args.verification_time)
|
|
469
|
+
if verification_time is None:
|
|
470
|
+
raise ValueError(f"--verification-time {args.verification_time!r} is not an "
|
|
471
|
+
"ISO-8601 timestamp (e.g. 2026-01-01T00:00:00Z)")
|
|
472
|
+
# It is HISTORICAL: a FUTURE instant is not a historical query and would let a policy that
|
|
473
|
+
# is not-yet-valid TODAY be forward-dated into force (Lens-2/3/4/6 review). Reject it — the
|
|
474
|
+
# honest present-tense verdict for a not-yet-valid policy is exit 3 in current mode.
|
|
475
|
+
from datetime import datetime, timezone # noqa: PLC0415
|
|
476
|
+
if verification_time >= datetime.now(timezone.utc):
|
|
477
|
+
raise ValueError("--verification-time must be in the past — it evaluates the policy AS "
|
|
478
|
+
"OF a historical instant; a future instant is not a historical query")
|
|
479
|
+
# A-P0-1: a signed C2SP checkpoint is the ONE authenticated source of BOTH the expected root
|
|
480
|
+
# AND the expected tree size (the atomic tree context). Both flags belong together; a note
|
|
481
|
+
# that parses but does not verify is a verification failure (exit 1), a malformed note is
|
|
482
|
+
# malformed input (exit 2), and an explicit flag conflicting with the checkpoint is ambiguous.
|
|
483
|
+
cp_supplied = getattr(args, "trusted_checkpoint", None) is not None
|
|
484
|
+
cp_vkey = getattr(args, "checkpoint_vkey", None)
|
|
485
|
+
if cp_supplied != (cp_vkey is not None):
|
|
486
|
+
raise ValueError("--trusted-checkpoint and --checkpoint-vkey belong together (the "
|
|
487
|
+
"signed note and the key that authenticates it)")
|
|
488
|
+
cp_ok = None
|
|
489
|
+
cp_detail = ""
|
|
490
|
+
expected_root = getattr(args, "expected_root", None)
|
|
491
|
+
expected_tree_size = getattr(args, "expected_tree_size", None)
|
|
492
|
+
if cp_supplied:
|
|
493
|
+
assert cp_vkey is not None # the guard above ties cp_supplied ⇔ cp_vkey present (mypy narrowing)
|
|
494
|
+
import base64 as _b64mod # noqa: PLC0415
|
|
495
|
+
from .checkpoint import verify_checkpoint # noqa: PLC0415
|
|
496
|
+
with open(args.trusted_checkpoint, encoding="utf-8") as handle:
|
|
497
|
+
note = handle.read()
|
|
498
|
+
cp_res = verify_checkpoint(note, cp_vkey) # malformed note/vkey → BundleFormatError → exit 2
|
|
499
|
+
cp_ok = bool(cp_res["ok"])
|
|
500
|
+
if cp_ok:
|
|
501
|
+
cp_root_b64 = _b64mod.b64encode(cp_res["root"]).decode("ascii")
|
|
502
|
+
if expected_root is not None:
|
|
503
|
+
try:
|
|
504
|
+
explicit_root = _b64mod.b64decode(expected_root, validate=True)
|
|
505
|
+
except (ValueError, TypeError) as exc:
|
|
506
|
+
raise ValueError("--expected-root is not valid base64") from exc
|
|
507
|
+
if explicit_root != cp_res["root"]:
|
|
508
|
+
raise ValueError("--expected-root conflicts with the trusted checkpoint's "
|
|
509
|
+
"root — ambiguous; supply only one, or make them equal")
|
|
510
|
+
if expected_tree_size is not None and expected_tree_size != cp_res["tree_size"]:
|
|
511
|
+
raise ValueError("--expected-tree-size conflicts with the trusted checkpoint's "
|
|
512
|
+
"tree size — ambiguous; supply only one, or make them equal")
|
|
513
|
+
expected_root = cp_root_b64
|
|
514
|
+
expected_tree_size = cp_res["tree_size"]
|
|
515
|
+
cp_detail = (f"checkpoint origin {cp_res['origin']!r} authenticates "
|
|
516
|
+
f"(root, tree_size={cp_res['tree_size']}) atomically")
|
|
517
|
+
else:
|
|
518
|
+
cp_detail = ("checkpoint signature does not verify under the supplied vkey — the "
|
|
519
|
+
"expected root/tree size could not be authenticated (fail-closed)")
|
|
461
520
|
# Resolve the path to a dict ONCE and pass it to both verify_bundle and recompute — a second per-function
|
|
462
521
|
# re-read of the same path reopens a TOCTOU window (release-review consistency fix, mirrors show-eval).
|
|
463
522
|
bundle = load_bundle(args.bundle)
|
|
@@ -502,8 +561,11 @@ def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
502
561
|
merged.update(rp_trust_material or {}) # CLI flags take precedence on the same key
|
|
503
562
|
rp_trust_material = merged
|
|
504
563
|
result = verify_bundle(bundle, expected_aud=effective_aud, expected_nonce=flag_nonce,
|
|
505
|
-
expected_root_b64=
|
|
506
|
-
expected_tree_size=
|
|
564
|
+
expected_root_b64=expected_root,
|
|
565
|
+
expected_tree_size=expected_tree_size)
|
|
566
|
+
if cp_supplied:
|
|
567
|
+
# a real verification step: a non-verifying checkpoint fails the crypto verdict (exit 1).
|
|
568
|
+
result.add("checkpoint-authenticity", bool(cp_ok), cp_detail)
|
|
507
569
|
roots = recompute_merkle_root_b64(bundle) if args.verbose else None
|
|
508
570
|
except (ProofBundleError, OSError, ValueError, RecursionError) as exc: # file/JSON/format/policy errors → clean exit 2, never a raw traceback
|
|
509
571
|
# RecursionError: deeply-nested JSON overflows json.load's recursion; catch it here too so it
|
|
@@ -522,10 +584,19 @@ def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
522
584
|
# never a reason to trust bytes whose signature/merkle failed (fail-closed): policy_ok stays None
|
|
523
585
|
# (NOT_EVALUATED) and exit 1 dominates.
|
|
524
586
|
policy_ok = None
|
|
525
|
-
policy_result = None
|
|
587
|
+
policy_result = None # HISTORICAL evaluation (now=verification_time): drives the exit code + POLICY line
|
|
588
|
+
policy_result_now = None # CURRENT evaluation (now=None): drives safeForAutomation + the tree-context inputs
|
|
526
589
|
if policy is not None and crypto_ok:
|
|
527
|
-
|
|
590
|
+
# A-P0-2 §6.3: in historical mode `now` is the explicit --verification-time — the POLICY verdict +
|
|
591
|
+
# exit code answer "was it valid THEN". But safeForAutomation is a PRESENT-tense "safe to act on now"
|
|
592
|
+
# verdict, so its inputs (lifecycle, tree-context, checkpoint validity) MUST use the real current time
|
|
593
|
+
# (Lens-2/3/4/6 review: a not-yet-valid policy or an expired-today checkpoint must never read
|
|
594
|
+
# automation-safe just because a past instant was supplied). We therefore evaluate TWICE in historical
|
|
595
|
+
# mode; in current mode the two coincide (one evaluation, no behaviour change).
|
|
596
|
+
policy_result = evaluate_policy(bundle, result, policy, now=verification_time)
|
|
528
597
|
policy_ok = policy_result["policy_ok"]
|
|
598
|
+
policy_result_now = (policy_result if verification_time is None
|
|
599
|
+
else evaluate_policy(bundle, result, policy, now=None))
|
|
529
600
|
# WP4: the --require-anchor gate is a relying-party requirement layered OVER the crypto result,
|
|
530
601
|
# exactly like --policy — evaluated ONLY when crypto passed (fail-closed; a crypto failure dominates
|
|
531
602
|
# and exits 1). Unmet → anchor_required_ok False → exit 3. Without the flag it stays None and nothing
|
|
@@ -553,7 +624,11 @@ def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
553
624
|
# pins no signer ('attributes to nobody') must NOT set safeForAutomation. Computed once here, reused
|
|
554
625
|
# for fields["policy_warnings"] below (no double lint).
|
|
555
626
|
from .policy import policy_warnings as _policy_warnings # noqa: PLC0415
|
|
556
|
-
|
|
627
|
+
# safeForAutomation is a PRESENT-tense verdict, so it reads the CURRENT-time policy evaluation
|
|
628
|
+
# (policy_result_now); in current mode that IS policy_result (Lens-2/3/4/6 review). policy_ok_now is
|
|
629
|
+
# the current-time policy verdict; the exit code + POLICY line keep using the historical policy_ok.
|
|
630
|
+
policy_ok_now = (policy_result_now or {}).get("policy_ok")
|
|
631
|
+
pol_warns = _policy_warnings(policy) if (policy is not None and policy_ok_now) else []
|
|
557
632
|
# AP-1 §5 (+ L2 pre-land review fix): signer_trusted requires an ACTUALLY-PASSED signer-binding check
|
|
558
633
|
# on THIS (eval/verify) path — a passing `policy:signer_allowed` from evaluate_policy — NOT merely the
|
|
559
634
|
# absence of the 'attributes to nobody' warning. That warning counts
|
|
@@ -562,31 +637,105 @@ def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
562
637
|
# let a decision-maker-only policy FAKE a trusted signer and yield safeForAutomation:true against an
|
|
563
638
|
# unpinned bundle signer (fail-open). Key off the positive verdict instead.
|
|
564
639
|
# AP-2 §6.2: a RAW template (requiresIdentityOverlay:true) never qualifies (no identity overlay applied).
|
|
565
|
-
_signer_checks = [c for c in (
|
|
640
|
+
_signer_checks = [c for c in (policy_result_now or {}).get("checks", [])
|
|
566
641
|
if c.get("name") == "policy:signer_allowed"]
|
|
567
642
|
signer_matched = bool(_signer_checks) and all(c.get("ok") for c in _signer_checks)
|
|
568
|
-
signer_trusted = bool(
|
|
643
|
+
signer_trusted = bool(policy_ok_now) and signer_matched
|
|
569
644
|
# AP-2 §6.2/§6.4 (L2 pre-land audit): a RAW template (requiresIdentityOverlay:true) and an EXPIRED policy
|
|
570
645
|
# each force safeForAutomation:false with their OWN honest blocker (TEMPLATE_NOT_INSTANTIATED /
|
|
571
646
|
# POLICY_EXPIRED) — not a mislabelled SIGNER_NOT_PINNED. signer_trusted now reflects ONLY whether a signer
|
|
572
647
|
# was actually matched; the template/expiry conditions are surfaced as distinct blockers.
|
|
573
648
|
_requires_overlay = (policy or {}).get("requiresIdentityOverlay") is True
|
|
574
649
|
from .policy import policy_expired as _policy_expired # noqa: PLC0415
|
|
575
|
-
|
|
650
|
+
from .policy import policy_not_yet_valid as _policy_not_yet_valid # noqa: PLC0415
|
|
651
|
+
pol_expired = bool(policy is not None and _policy_expired(policy)) # at the REAL current time
|
|
652
|
+
pol_not_yet_valid = bool(policy is not None and _policy_not_yet_valid(policy)) # A-P0-2 not-before mirror
|
|
653
|
+
# A-P0-1 §5.3/§5.4: the atomic tree context. True via (a) a verified --trusted-checkpoint whose
|
|
654
|
+
# (root, size) both match, (b) a policy trusted_checkpoints match at CURRENT time (policy_result_now,
|
|
655
|
+
# so an expired-today checkpoint never authenticates even in historical mode), or (c) an RP-supplied
|
|
656
|
+
# --expected-root + --expected-tree-size PAIR that both passed. A naked root pin stays
|
|
657
|
+
# ROOT_BYTES_ONLY and never authorises automation.
|
|
658
|
+
_by_rs = {c.name: c.ok for c in result.checks}
|
|
659
|
+
_pair_requested = expected_root is not None and expected_tree_size is not None
|
|
660
|
+
_pair_passed = bool(_by_rs.get("root-authenticity")) and bool(_by_rs.get("tree-size"))
|
|
661
|
+
_pol_ctx = (policy_result_now or {}).get("tree_context_authenticated")
|
|
662
|
+
# Any authenticated source that DISAGREES dominates (fail-closed, No-Fake, Lens-1 review F1): if a
|
|
663
|
+
# pinned policy checkpoint says the (root, tree_size) do not match, treeContextAuthenticity is FALSE
|
|
664
|
+
# even when a separately-supplied --expected-root/--expected-tree-size pair happens to pass — the
|
|
665
|
+
# relying party contradicted their own trusted inputs, and the safe verdict is "not authenticated".
|
|
666
|
+
_ctx_false = ((_pol_ctx is False) or (cp_supplied and not cp_ok)
|
|
667
|
+
or (_pair_requested and not _pair_passed))
|
|
668
|
+
_ctx_true = ((_pol_ctx is True)
|
|
669
|
+
or (_pair_requested and _pair_passed and (not cp_supplied or cp_ok)))
|
|
670
|
+
if _ctx_false:
|
|
671
|
+
tree_ctx = False
|
|
672
|
+
elif _ctx_true:
|
|
673
|
+
tree_ctx = True
|
|
674
|
+
else:
|
|
675
|
+
tree_ctx = None
|
|
676
|
+
# checkpointAuthenticity reports whether a checkpoint authenticated AND MATCHED this bundle's
|
|
677
|
+
# (root, tree_size) — NOT merely that some pinned checkpoint's signature verified (Lens-3/4 review:
|
|
678
|
+
# a verified-but-non-matching checkpoint must not read PASS, else rootTrustLevel would overclaim
|
|
679
|
+
# CHECKPOINT). PASS only when the checkpoint is the source of a PASS tree context; FAIL when a
|
|
680
|
+
# checkpoint was supplied/pinned but did not authenticate-and-match; NOT_EVALUATED when none supplied.
|
|
681
|
+
_cli_cp_matched = cp_supplied and cp_ok and _pair_passed
|
|
682
|
+
_pol_cp_matched = (policy_result_now or {}).get("tree_context_authenticated") is True
|
|
683
|
+
_cp_present = cp_supplied or ((policy_result_now or {}).get("checkpoint_authenticity") in ("PASS", "FAIL"))
|
|
684
|
+
if not _cp_present:
|
|
685
|
+
cpa = None
|
|
686
|
+
elif _cli_cp_matched or _pol_cp_matched:
|
|
687
|
+
cpa = "PASS"
|
|
688
|
+
else:
|
|
689
|
+
cpa = "FAIL"
|
|
576
690
|
root_summary = root_authenticity_summary(
|
|
577
|
-
result, policy_authenticated_root=(
|
|
578
|
-
policy_ok=
|
|
691
|
+
result, policy_authenticated_root=(policy_result_now or {}).get("root_authenticated"),
|
|
692
|
+
policy_ok=policy_ok_now, anchor_ok=anchor_required_ok,
|
|
579
693
|
signer_trusted=signer_trusted, policy_warnings=pol_warns, policy_expired=pol_expired,
|
|
580
|
-
|
|
694
|
+
policy_not_yet_valid=pol_not_yet_valid,
|
|
695
|
+
requires_identity_overlay=_requires_overlay,
|
|
696
|
+
tree_context_authenticated=tree_ctx, checkpoint_authenticity=cpa)
|
|
581
697
|
fields["root_authenticity"] = root_summary
|
|
698
|
+
# A-P0-2 §6.3: the labelled historical-verification report (additive; absent in current mode).
|
|
699
|
+
verification_time_report = None
|
|
700
|
+
if verification_time is not None:
|
|
701
|
+
# CURRENT_POLICY_STATUS is the present-tense lifecycle verdict (why safeForAutomation can be NO
|
|
702
|
+
# even when the historical verification passes). Expiry and not-before are BOTH surfaced now
|
|
703
|
+
# (Lens-2/3/4/6: not-yet-valid was previously reported as NO_EXPIRY, hiding the real state).
|
|
704
|
+
if pol_expired:
|
|
705
|
+
current_status = "EXPIRED"
|
|
706
|
+
elif pol_not_yet_valid:
|
|
707
|
+
current_status = "NOT_YET_VALID"
|
|
708
|
+
elif policy is not None and _policy_expired(policy) is None and _policy_not_yet_valid(policy) is None:
|
|
709
|
+
current_status = "NO_LIFECYCLE_WINDOW"
|
|
710
|
+
else:
|
|
711
|
+
current_status = "VALID"
|
|
712
|
+
verification_time_report = {
|
|
713
|
+
"mode": "HISTORICAL",
|
|
714
|
+
"time": args.verification_time,
|
|
715
|
+
"current_policy_status": current_status,
|
|
716
|
+
"historical_policy_status": ("PASS" if policy_ok
|
|
717
|
+
else "FAIL" if policy_ok is False else "NOT_EVALUATED"),
|
|
718
|
+
}
|
|
719
|
+
fields["verification_time"] = verification_time_report
|
|
582
720
|
# P0-A (audit 2026-07-13): surface --expected-tree-size as a machine-readable object. The check
|
|
583
721
|
# itself already runs INDEPENDENTLY in verify_bundle (never gated on --expected-root), so a mismatch
|
|
584
722
|
# already fails the crypto verdict; this makes status/expected/actual explicit so an integrator can
|
|
585
723
|
# confirm the gate ran rather than inferring it from a check name. NOT_REQUESTED when the flag is absent.
|
|
586
|
-
_ets =
|
|
724
|
+
_ets = expected_tree_size # effective: an explicit flag OR the checkpoint-derived size (A-P0-1)
|
|
587
725
|
_tsc = next((c for c in result.checks if c.name == "tree-size"), None)
|
|
726
|
+
# A tree-size authentication WAS requested via a checkpoint whose signature did not verify: the
|
|
727
|
+
# checkpoint never yielded an expected size, so no `tree-size` check ran — but reporting
|
|
728
|
+
# NOT_REQUESTED would be dishonest (a request was made, it could not be evaluated). Report FAIL, the
|
|
729
|
+
# honest "requested but unauthenticated" verdict (Lens-3 review; the run already exits 1 on the
|
|
730
|
+
# checkpoint-authenticity failure).
|
|
731
|
+
if _tsc is not None:
|
|
732
|
+
_ts_status = "PASS" if _tsc.ok else "FAIL"
|
|
733
|
+
elif cp_supplied and not cp_ok:
|
|
734
|
+
_ts_status = "FAIL"
|
|
735
|
+
else:
|
|
736
|
+
_ts_status = "NOT_REQUESTED"
|
|
588
737
|
fields["treeSizeExpectation"] = {
|
|
589
|
-
"status":
|
|
738
|
+
"status": _ts_status,
|
|
590
739
|
"expected": _ets,
|
|
591
740
|
"actual": (bundle.get("merkle") or {}).get("tree_size") if isinstance(bundle, dict) else None,
|
|
592
741
|
}
|
|
@@ -649,6 +798,8 @@ def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
649
798
|
print(f"ROOT-AUTHENTICITY: {root_summary['rootAuthenticity']} "
|
|
650
799
|
f"(payload-signature {root_summary['payloadSignature']}, "
|
|
651
800
|
f"merkle-consistency {root_summary['merkleConsistency']}, "
|
|
801
|
+
f"tree-context {root_summary['treeContextAuthenticity']}, "
|
|
802
|
+
f"root-trust-level {root_summary['rootTrustLevel']}, "
|
|
652
803
|
f"safe-for-automation {str(root_summary['safeForAutomation']).lower()})")
|
|
653
804
|
# AP-1 §5.3: a dedicated, human-legible automation verdict — YES, or NO with the reason(s). The
|
|
654
805
|
# reasons are derived one-for-one from root_summary['automationBlockers'] via the SSOT map, so the
|
|
@@ -672,6 +823,11 @@ def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
672
823
|
if policy_ok and warns:
|
|
673
824
|
line += f" (WARNING: {_safe_line(warns[0].split(':', 1)[0])})"
|
|
674
825
|
print(f"POLICY: {line}")
|
|
826
|
+
if verification_time_report is not None:
|
|
827
|
+
# A-P0-2 §6.3: historical mode is LABELLED — the current and the as-of status both show.
|
|
828
|
+
print(f"VERIFICATION_TIME: HISTORICAL ({_safe_line(str(args.verification_time))})")
|
|
829
|
+
print(f"CURRENT_POLICY_STATUS: {verification_time_report['current_policy_status']}")
|
|
830
|
+
print(f"HISTORICAL_POLICY_STATUS: {verification_time_report['historical_policy_status']}")
|
|
675
831
|
print(f"ASSURANCE: {assurance_line}")
|
|
676
832
|
print(f"LIMITATIONS: {VERIFY_NON_MEANING}")
|
|
677
833
|
# WP4: the ANCHOR line is printed ONLY when --require-anchor was given (default output unchanged).
|
|
@@ -1330,6 +1486,25 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1330
1486
|
verify.add_argument("--expected-tree-size", dest="expected_tree_size", default=None, type=int,
|
|
1331
1487
|
metavar="N", help="require the merkle tree_size to equal N (guards tree-size "
|
|
1332
1488
|
"substitution); a mismatch FAILS")
|
|
1489
|
+
verify.add_argument("--trusted-checkpoint", dest="trusted_checkpoint", default=None, metavar="FILE",
|
|
1490
|
+
help="A-P0-1: a signed C2SP checkpoint note file (SPEC §7c) as the ONE "
|
|
1491
|
+
"authenticated source of BOTH the expected root AND tree size — the "
|
|
1492
|
+
"atomic tree context. Needs --checkpoint-vkey. A bundle whose root or "
|
|
1493
|
+
"tree_size differs from the checkpoint FAILS (exit 1); a checkpoint "
|
|
1494
|
+
"that does not verify under the vkey FAILS (exit 1). Only this (or a "
|
|
1495
|
+
"policy trusted_checkpoints match, or supplying BOTH --expected-root "
|
|
1496
|
+
"and --expected-tree-size) reaches TREE_CONTEXT_AUTHENTICITY: PASS")
|
|
1497
|
+
verify.add_argument("--checkpoint-vkey", dest="checkpoint_vkey", default=None, metavar="VKEY",
|
|
1498
|
+
help="the checkpoint log's C2SP verifier key (name+hexKeyID+base64KeyMaterial) "
|
|
1499
|
+
"for --trusted-checkpoint")
|
|
1500
|
+
verify.add_argument("--verification-time", dest="verification_time", default=None, metavar="ISO8601",
|
|
1501
|
+
help="A-P0-2 §6.3: verify the supplied --policy AS OF this explicit PAST "
|
|
1502
|
+
"instant (e.g. 2026-01-01T00:00:00Z; a future instant is a usage error). "
|
|
1503
|
+
"Output is labelled VERIFICATION_TIME: HISTORICAL with both the CURRENT and "
|
|
1504
|
+
"the HISTORICAL policy status — never a silent backdating. safeForAutomation "
|
|
1505
|
+
"is a present-tense verdict and stays false for a policy expired OR "
|
|
1506
|
+
"not-yet-valid TODAY (or an expired-today checkpoint), even in historical "
|
|
1507
|
+
"mode. Requires --policy")
|
|
1333
1508
|
verify.add_argument("--policy", default=None,
|
|
1334
1509
|
help="path to a trust-policy JSON (proofbundle/trust-policy/v0.1), OR the name "
|
|
1335
1510
|
"of a packaged profile (WP3, e.g. strict-eval-v1 — see "
|
|
@@ -439,7 +439,11 @@ def verify_decision_receipt(envelope: dict, public_key: bytes, *, strict: bool =
|
|
|
439
439
|
_val = predicate.get("validity")
|
|
440
440
|
_validity = _val if isinstance(_val, dict) else {}
|
|
441
441
|
if expected_audience is not None:
|
|
442
|
-
|
|
442
|
+
# audience MUST be a real JSON array: with a STRING value Python's `in` degrades to
|
|
443
|
+
# SUBSTRING matching ("rp.example" in "rp.example" is True) — a wrong-TYPE audience
|
|
444
|
+
# would satisfy the binding (found by the 3.1.3 regression corpus, fail-closed now).
|
|
445
|
+
_aud = _validity.get("audience")
|
|
446
|
+
r["audience_ok"] = isinstance(_aud, list) and expected_audience in _aud
|
|
443
447
|
if not r["audience_ok"]:
|
|
444
448
|
r["errors"].append(
|
|
445
449
|
"audience mismatch or absent validity.audience — requested audience binding cannot be "
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "proofbundle/trust-policy/v0.1",
|
|
3
|
+
"policy_id": "proofbundle-policy/research-preview-v1",
|
|
4
|
+
"policyPurpose": "eval",
|
|
5
|
+
"allowed_schema_versions": [
|
|
6
|
+
"proofbundle/v0.1"
|
|
7
|
+
],
|
|
8
|
+
"signature": {
|
|
9
|
+
"allowed_algs": [
|
|
10
|
+
"ed25519"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"merkle": {
|
|
14
|
+
"required_hash_alg": "sha256-rfc6962"
|
|
15
|
+
}
|
|
16
|
+
}
|