proofbundle 3.1.0__tar.gz → 3.1.2__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.0/src/proofbundle.egg-info → proofbundle-3.1.2}/PKG-INFO +8 -1
- {proofbundle-3.1.0 → proofbundle-3.1.2}/README.md +7 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/pyproject.toml +1 -1
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/__init__.py +1 -1
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/bundle.py +122 -10
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/cli.py +163 -9
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/decision.py +21 -10
- proofbundle-3.1.2/src/proofbundle/policies/decision-receipt-template-v1.json +24 -0
- proofbundle-3.1.2/src/proofbundle/policies/strict-eval-authenticated-root-template-v1.json +25 -0
- proofbundle-3.1.2/src/proofbundle/policies/strict-eval-template-v1.json +24 -0
- proofbundle-3.1.2/src/proofbundle/policies/strict-prereg-template-v1.json +25 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/policy.py +84 -4
- proofbundle-3.1.2/src/proofbundle/policy_profiles.py +236 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2/src/proofbundle.egg-info}/PKG-INFO +8 -1
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle.egg-info/SOURCES.txt +5 -4
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_decision_hardening.py +17 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_decision_policy.py +73 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_kbjwt.py +29 -8
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_ok_semantics.py +13 -3
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_policy_profiles.py +21 -9
- proofbundle-3.1.2/tests/test_policy_templates.py +185 -0
- proofbundle-3.1.2/tests/test_root_authenticity.py +679 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_sdjwt_verify_binding.py +94 -1
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_trust_policy.py +14 -4
- proofbundle-3.1.0/src/proofbundle/policies/decision-receipt-v1.json +0 -14
- proofbundle-3.1.0/src/proofbundle/policies/strict-eval-authenticated-root-v1.json +0 -9
- proofbundle-3.1.0/src/proofbundle/policies/strict-eval-v1.json +0 -9
- proofbundle-3.1.0/src/proofbundle/policies/strict-prereg-v1.json +0 -9
- proofbundle-3.1.0/src/proofbundle/policy_profiles.py +0 -86
- proofbundle-3.1.0/tests/test_root_authenticity.py +0 -244
- {proofbundle-3.1.0 → proofbundle-3.1.2}/LICENSE +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/setup.cfg +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/_integration.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/_strict_json.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/adapters/__init__.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/adapters/_provenance.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/adapters/eee.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/adapters/promptfoo.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/adapters/samples.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/anchors.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/anchors_chia.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/anchors_chia_add.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/anchors_markovian.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/anchors_ots.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/anchors_rfc3161.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/beacon.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/canonical.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/checkpoint.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/demo.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/dsse.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/emit.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/errors.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/evalclaim.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/experimental/__init__.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/experimental/enclave.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/hf_evals.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/intoto.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/kbjwt.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/merkle.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/persample.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/policies/research-preview-v1.json +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/prereg.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/py.typed +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/sdjwt.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/sdjwt_issue.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/signature.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/statuslist.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle/tlogproof.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle.egg-info/requires.txt +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_adapters.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_adversarial.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_anchor_target_trustedtime.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_anchors_chia.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_anchors_chia_add.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_anchors_chia_claims.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_anchors_generic.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_anchors_markovian.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_anchors_ots.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_anchors_rfc3161.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_beacon.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_branch_base_check.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_bundle.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_canonical.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_checkpoint.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_claims_hygiene.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_cli.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_cli_eval.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_cli_require_anchor.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_conformance.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_cosignature.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_decision_anchors.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_decision_cli.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_decision_evidence.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_decision_fuzz.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_decision_schema.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_decision_validator_api.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_decision_verify.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_demo.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_docs_truth.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_dup_key_reject.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_ed25519_semantics.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_eee.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_emit.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_eval_evidence_class.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_evalclaim.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_examples.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_experimental_enclave.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_fork_pr_secret_isolation.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_fuzz_parsers.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_hf_entry_verify_eee_digest.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_hf_evals.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_inspect_hook.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_intoto.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_intoto_claims_hygiene.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_intoto_content_root_migration.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_intoto_eval_result.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_intoto_examples.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_intoto_spec_diff.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_intoto_svr.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_merkle.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_merkle_property.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_oversized_int_guard.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_persample.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_pinned_key_validation.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_policy_explain_lint.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_predicate_type_enforcement.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_prereg.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_promptfoo.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_provenance.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_rekor_interop.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_release_integrity_gate.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_schema.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_sdjwt_duplicate_cnf.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_signature.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_statuslist.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/tests/test_tlogproof.py +0 -0
- {proofbundle-3.1.0 → proofbundle-3.1.2}/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.2
|
|
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
|
|
@@ -123,6 +123,13 @@ proofbundle verify receipt.json # exit 0 = crypto OK, 1 = crypto/verifica
|
|
|
123
123
|
|
|
124
124
|
# apply YOUR trust decision — verify makes NO trust decision on its own:
|
|
125
125
|
proofbundle verify receipt.json --policy trust_policy.json # POLICY: OK | FAIL (exit 3) | NOT_EVALUATED
|
|
126
|
+
|
|
127
|
+
# start from a shipped TEMPLATE and pin your own signer — offline, no network:
|
|
128
|
+
proofbundle policy instantiate strict-eval-template-v1 \
|
|
129
|
+
--issuer-key org-eval.pub --policy-id org/strict-eval-v1 --output org.json
|
|
130
|
+
proofbundle verify receipt.json --json --policy org.json --expected-root <b64>
|
|
131
|
+
# → root_authenticity.safeForAutomation: true only when the policy pins a trusted signer AND the
|
|
132
|
+
# root is authenticated (else false, with automationBlockers naming every reason)
|
|
126
133
|
```
|
|
127
134
|
|
|
128
135
|
## Inspect-native? (METR Task Standard, UK-AISI ecosystem)
|
|
@@ -69,6 +69,13 @@ proofbundle verify receipt.json # exit 0 = crypto OK, 1 = crypto/verifica
|
|
|
69
69
|
|
|
70
70
|
# apply YOUR trust decision — verify makes NO trust decision on its own:
|
|
71
71
|
proofbundle verify receipt.json --policy trust_policy.json # POLICY: OK | FAIL (exit 3) | NOT_EVALUATED
|
|
72
|
+
|
|
73
|
+
# start from a shipped TEMPLATE and pin your own signer — offline, no network:
|
|
74
|
+
proofbundle policy instantiate strict-eval-template-v1 \
|
|
75
|
+
--issuer-key org-eval.pub --policy-id org/strict-eval-v1 --output org.json
|
|
76
|
+
proofbundle verify receipt.json --json --policy org.json --expected-root <b64>
|
|
77
|
+
# → root_authenticity.safeForAutomation: true only when the policy pins a trusted signer AND the
|
|
78
|
+
# root is authenticated (else false, with automationBlockers naming every reason)
|
|
72
79
|
```
|
|
73
80
|
|
|
74
81
|
## 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.2"
|
|
8
8
|
description = "Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
15
|
|
|
16
|
-
__version__ = "3.1.
|
|
16
|
+
__version__ = "3.1.2"
|
|
17
17
|
|
|
18
18
|
# The `proofbundle/v0.1` normative spec revision this build implements — kept in sync with the
|
|
19
19
|
# `Revision:` line at the top of SPEC.md by tests/test_docs_truth.py (WP-B1, closes #28). Bump
|
|
@@ -33,7 +33,27 @@ from .signature import verify_ed25519
|
|
|
33
33
|
from .sdjwt import verify_sd_jwt
|
|
34
34
|
|
|
35
35
|
__all__ = ["SCHEMA", "verify_bundle", "load_bundle", "recompute_merkle_root_b64",
|
|
36
|
-
"root_authenticity_summary"]
|
|
36
|
+
"root_authenticity_summary", "AUTOMATION_BLOCKER_REASONS"]
|
|
37
|
+
|
|
38
|
+
# AP-1 §5.3: the human-legible reason for each automationBlockers enum value. Kept HERE, next to the
|
|
39
|
+
# blocker logic in root_authenticity_summary, so the human `SAFE_FOR_AUTOMATION` line and the JSON flag
|
|
40
|
+
# can never drift apart (one source of truth — Iteration F). A blocker with no entry falls back to its
|
|
41
|
+
# raw enum string, so a future blocker is never silently unexplained.
|
|
42
|
+
AUTOMATION_BLOCKER_REASONS = {
|
|
43
|
+
"CRYPTO_FAILED": "Cryptographic verification did not pass",
|
|
44
|
+
"ROOT_NOT_AUTHENTICATED": "The Merkle root was not authenticated against a relying-party value "
|
|
45
|
+
"(--expected-root or a policy trusted_roots entry)",
|
|
46
|
+
"POLICY_NOT_EVALUATED": "No trust policy was evaluated (supply --policy to authorise a signer)",
|
|
47
|
+
"POLICY_FAILED": "The supplied trust policy was not satisfied",
|
|
48
|
+
"SIGNER_NOT_PINNED": "The trust policy pins no trusted signer identity (attributes to nobody)",
|
|
49
|
+
"TEMPLATE_NOT_INSTANTIATED": "The trust policy is a raw template (requiresIdentityOverlay:true) — "
|
|
50
|
+
"instantiate it with a signer identity before depending on it for automation",
|
|
51
|
+
"POLICY_EXPIRED": "The trust policy has expired (its valid_until is in the past)",
|
|
52
|
+
"POLICY_WARNINGS_PRESENT": "The trust policy carries a warning that blocks automation",
|
|
53
|
+
"ANCHOR_REQUIRED_FAILED": "A required external time anchor did not verify",
|
|
54
|
+
"PUBLIC_TRANSPARENCY_REQUIRED_FAILED": "A required public-transparency proof did not verify",
|
|
55
|
+
"REPLAY_BINDING_REQUIRED_FAILED": "A required replay/audience binding did not verify",
|
|
56
|
+
}
|
|
37
57
|
|
|
38
58
|
|
|
39
59
|
def _issuer_requires_holder_binding(sd_part: str) -> bool:
|
|
@@ -70,6 +90,27 @@ _MERKLE_KEYS = {"hash_alg", "leaf_index", "tree_size", "inclusion_proof_b64", "r
|
|
|
70
90
|
_SD_KEYS = {"compact", "issuer_public_key_b64"}
|
|
71
91
|
|
|
72
92
|
|
|
93
|
+
def _sd_jwt_carries_eval_root_commitment(sd_payload) -> bool:
|
|
94
|
+
"""N1 (audit 2026-07-13; discriminator hardened after pre-land L1 review): True iff an SD-JWT issuer
|
|
95
|
+
payload carries the eval-binding ROOT COMMITMENT (``receipt.root_b64``, a non-empty base64 string)
|
|
96
|
+
that ``check_binds_bundle`` binds against — i.e. it CLAIMS to be anchored to a proofbundle merkle
|
|
97
|
+
root. That commitment is the exact cross-receipt substitution vector, so it (NOT a heuristic
|
|
98
|
+
word-match on generic keys like ``passed``/``suite``/``threshold``) is what marks an eval SD-JWT
|
|
99
|
+
grafted onto a non-eval payload. `issue_sd_jwt` always writes ``receipt.root_b64`` always-open, so a
|
|
100
|
+
genuine eval SD-JWT is caught even if its ``passed``/``threshold`` facts are moved into selective
|
|
101
|
+
disclosures; a generic SD-JWT-VC (``iss``/``vct``, no receipt commitment) is not an eval graft and
|
|
102
|
+
stays in scope (backward-compatible). Residual (documented, out of scope): a SELF-SIGNED credential
|
|
103
|
+
that hides even ``receipt.root_b64`` in a disclosure asserts no always-open anchoring claim and cannot
|
|
104
|
+
forge a trusted-issuer graft (the genuine emitter never does this)."""
|
|
105
|
+
if not isinstance(sd_payload, dict):
|
|
106
|
+
return False
|
|
107
|
+
receipt = sd_payload.get("receipt")
|
|
108
|
+
# Fire on the PRESENCE of a receipt.root_b64 string, INCLUDING "" (L1 pre-land audit F3): an empty root
|
|
109
|
+
# commits nothing concrete, but "an eval-shaped commitment present yet evading N1" should not exist. A
|
|
110
|
+
# generic SD-JWT-VC has no receipt object at all, so this never false-refuses one.
|
|
111
|
+
return isinstance(receipt, dict) and isinstance(receipt.get("root_b64"), str)
|
|
112
|
+
|
|
113
|
+
|
|
73
114
|
def _b64d(value: str, field: str) -> bytes:
|
|
74
115
|
try:
|
|
75
116
|
return base64.b64decode(value, validate=True)
|
|
@@ -350,6 +391,25 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
|
|
|
350
391
|
"sd_jwt_vc disclosures do NOT bind this bundle (reason: unbound/mismatch — cross-receipt "
|
|
351
392
|
"substitution; the SD-JWT's passed/threshold/comparator/suite/issuer/root differ from the "
|
|
352
393
|
"signed claim)")
|
|
394
|
+
elif _sd_jwt_carries_eval_root_commitment(_sd_p):
|
|
395
|
+
# N1 (audit 2026-07-13, L1 live PoC; discriminator hardened after pre-land L1 review): an
|
|
396
|
+
# EVAL SD-JWT (issue_sd_jwt writes the always-open root commitment receipt.root_b64) MUST
|
|
397
|
+
# bind to a proofbundle/eval-claim/v0.1 payload — check_binds_bundle above is the only
|
|
398
|
+
# binding for it. Grafted onto a NON-eval payload it has nothing to bind against, so
|
|
399
|
+
# previously the exact same issuer-valid eval SD-JWT verified CRYPTO: OK on ANY
|
|
400
|
+
# opaque-payload bundle (cross-receipt substitution, sd_jwt_ok stayed true, zero operator
|
|
401
|
+
# signal). Fail-closed (Reifegradpolitik §0.6: never a silent PASS for insecure legacy
|
|
402
|
+
# behaviour). Keying on receipt.root_b64 (the real substitution vector) rather than a
|
|
403
|
+
# word-match on generic keys catches a graft even when passed/threshold are moved into
|
|
404
|
+
# disclosures, and never false-refuses a GENERIC SD-JWT-VC (iss/vct, no receipt commitment,
|
|
405
|
+
# e.g. examples/example_bundle.json), which carries no eval anchoring claim and is out of
|
|
406
|
+
# scope (backward-compatible).
|
|
407
|
+
result.add(
|
|
408
|
+
"sd-jwt-bundle-binding", False,
|
|
409
|
+
"sd_jwt_vc carries an eval-claim root commitment (receipt.root_b64) but the signed "
|
|
410
|
+
"payload is not a proofbundle/eval-claim/v0.1 claim, so that anchoring claim cannot be "
|
|
411
|
+
"bound to this bundle (reason: unbindable eval SD-JWT — a valid issuer signature does not "
|
|
412
|
+
"make an unbound eval anchoring claim belong to this receipt; refused fail-closed)")
|
|
353
413
|
|
|
354
414
|
# F4 (v1.9.2, fail-closed): supplying expected_aud/expected_nonce asks for RFC 9901 §7.3
|
|
355
415
|
# replay/audience binding. A bundle with no verifiable KB-JWT (no sd_jwt_vc at all, or an
|
|
@@ -369,7 +429,13 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
|
|
|
369
429
|
def root_authenticity_summary(result: VerificationResult, *,
|
|
370
430
|
policy_authenticated_root: Optional[bool] = None,
|
|
371
431
|
policy_ok: Optional[bool] = None,
|
|
372
|
-
anchor_ok: Optional[bool] = None
|
|
432
|
+
anchor_ok: Optional[bool] = None,
|
|
433
|
+
signer_trusted: Optional[bool] = None,
|
|
434
|
+
policy_warnings: Optional[list] = None,
|
|
435
|
+
policy_expired: Optional[bool] = None,
|
|
436
|
+
requires_identity_overlay: Optional[bool] = None,
|
|
437
|
+
public_transparency_ok: Optional[bool] = None,
|
|
438
|
+
replay_ok: Optional[bool] = None) -> dict:
|
|
373
439
|
"""Structured root-authenticity verdicts (P0-A §6.3), derived from a completed VerificationResult.
|
|
374
440
|
|
|
375
441
|
Separates what Merkle inclusion actually proves from what it does NOT, as three-state strings so a
|
|
@@ -381,14 +447,18 @@ def root_authenticity_summary(result: VerificationResult, *,
|
|
|
381
447
|
relying-party value (``expected_root``, or a policy's ``trusted_roots``)?
|
|
382
448
|
publicTransparency NOT_EVALUATED — a public-log receipt is the separate §10 profile
|
|
383
449
|
safeForAutomation bool — True ONLY if the whole crypto verdict passed, the root was
|
|
384
|
-
affirmatively authenticated, AND
|
|
385
|
-
|
|
450
|
+
affirmatively authenticated, AND a supplied trust policy
|
|
451
|
+
PASSED with a real signer pin (P0-B, audit 2026-07-13)
|
|
452
|
+
automationBlockers list[str] — every reason safeForAutomation is false, so a consumer
|
|
453
|
+
keying off the flag sees exactly WHY (fail-closed)
|
|
386
454
|
|
|
387
455
|
``policy_authenticated_root`` folds the policy layer's root verdict in when no explicit
|
|
388
456
|
``root-authenticity`` check ran (e.g. the root matched a policy ``trusted_roots`` entry).
|
|
389
|
-
``policy_ok`` / ``anchor_ok`` are the relying-party gate verdicts (True/False/None=not-evaluated)
|
|
390
|
-
|
|
391
|
-
|
|
457
|
+
``policy_ok`` / ``anchor_ok`` are the relying-party gate verdicts (True/False/None=not-evaluated).
|
|
458
|
+
P0-B: ``safeForAutomation`` is a GLOBAL trust verdict, so ``policy_ok`` must be True (a policy that
|
|
459
|
+
passed) — ``None`` (no policy evaluated) can never make it true. ``policy_warnings`` (the vacuous
|
|
460
|
+
'attributes to nobody' lint) forces it false too: a policy that pins no signer authorises no
|
|
461
|
+
identity, so a crypto-valid, root-pinned receipt under it is NOT automation-safe.
|
|
392
462
|
"""
|
|
393
463
|
by = {c.name: c.ok for c in result.checks}
|
|
394
464
|
|
|
@@ -403,14 +473,56 @@ def root_authenticity_summary(result: VerificationResult, *,
|
|
|
403
473
|
root_auth = "FAIL"
|
|
404
474
|
else:
|
|
405
475
|
root_auth = "NOT_EVALUATED"
|
|
406
|
-
|
|
407
|
-
|
|
476
|
+
# P0-B (audit 2026-07-13): the former `policy_ok is not False` let policy_ok=None (no policy
|
|
477
|
+
# evaluated) through → a crypto-valid, root-pinned receipt looked automation-safe though NO trusted
|
|
478
|
+
# signer was ever authorised. safeForAutomation is now a GLOBAL trust verdict: policy_ok must be True
|
|
479
|
+
# AND carry no vacuous 'attributes to nobody' warning. Every failed condition is surfaced in
|
|
480
|
+
# automationBlockers (fail-closed, so the flag is never a silent yes).
|
|
481
|
+
# P0-B / AP-1 §5 (audit 2026-07-13): safeForAutomation is a GLOBAL trust verdict. Variant A (strict):
|
|
482
|
+
# true ONLY when crypto passed, the root was authenticated, a supplied policy PASSED (policy_ok is
|
|
483
|
+
# True — None/no-policy never qualifies), that policy actually PINS a trusted signer (signer_trusted),
|
|
484
|
+
# and no required anchor / public-transparency / replay gate FAILED. automationBlockers enumerates
|
|
485
|
+
# every reason it is false (fail-closed, never a silent yes). NOTE for 3.1.1: public_transparency_ok
|
|
486
|
+
# and replay_ok are relying-party gates that are None (not-requested) on the current core — the §10
|
|
487
|
+
# public-transparency policy section is 3.2.0, and replay (aud/nonce) already fails the crypto verdict
|
|
488
|
+
# (CRYPTO_FAILED) when a required KB-JWT is absent — so these two blockers are defined for forward
|
|
489
|
+
# compatibility and stay dormant unless a future policy layer supplies a False verdict.
|
|
490
|
+
blockers: list[str] = []
|
|
491
|
+
if not bool(result.ok):
|
|
492
|
+
blockers.append("CRYPTO_FAILED")
|
|
493
|
+
if root_auth != "PASS":
|
|
494
|
+
blockers.append("ROOT_NOT_AUTHENTICATED")
|
|
495
|
+
if policy_ok is None:
|
|
496
|
+
blockers.append("POLICY_NOT_EVALUATED")
|
|
497
|
+
elif policy_ok is False:
|
|
498
|
+
blockers.append("POLICY_FAILED")
|
|
499
|
+
elif requires_identity_overlay:
|
|
500
|
+
# AP-2 §6.2 (L2 pre-land audit): a RAW template (requiresIdentityOverlay:true) is never automation-safe
|
|
501
|
+
# — 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).
|
|
503
|
+
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
|
+
# AP-2 §6.4 lifecycle: an EXPIRED policy is unsafe to automate on even if it otherwise passed (a stale
|
|
509
|
+
# signer pin the relying party has since rotated away from). Independent of the signer/warning chain so
|
|
510
|
+
# it is reported alongside, never in place of, another reason.
|
|
511
|
+
if policy_expired is True:
|
|
512
|
+
blockers.append("POLICY_EXPIRED")
|
|
513
|
+
if anchor_ok is False:
|
|
514
|
+
blockers.append("ANCHOR_REQUIRED_FAILED")
|
|
515
|
+
if public_transparency_ok is False:
|
|
516
|
+
blockers.append("PUBLIC_TRANSPARENCY_REQUIRED_FAILED")
|
|
517
|
+
if replay_ok is False:
|
|
518
|
+
blockers.append("REPLAY_BINDING_REQUIRED_FAILED")
|
|
408
519
|
return {
|
|
409
520
|
"payloadSignature": _tri("ed25519-signature"),
|
|
410
521
|
"merkleConsistency": _tri("merkle-inclusion"),
|
|
411
522
|
"rootAuthenticity": root_auth,
|
|
412
523
|
"publicTransparency": "NOT_EVALUATED",
|
|
413
|
-
"safeForAutomation":
|
|
524
|
+
"safeForAutomation": not blockers,
|
|
525
|
+
"automationBlockers": blockers,
|
|
414
526
|
}
|
|
415
527
|
|
|
416
528
|
|
|
@@ -549,18 +549,54 @@ def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
549
549
|
# in when no --expected-root was given. Always separate, so merkle-inclusion is never read as root
|
|
550
550
|
# authentication (additive JSON field + a human line).
|
|
551
551
|
from .bundle import root_authenticity_summary # noqa: PLC0415
|
|
552
|
+
# P0-B (audit 2026-07-13): thread the vacuous-policy lint into the summary — a PASSING policy that
|
|
553
|
+
# pins no signer ('attributes to nobody') must NOT set safeForAutomation. Computed once here, reused
|
|
554
|
+
# for fields["policy_warnings"] below (no double lint).
|
|
555
|
+
from .policy import policy_warnings as _policy_warnings # noqa: PLC0415
|
|
556
|
+
pol_warns = _policy_warnings(policy) if (policy is not None and policy_ok) else []
|
|
557
|
+
# AP-1 §5 (+ L2 pre-land review fix): signer_trusted requires an ACTUALLY-PASSED signer-binding check
|
|
558
|
+
# on THIS (eval/verify) path — a passing `policy:signer_allowed` from evaluate_policy — NOT merely the
|
|
559
|
+
# absence of the 'attributes to nobody' warning. That warning counts
|
|
560
|
+
# decision_receipt.trusted_decision_makers as a signer pin, but evaluate_policy (the verify path) never
|
|
561
|
+
# matches the bundle signer against it (that is the verify-decision path); keying off the warning would
|
|
562
|
+
# let a decision-maker-only policy FAKE a trusted signer and yield safeForAutomation:true against an
|
|
563
|
+
# unpinned bundle signer (fail-open). Key off the positive verdict instead.
|
|
564
|
+
# AP-2 §6.2: a RAW template (requiresIdentityOverlay:true) never qualifies (no identity overlay applied).
|
|
565
|
+
_signer_checks = [c for c in (policy_result or {}).get("checks", [])
|
|
566
|
+
if c.get("name") == "policy:signer_allowed"]
|
|
567
|
+
signer_matched = bool(_signer_checks) and all(c.get("ok") for c in _signer_checks)
|
|
568
|
+
signer_trusted = bool(policy_ok) and signer_matched
|
|
569
|
+
# AP-2 §6.2/§6.4 (L2 pre-land audit): a RAW template (requiresIdentityOverlay:true) and an EXPIRED policy
|
|
570
|
+
# each force safeForAutomation:false with their OWN honest blocker (TEMPLATE_NOT_INSTANTIATED /
|
|
571
|
+
# POLICY_EXPIRED) — not a mislabelled SIGNER_NOT_PINNED. signer_trusted now reflects ONLY whether a signer
|
|
572
|
+
# was actually matched; the template/expiry conditions are surfaced as distinct blockers.
|
|
573
|
+
_requires_overlay = (policy or {}).get("requiresIdentityOverlay") is True
|
|
574
|
+
from .policy import policy_expired as _policy_expired # noqa: PLC0415
|
|
575
|
+
pol_expired = bool(policy is not None and _policy_expired(policy))
|
|
552
576
|
root_summary = root_authenticity_summary(
|
|
553
577
|
result, policy_authenticated_root=(policy_result or {}).get("root_authenticated"),
|
|
554
|
-
policy_ok=policy_ok, anchor_ok=anchor_required_ok
|
|
578
|
+
policy_ok=policy_ok, anchor_ok=anchor_required_ok,
|
|
579
|
+
signer_trusted=signer_trusted, policy_warnings=pol_warns, policy_expired=pol_expired,
|
|
580
|
+
requires_identity_overlay=_requires_overlay)
|
|
555
581
|
fields["root_authenticity"] = root_summary
|
|
582
|
+
# P0-A (audit 2026-07-13): surface --expected-tree-size as a machine-readable object. The check
|
|
583
|
+
# itself already runs INDEPENDENTLY in verify_bundle (never gated on --expected-root), so a mismatch
|
|
584
|
+
# already fails the crypto verdict; this makes status/expected/actual explicit so an integrator can
|
|
585
|
+
# confirm the gate ran rather than inferring it from a check name. NOT_REQUESTED when the flag is absent.
|
|
586
|
+
_ets = getattr(args, "expected_tree_size", None)
|
|
587
|
+
_tsc = next((c for c in result.checks if c.name == "tree-size"), None)
|
|
588
|
+
fields["treeSizeExpectation"] = {
|
|
589
|
+
"status": (("PASS" if _tsc.ok else "FAIL") if _tsc is not None else "NOT_REQUESTED"),
|
|
590
|
+
"expected": _ets,
|
|
591
|
+
"actual": (bundle.get("merkle") or {}).get("tree_size") if isinstance(bundle, dict) else None,
|
|
592
|
+
}
|
|
556
593
|
if policy is not None:
|
|
557
594
|
fields["policy_id"] = policy.get("policy_id")
|
|
558
595
|
# WP-TP1: non-fatal honesty warnings (e.g. "attributes to nobody") — exit code unchanged.
|
|
559
596
|
# Mirror the human line (six-lens review): the warning is meaningful only for a PASSING
|
|
560
597
|
# policy (a POLICY: OK that attributes to nobody); on FAIL / crypto-FAIL the FAIL is the
|
|
561
598
|
# message, so the JSON field is empty too — the key stays present for contract stability.
|
|
562
|
-
|
|
563
|
-
fields["policy_warnings"] = policy_warnings(policy) if policy_ok else []
|
|
599
|
+
fields["policy_warnings"] = pol_warns # P0-B: computed once above (no double lint)
|
|
564
600
|
if policy_result is not None:
|
|
565
601
|
fields["policy_checks"] = policy_result["checks"]
|
|
566
602
|
if anchor_report is not None:
|
|
@@ -614,6 +650,17 @@ def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
614
650
|
f"(payload-signature {root_summary['payloadSignature']}, "
|
|
615
651
|
f"merkle-consistency {root_summary['merkleConsistency']}, "
|
|
616
652
|
f"safe-for-automation {str(root_summary['safeForAutomation']).lower()})")
|
|
653
|
+
# AP-1 §5.3: a dedicated, human-legible automation verdict — YES, or NO with the reason(s). The
|
|
654
|
+
# reasons are derived one-for-one from root_summary['automationBlockers'] via the SSOT map, so the
|
|
655
|
+
# human and JSON forms of safeForAutomation can never disagree (Iteration F).
|
|
656
|
+
from .bundle import AUTOMATION_BLOCKER_REASONS # noqa: PLC0415
|
|
657
|
+
if root_summary["safeForAutomation"]:
|
|
658
|
+
print("SAFE_FOR_AUTOMATION: YES")
|
|
659
|
+
else:
|
|
660
|
+
print("SAFE_FOR_AUTOMATION: NO")
|
|
661
|
+
print("Reason:")
|
|
662
|
+
for _blk in root_summary["automationBlockers"]:
|
|
663
|
+
print(f" {AUTOMATION_BLOCKER_REASONS.get(_blk, _blk)}")
|
|
617
664
|
if policy is not None and not crypto_ok:
|
|
618
665
|
print("POLICY: NOT_EVALUATED (crypto failed — policy not checked)")
|
|
619
666
|
else:
|
|
@@ -963,8 +1010,11 @@ def _cmd_decision_verify(args: argparse.Namespace) -> int:
|
|
|
963
1010
|
policy = None
|
|
964
1011
|
if args.policy:
|
|
965
1012
|
from .policy import PolicyError, load_policy # noqa: PLC0415
|
|
1013
|
+
from .policy_profiles import resolve_policy_source # noqa: PLC0415
|
|
966
1014
|
try:
|
|
967
|
-
|
|
1015
|
+
# parity with the eval verify path (L5 audit L5-F3): accept a packaged profile NAME
|
|
1016
|
+
# (e.g. decision-receipt-template-v1), not only a file path, via resolve_policy_source.
|
|
1017
|
+
policy = load_policy(resolve_policy_source(args.policy))
|
|
968
1018
|
except PolicyError as exc:
|
|
969
1019
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
970
1020
|
return 2
|
|
@@ -1151,17 +1201,91 @@ def _cmd_policy_lint(args: argparse.Namespace) -> int:
|
|
|
1151
1201
|
|
|
1152
1202
|
def _cmd_policy_list_profiles(args: argparse.Namespace) -> int:
|
|
1153
1203
|
from .policy import explain_policy, load_policy # noqa: PLC0415
|
|
1154
|
-
from .policy_profiles import list_profiles, profile_path # noqa: PLC0415
|
|
1204
|
+
from .policy_profiles import list_profiles, profile_aliases, profile_path # noqa: PLC0415
|
|
1205
|
+
aliases = profile_aliases()
|
|
1206
|
+
# canonical -> [deprecated old names] so each canonical row can list what still resolves to it.
|
|
1207
|
+
canonical_aliases: dict = {}
|
|
1208
|
+
for old, canonical in aliases.items():
|
|
1209
|
+
canonical_aliases.setdefault(canonical, []).append(old)
|
|
1155
1210
|
rows = []
|
|
1156
|
-
for name in list_profiles():
|
|
1211
|
+
for name in list_profiles(): # AP-2 §6.1: canonical names FIRST
|
|
1212
|
+
# load via the packaged file directly (no alias, no deprecation line) for the metadata
|
|
1157
1213
|
policy = load_policy(profile_path(name))
|
|
1158
1214
|
rows.append({"name": name, "policy_id": policy.get("policy_id"),
|
|
1159
|
-
|
|
1215
|
+
"schema": policy.get("schema"), "pin_count": len(explain_policy(policy)),
|
|
1216
|
+
"deploymentReady": policy.get("deploymentReady"),
|
|
1217
|
+
"requiresIdentityOverlay": policy.get("requiresIdentityOverlay"),
|
|
1218
|
+
"is_template": policy.get("requiresIdentityOverlay") is True
|
|
1219
|
+
or policy.get("deploymentReady") is False,
|
|
1220
|
+
"deprecated_aliases": sorted(canonical_aliases.get(name, []))})
|
|
1160
1221
|
if args.json:
|
|
1161
1222
|
print(json.dumps(rows, indent=2, ensure_ascii=False))
|
|
1162
1223
|
return 0
|
|
1163
1224
|
for row in rows:
|
|
1164
|
-
|
|
1225
|
+
kind = "template" if row["is_template"] else "profile "
|
|
1226
|
+
print(f"{row['name']:44} {kind} {row['schema']:32} {row['pin_count']} pin(s) {row['policy_id']}")
|
|
1227
|
+
if row["deprecated_aliases"]:
|
|
1228
|
+
print(f"{'':44} (deprecated aliases: {', '.join(row['deprecated_aliases'])})")
|
|
1229
|
+
return 0
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
def _read_pubkey_line(text: str) -> str:
|
|
1233
|
+
"""Extract a base64 Ed25519 public key from a key file's content: accepts a bare base64 string, an
|
|
1234
|
+
``ed25519:<b64>`` issuer string, or a JSON object with ``public_key_b64`` / ``issuer_public_key_b64``.
|
|
1235
|
+
Returns the base64 string (empty if none found — the caller fail-closes on that)."""
|
|
1236
|
+
s = text.strip()
|
|
1237
|
+
if s.startswith("{"):
|
|
1238
|
+
try:
|
|
1239
|
+
obj = json.loads(s)
|
|
1240
|
+
except ValueError:
|
|
1241
|
+
return ""
|
|
1242
|
+
return obj.get("public_key_b64") or obj.get("issuer_public_key_b64") or ""
|
|
1243
|
+
if s.startswith("ed25519:"):
|
|
1244
|
+
return s[len("ed25519:"):].strip()
|
|
1245
|
+
return s
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def _cmd_policy_instantiate(args: argparse.Namespace) -> int:
|
|
1249
|
+
"""AP-2 §6.3: turn a shipped template into a deployment-ready org policy, offline. Reads the issuer
|
|
1250
|
+
key file(s) and optional expected-root file, pins them, and writes the instantiated policy JSON."""
|
|
1251
|
+
from .policy import PolicyError # noqa: PLC0415
|
|
1252
|
+
from .policy_profiles import instantiate_template # noqa: PLC0415
|
|
1253
|
+
try:
|
|
1254
|
+
keys = []
|
|
1255
|
+
for kf in args.issuer_key:
|
|
1256
|
+
with open(kf, encoding="utf-8") as fh:
|
|
1257
|
+
key = _read_pubkey_line(fh.read())
|
|
1258
|
+
if not key:
|
|
1259
|
+
raise PolicyError(f"issuer key file {kf!r} carries no public key")
|
|
1260
|
+
keys.append(key)
|
|
1261
|
+
expected_root = None
|
|
1262
|
+
if args.expected_root_file:
|
|
1263
|
+
with open(args.expected_root_file, encoding="utf-8") as fh:
|
|
1264
|
+
expected_root = fh.read().strip()
|
|
1265
|
+
inst = instantiate_template(args.template, issuer_keys=keys, policy_id=args.policy_id,
|
|
1266
|
+
expected_root=expected_root, valid_until=args.valid_until)
|
|
1267
|
+
except (PolicyError, OSError, ValueError) as exc:
|
|
1268
|
+
if getattr(args, "json", False):
|
|
1269
|
+
print(json.dumps({"ok": False, "error": str(exc)}))
|
|
1270
|
+
else:
|
|
1271
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
1272
|
+
return 2
|
|
1273
|
+
out = json.dumps(inst, indent=2, ensure_ascii=False)
|
|
1274
|
+
if not inst.get("deploymentReady"):
|
|
1275
|
+
# honest, loud: the instance is missing a required field (e.g. an authenticated-root template
|
|
1276
|
+
# instantiated without --expected-root-file). It is written so it can be completed, but it is NOT
|
|
1277
|
+
# production-ready and `policy lint --strict` will refuse it (No-Fake).
|
|
1278
|
+
print("[policy-instantiate] WARNING: result is deploymentReady=false — a required field is "
|
|
1279
|
+
"missing (this template needs --expected-root-file); complete it before deploying.",
|
|
1280
|
+
file=sys.stderr)
|
|
1281
|
+
if args.output and args.output != "-":
|
|
1282
|
+
with open(args.output, "w", encoding="utf-8") as fh:
|
|
1283
|
+
fh.write(out + "\n")
|
|
1284
|
+
print(f"[policy-instantiate] {args.template} -> {args.output} "
|
|
1285
|
+
f"deploymentReady={inst.get('deploymentReady')} policy_id={inst.get('policy_id')}",
|
|
1286
|
+
file=sys.stderr)
|
|
1287
|
+
else:
|
|
1288
|
+
print(out)
|
|
1165
1289
|
return 0
|
|
1166
1290
|
|
|
1167
1291
|
|
|
@@ -1382,9 +1506,39 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1382
1506
|
p_lint.add_argument("--json", action="store_true", help="machine readable output")
|
|
1383
1507
|
p_lint.set_defaults(func=_cmd_policy_lint)
|
|
1384
1508
|
p_list = psub.add_parser(
|
|
1385
|
-
"list-profiles", help="list the named trust-policy profiles shipped with this package (WP3)"
|
|
1509
|
+
"list-profiles", help="list the named trust-policy profiles shipped with this package (WP3); "
|
|
1510
|
+
"canonical template names first, deprecated aliases marked")
|
|
1386
1511
|
p_list.add_argument("--json", action="store_true", help="machine readable output")
|
|
1387
1512
|
p_list.set_defaults(func=_cmd_policy_list_profiles)
|
|
1513
|
+
p_inst = psub.add_parser(
|
|
1514
|
+
"instantiate", help="AP-2 §6.3: turn a shipped TEMPLATE into a deployment-ready org policy by "
|
|
1515
|
+
"pinning your signer identity (and root, when required); offline",
|
|
1516
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1517
|
+
epilog="Example:\n"
|
|
1518
|
+
" proofbundle policy instantiate strict-eval-template-v1 \\\n"
|
|
1519
|
+
" --issuer-key org-eval.pub --policy-id org/strict-eval-v1 \\\n"
|
|
1520
|
+
" --output org-strict-eval-v1.json\n"
|
|
1521
|
+
"The result pins the issuer key(s), sets requiresIdentityOverlay=false, and is\n"
|
|
1522
|
+
"deploymentReady=true only when every required field is filled (an authenticated-root\n"
|
|
1523
|
+
"template also needs --expected-root-file). Exit 0 on success, 2 on any error.")
|
|
1524
|
+
p_inst.add_argument("template", help="a template profile name, e.g. strict-eval-template-v1 "
|
|
1525
|
+
"(deprecated aliases resolve with a warning)")
|
|
1526
|
+
p_inst.add_argument("--issuer-key", action="append", required=True, metavar="FILE",
|
|
1527
|
+
help="file with a base64 Ed25519 public key to pin (repeat to pin several)")
|
|
1528
|
+
p_inst.add_argument("--policy-id", required=True,
|
|
1529
|
+
help="the new policy_id in YOUR organisation namespace (must differ from the "
|
|
1530
|
+
"template's)")
|
|
1531
|
+
p_inst.add_argument("--expected-root-file", metavar="FILE",
|
|
1532
|
+
help="file with a base64 merkle root to pin as trusted_roots — REQUIRED for a "
|
|
1533
|
+
"template that sets merkle.require_authenticated_root, optional otherwise")
|
|
1534
|
+
p_inst.add_argument("--valid-until", metavar="ISO8601",
|
|
1535
|
+
help="optional expiry stamped onto the instance (e.g. 2027-01-01T00:00:00Z); "
|
|
1536
|
+
"policy lint --strict fails once it is in the past")
|
|
1537
|
+
p_inst.add_argument("--output", "-o", metavar="FILE",
|
|
1538
|
+
help="write the instantiated policy JSON here (default: stdout)")
|
|
1539
|
+
p_inst.add_argument("--json", action="store_true",
|
|
1540
|
+
help="on error, emit a JSON error object instead of a stderr line")
|
|
1541
|
+
p_inst.set_defaults(func=_cmd_policy_instantiate)
|
|
1388
1542
|
|
|
1389
1543
|
prereg = sub.add_parser(
|
|
1390
1544
|
"prereg",
|
|
@@ -429,16 +429,27 @@ def verify_decision_receipt(envelope: dict, public_key: bytes, *, strict: bool =
|
|
|
429
429
|
# confirmation is a separate step (resolve_evidence_ref), never implied by evidence_bound.
|
|
430
430
|
if isinstance(ev, list) and ev:
|
|
431
431
|
r["evidence_bound"] = all(isinstance(x, dict) and _is_digest(x.get("digest")) for x in ev)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
432
|
+
# 3.1.2 fail-closed fix (audit 2026-07-13, sibling of the eval-path F4 hardening and the decision
|
|
433
|
+
# template/expiry gates): a relying party who supplies expected_audience/expected_nonce is ASKING for
|
|
434
|
+
# RFC-9901-§7.3-style replay/audience binding. If the receipt carries NO validity object (or a
|
|
435
|
+
# non-dict one), the previous `if isinstance(val, dict):` guard SKIPPED the checks entirely →
|
|
436
|
+
# audience_ok/nonce_ok stayed None → the CLI exit gate (None is not False) let it pass exit 0: a
|
|
437
|
+
# requested binding silently unenforced (fail-OPEN downgrade). Evaluate against {} instead, so an
|
|
438
|
+
# absent validity/audience/nonce is a FAIL (fail-closed), never a silent pass.
|
|
439
|
+
_val = predicate.get("validity")
|
|
440
|
+
_validity = _val if isinstance(_val, dict) else {}
|
|
441
|
+
if expected_audience is not None:
|
|
442
|
+
r["audience_ok"] = expected_audience in (_validity.get("audience") or [])
|
|
443
|
+
if not r["audience_ok"]:
|
|
444
|
+
r["errors"].append(
|
|
445
|
+
"audience mismatch or absent validity.audience — requested audience binding cannot be "
|
|
446
|
+
"enforced (cross-audience replay?, fail-closed)")
|
|
447
|
+
if expected_nonce is not None:
|
|
448
|
+
r["nonce_ok"] = _validity.get("nonce") == expected_nonce
|
|
449
|
+
if not r["nonce_ok"]:
|
|
450
|
+
r["errors"].append(
|
|
451
|
+
"nonce mismatch or absent validity.nonce — requested replay binding cannot be enforced "
|
|
452
|
+
"(replay?, fail-closed)")
|
|
442
453
|
|
|
443
454
|
# Detached anchors (Fix 2): verify anchor evidence for the statement's OWN content root — sha256 over the
|
|
444
455
|
# EXACT signed payload bytes (never re-canonicalized), computed only once the bytes are authentic+canonical.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "proofbundle/trust-policy/v0.2",
|
|
3
|
+
"policy_id": "proofbundle-policy/decision-receipt-template-v1",
|
|
4
|
+
"deploymentReady": false,
|
|
5
|
+
"requiresIdentityOverlay": true,
|
|
6
|
+
"allowed_schema_versions": [
|
|
7
|
+
"proofbundle/v0.1"
|
|
8
|
+
],
|
|
9
|
+
"signature": {
|
|
10
|
+
"allowed_algs": [
|
|
11
|
+
"ed25519"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"decision_receipt": {
|
|
15
|
+
"accepted_predicate_types": [
|
|
16
|
+
"https://b7n0de.com/proofbundle/predicates/decision-receipt/v0.1"
|
|
17
|
+
],
|
|
18
|
+
"require_not_checked": true,
|
|
19
|
+
"require_decision_change_conditions": true,
|
|
20
|
+
"require_audience": true,
|
|
21
|
+
"require_nonce": true,
|
|
22
|
+
"allow_raw_inputs": false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "proofbundle/trust-policy/v0.1",
|
|
3
|
+
"policy_id": "proofbundle-policy/strict-eval-authenticated-root-template-v1",
|
|
4
|
+
"deploymentReady": false,
|
|
5
|
+
"requiresIdentityOverlay": true,
|
|
6
|
+
"allowed_schema_versions": [
|
|
7
|
+
"proofbundle/v0.1"
|
|
8
|
+
],
|
|
9
|
+
"signature": {
|
|
10
|
+
"allowed_algs": [
|
|
11
|
+
"ed25519"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"merkle": {
|
|
15
|
+
"required_hash_alg": "sha256-rfc6962",
|
|
16
|
+
"require_authenticated_root": true
|
|
17
|
+
},
|
|
18
|
+
"sd_jwt": {
|
|
19
|
+
"require_key_binding_when_cnf_present": true
|
|
20
|
+
},
|
|
21
|
+
"assurance": {
|
|
22
|
+
"minimum_level": "reproduced",
|
|
23
|
+
"reject_self_attested_without_prereg": true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "proofbundle/trust-policy/v0.1",
|
|
3
|
+
"policy_id": "proofbundle-policy/strict-eval-template-v1",
|
|
4
|
+
"deploymentReady": false,
|
|
5
|
+
"requiresIdentityOverlay": true,
|
|
6
|
+
"allowed_schema_versions": [
|
|
7
|
+
"proofbundle/v0.1"
|
|
8
|
+
],
|
|
9
|
+
"signature": {
|
|
10
|
+
"allowed_algs": [
|
|
11
|
+
"ed25519"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"merkle": {
|
|
15
|
+
"required_hash_alg": "sha256-rfc6962"
|
|
16
|
+
},
|
|
17
|
+
"sd_jwt": {
|
|
18
|
+
"require_key_binding_when_cnf_present": true
|
|
19
|
+
},
|
|
20
|
+
"assurance": {
|
|
21
|
+
"minimum_level": "reproduced",
|
|
22
|
+
"reject_self_attested_without_prereg": true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "proofbundle/trust-policy/v0.2",
|
|
3
|
+
"policy_id": "proofbundle-policy/strict-prereg-template-v1",
|
|
4
|
+
"deploymentReady": false,
|
|
5
|
+
"requiresIdentityOverlay": true,
|
|
6
|
+
"allowed_schema_versions": [
|
|
7
|
+
"proofbundle/v0.1"
|
|
8
|
+
],
|
|
9
|
+
"signature": {
|
|
10
|
+
"allowed_algs": [
|
|
11
|
+
"ed25519"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"merkle": {
|
|
15
|
+
"required_hash_alg": "sha256-rfc6962"
|
|
16
|
+
},
|
|
17
|
+
"assurance": {
|
|
18
|
+
"reject_self_attested_without_prereg": true
|
|
19
|
+
},
|
|
20
|
+
"anchors": {
|
|
21
|
+
"require_anchor": "any",
|
|
22
|
+
"require_anchor_target": "preRegistration",
|
|
23
|
+
"allow_pending": false
|
|
24
|
+
}
|
|
25
|
+
}
|