actenon-kernel 0.1.0__py3-none-any.whl
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.
- actenon/__init__.py +11 -0
- actenon/adapters/__init__.py +20 -0
- actenon/adapters/_authorization.py +30 -0
- actenon/adapters/base.py +104 -0
- actenon/adapters/edge.py +129 -0
- actenon/adapters/fastapi.py +161 -0
- actenon/adapters/langchain.py +111 -0
- actenon/adapters/mcp.py +118 -0
- actenon/anchors/__init__.py +29 -0
- actenon/anchors/local_log.py +224 -0
- actenon/anchors/models.py +153 -0
- actenon/api/__init__.py +12 -0
- actenon/api/intake.py +39 -0
- actenon/api/invoice_payment.py +143 -0
- actenon/api/refund.py +57 -0
- actenon/cli.py +2023 -0
- actenon/conformance/__init__.py +5 -0
- actenon/conformance/helpers.py +136 -0
- actenon/conformance/manifest.py +185 -0
- actenon/conformance/test_artifact_shape_conformance.py +53 -0
- actenon/conformance/test_canonicalization_strict_conformance.py +288 -0
- actenon/conformance/test_countersignature_conformance.py +95 -0
- actenon/conformance/test_execution_state_conformance.py +78 -0
- actenon/conformance/test_outcome_attestation_conformance.py +109 -0
- actenon/conformance/test_replay_conformance.py +38 -0
- actenon/conformance/test_transparency_log_conformance.py +130 -0
- actenon/conformance/test_trust_artifacts_conformance.py +107 -0
- actenon/conformance/test_verifier_sdk_conformance.py +191 -0
- actenon/conformance/vectors/canonicalization_strict_v1/cases.json +142 -0
- actenon/conformance/vectors/canonicalization_strict_v1/profile.json +24 -0
- actenon/conformance/vectors/verifier_sdk_v1/README.md +18 -0
- actenon/conformance/vectors/verifier_sdk_v1/action_intent.json +41 -0
- actenon/conformance/vectors/verifier_sdk_v1/cases.json +285 -0
- actenon/conformance/vectors/verifier_sdk_v1/pccb.json +73 -0
- actenon/conformance/version.py +6 -0
- actenon/core/__init__.py +48 -0
- actenon/core/errors.py +111 -0
- actenon/core/json.py +68 -0
- actenon/core/kernel.py +195 -0
- actenon/core/redaction.py +21 -0
- actenon/coverage_matrix.py +903 -0
- actenon/credentials/__init__.py +9 -0
- actenon/credentials/broker.py +91 -0
- actenon/demo/__init__.py +2 -0
- actenon/demo/local_proof.py +652 -0
- actenon/demo/portable_local_proof.py +135 -0
- actenon/escrow/__init__.py +16 -0
- actenon/escrow/base.py +31 -0
- actenon/escrow/memory.py +65 -0
- actenon/escrow/service.py +25 -0
- actenon/escrow/sqlite.py +261 -0
- actenon/evidence/__init__.py +24 -0
- actenon/evidence/query.py +535 -0
- actenon/evidence/stores.py +117 -0
- actenon/execution/__init__.py +32 -0
- actenon/execution/mode_aware.py +404 -0
- actenon/execution/protected_executor.py +337 -0
- actenon/execution_graph.py +129 -0
- actenon/gate.py +620 -0
- actenon/idempotency.py +162 -0
- actenon/local_runtime.py +3412 -0
- actenon/local_runtime_server.py +960 -0
- actenon/models/__init__.py +125 -0
- actenon/models/contracts.py +866 -0
- actenon/models/evidence.py +15 -0
- actenon/models/execution.py +89 -0
- actenon/models/intent_record.py +334 -0
- actenon/models/invoice_payment.py +243 -0
- actenon/models/policy_bundle.py +147 -0
- actenon/models/runtime.py +73 -0
- actenon/models/serialization.py +46 -0
- actenon/outcomes.py +234 -0
- actenon/policy/__init__.py +65 -0
- actenon/policy/engine.py +252 -0
- actenon/policy/evidence.py +92 -0
- actenon/policy/invoice_payment.py +442 -0
- actenon/policy/refund.py +177 -0
- actenon/preflight/__init__.py +57 -0
- actenon/preflight/engine.py +208 -0
- actenon/preflight/evidence.py +135 -0
- actenon/preflight/models.py +129 -0
- actenon/preflight/policy_packs.py +1050 -0
- actenon/proof/__init__.py +131 -0
- actenon/proof/audit.py +87 -0
- actenon/proof/canonical.py +102 -0
- actenon/proof/local.py +19 -0
- actenon/proof/refusal_messages.py +40 -0
- actenon/proof/service.py +612 -0
- actenon/proof/signers/__init__.py +120 -0
- actenon/proof/signers/base.py +61 -0
- actenon/proof/signers/external_managed.py +252 -0
- actenon/proof/signers/hsm.py +63 -0
- actenon/proof/signers/kms.py +68 -0
- actenon/proof/signers/local.py +124 -0
- actenon/proof/signers/proof_seal.py +220 -0
- actenon/proof/signers/well_known.py +814 -0
- actenon/proof/signing.py +10 -0
- actenon/receipts/__init__.py +47 -0
- actenon/receipts/attestation.py +499 -0
- actenon/receipts/factory.py +211 -0
- actenon/receipts/invoice_payment.py +102 -0
- actenon/receipts/refund.py +60 -0
- actenon/receipts/store.py +110 -0
- actenon/receipts/writers.py +152 -0
- actenon/reconciliation/__init__.py +19 -0
- actenon/reconciliation/base.py +114 -0
- actenon/replay/__init__.py +20 -0
- actenon/replay/base.py +64 -0
- actenon/replay/dbapi.py +442 -0
- actenon/replay/postgres.py +55 -0
- actenon/replay/service.py +84 -0
- actenon/replay/sqlite.py +33 -0
- actenon/scanner.py +3264 -0
- actenon/ui/__init__.py +2 -0
- actenon/ui/trace_viewer/__init__.py +2 -0
- actenon/ui/trace_viewer/app.py +110 -0
- actenon/ui/trace_viewer/static/app.js +253 -0
- actenon/ui/trace_viewer/static/index.html +136 -0
- actenon/ui/trace_viewer/static/styles.css +288 -0
- actenon/ui/trace_viewer/trace_loader.py +875 -0
- actenon/verifier/__init__.py +56 -0
- actenon/verifier/countersignature.py +404 -0
- actenon/verifier/endpoint.py +271 -0
- actenon/verifier/middleware.py +96 -0
- actenon/verifier/sdk.py +104 -0
- actenon/verifier/transparency.py +778 -0
- actenon/verifier/trust_artifacts.py +570 -0
- actenon_kernel-0.1.0.dist-info/METADATA +644 -0
- actenon_kernel-0.1.0.dist-info/RECORD +269 -0
- actenon_kernel-0.1.0.dist-info/WHEEL +5 -0
- actenon_kernel-0.1.0.dist-info/entry_points.txt +4 -0
- actenon_kernel-0.1.0.dist-info/licenses/LICENSE +201 -0
- actenon_kernel-0.1.0.dist-info/top_level.txt +4 -0
- conformance/CHANGELOG.md +21 -0
- conformance/MATRIX.md +27 -0
- conformance/README.md +103 -0
- conformance/VERSION +1 -0
- conformance/__init__.py +1 -0
- conformance/suite.json +108 -0
- conformance/vector-lock.json +63 -0
- conformance/vectors/cloud_invoice_payment_v1/action_intent.json +74 -0
- conformance/vectors/cloud_invoice_payment_v1/issuer_keys.json +57 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/action_hash_changed_pccb.json +78 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/amount_changed_action_intent.json +74 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/audience_changed_pccb.json +78 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/expired_pccb.json +78 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/outcome_attestation_hard_revoked_issuer_keys.json +58 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/outcome_attestation_wrong_purpose_issuer_keys.json +57 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_artifact_digest_changed.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_issued_at_changed.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_issuer_changed.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_outcome_artifact_changed.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_proof_binding_changed.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/receipt_attestation_signature_changed.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/refusal_attestation_issued_at_changed.json +56 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/refusal_attestation_issuer_changed.json +56 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/refusal_attestation_signature_changed.json +56 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/signature_changed_pccb.json +78 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/wrong_kid_pccb.json +78 -0
- conformance/vectors/cloud_invoice_payment_v1/mutations/wrong_purpose_issuer_keys.json +57 -0
- conformance/vectors/cloud_invoice_payment_v1/pccb.json +78 -0
- conformance/vectors/cloud_invoice_payment_v1/receipt.json +19 -0
- conformance/vectors/cloud_invoice_payment_v1/receipt_attestation.json +55 -0
- conformance/vectors/cloud_invoice_payment_v1/refusal.json +20 -0
- conformance/vectors/cloud_invoice_payment_v1/refusal_attestation.json +56 -0
- conformance/vectors/receipt_countersignature_v1/countersignature.json +28 -0
- conformance/vectors/receipt_countersignature_v1/mutations/countersignature_altered_digest.json +28 -0
- conformance/vectors/receipt_countersignature_v1/mutations/countersignature_unknown_kid.json +28 -0
- conformance/vectors/receipt_countersignature_v1/mutations/trusted_keys_wrong_key.json +50 -0
- conformance/vectors/receipt_countersignature_v1/receipt.json +19 -0
- conformance/vectors/receipt_countersignature_v1/trusted_keys.json +50 -0
- conformance/vectors/transparency_log_v1/checkpoint_new.json +23 -0
- conformance/vectors/transparency_log_v1/checkpoint_old.json +23 -0
- conformance/vectors/transparency_log_v1/consistency_proof.json +13 -0
- conformance/vectors/transparency_log_v1/countersignature.json +27 -0
- conformance/vectors/transparency_log_v1/inclusion_proof.json +19 -0
- conformance/vectors/transparency_log_v1/leaf_digest.json +5 -0
- conformance/vectors/transparency_log_v1/mutations/checkpoint_new_forked.json +23 -0
- conformance/vectors/transparency_log_v1/mutations/checkpoint_unknown_kid.json +23 -0
- conformance/vectors/transparency_log_v1/mutations/countersignature_orphan.json +27 -0
- conformance/vectors/transparency_log_v1/trusted_keys.json +60 -0
- conformance/vectors/trust_artifacts_v1/README.md +13 -0
- conformance/vectors/trust_artifacts_v1/action_intent.json +74 -0
- conformance/vectors/trust_artifacts_v1/approval.json +25 -0
- conformance/vectors/trust_artifacts_v1/approval_trusted_keys.json +28 -0
- conformance/vectors/trust_artifacts_v1/issuer.json +4 -0
- conformance/vectors/trust_artifacts_v1/issuer_status_expired.json +24 -0
- conformance/vectors/trust_artifacts_v1/issuer_status_good.json +24 -0
- conformance/vectors/trust_artifacts_v1/issuer_status_revoked.json +24 -0
- conformance/vectors/trust_artifacts_v1/issuer_status_stale.json +24 -0
- conformance/vectors/trust_artifacts_v1/issuer_status_trusted_keys.json +28 -0
- conformance/vectors/trust_artifacts_v1/mutations/approval_action_changed.json +25 -0
- conformance/vectors/trust_artifacts_v1/mutations/approval_signature_changed.json +25 -0
- conformance/vectors/trust_artifacts_v1/mutations/issuer_status_signature_changed.json +24 -0
- examples/__init__.py +2 -0
- examples/adversarial_rsa_policy_stress_test.py +339 -0
- examples/claude_managed_agents_protected_tool/tool.py +456 -0
- examples/credential_broker_infra_delete/__init__.py +1 -0
- examples/credential_broker_infra_delete/demo.py +171 -0
- examples/crewai_protected_tool/tool.py +160 -0
- examples/edge_templates/__init__.py +2 -0
- examples/edge_templates/_support.py +66 -0
- examples/edge_templates/ci_cd.py +14 -0
- examples/edge_templates/cloud.py +14 -0
- examples/edge_templates/communications.py +14 -0
- examples/edge_templates/database.py +14 -0
- examples/edge_templates/http_api.py +14 -0
- examples/edge_templates/iam.py +14 -0
- examples/edge_templates/ot.py +14 -0
- examples/edge_templates/storage.py +14 -0
- examples/fastapi_protected_route/app.py +114 -0
- examples/fastapi_resource_boundary_refund/__init__.py +0 -0
- examples/fastapi_resource_boundary_refund/app.py +151 -0
- examples/fastapi_resource_boundary_refund/test_resource_boundary_refund.py +92 -0
- examples/fastapi_verifier_only_refund/__init__.py +0 -0
- examples/fastapi_verifier_only_refund/app.py +122 -0
- examples/fastapi_verifier_only_refund/issuer.py +47 -0
- examples/fastapi_verifier_only_refund/test_verifier_only_refund.py +58 -0
- examples/fastmcp_financial_transfer/__init__.py +1 -0
- examples/fastmcp_financial_transfer/mcp_server.py +88 -0
- examples/fastmcp_financial_transfer/test_fastmcp_financial_transfer.py +120 -0
- examples/financial_agent_protected_transfer/financial_agent.py +182 -0
- examples/financial_agent_protected_transfer/test_financial_agent.py +132 -0
- examples/hello_protected_endpoint/server.py +113 -0
- examples/hello_world_protected_resource_python/__init__.py +1 -0
- examples/hello_world_protected_resource_python/protected_resource.py +49 -0
- examples/integration_support.py +123 -0
- examples/interactive_execution_demo.py +139 -0
- examples/invoice_payment_guard_local/__init__.py +1 -0
- examples/invoice_payment_guard_local/protected_endpoint.py +376 -0
- examples/langchain_protected_tool/tool.py +123 -0
- examples/llamaindex_protected_tool/tool.py +142 -0
- examples/mcp_protected_tool/__init__.py +2 -0
- examples/mcp_protected_tool/demo.py +269 -0
- examples/mcp_server_protected_tool/__init__.py +2 -0
- examples/mcp_server_protected_tool/demo.py +66 -0
- examples/mcp_server_protected_tool/proof_gate.py +463 -0
- examples/mcp_server_protected_tool/server.py +208 -0
- examples/openai_agents_sdk_protected_tool/app.py +104 -0
- examples/protected_clinical_ehr_agent/protected_clinical_ehr_agent.py +274 -0
- examples/protected_clinical_ehr_agent/test_protected_clinical_ehr_agent.py +25 -0
- examples/protected_iam_control_plane/protected_iam_control_plane.py +273 -0
- examples/protected_iam_control_plane/test_protected_iam_control_plane.py +21 -0
- examples/protected_langchain_finance_agent/protected_langchain_finance_agent.py +421 -0
- examples/protected_langchain_finance_agent/test_protected_langchain_finance_agent.py +27 -0
- examples/protected_multi_agent_swarm/protected_multi_agent_swarm.py +233 -0
- examples/protected_multi_agent_swarm/test_protected_multi_agent_swarm.py +20 -0
- examples/protected_policy_preflight_refund/__init__.py +0 -0
- examples/protected_policy_preflight_refund/policy_preflight_refund.py +87 -0
- examples/protected_policy_preflight_refund/test_policy_preflight_refund.py +76 -0
- examples/quickstart_min.py +16 -0
- examples/refund_guard_local/__init__.py +2 -0
- examples/refund_guard_local/call_endpoint.py +113 -0
- examples/refund_guard_local/protected_endpoint.py +87 -0
- examples/refund_guard_local/server.py +560 -0
- examples/semantic_kernel_protected_tool/tool.py +141 -0
- schemas/__init__.py +1 -0
- schemas/action_intent.v1.json +256 -0
- schemas/approval_artifact.v1.json +62 -0
- schemas/issuer_status.v1.json +53 -0
- schemas/pccb.v1.json +336 -0
- schemas/receipt.v1.json +343 -0
- schemas/receipt_attestation.v2alpha1.json +237 -0
- schemas/receipt_countersignature.v1.json +150 -0
- schemas/refusal.v1.json +348 -0
- schemas/refusal_attestation.v2alpha1.json +237 -0
- schemas/transparency_checkpoint.v1.json +51 -0
- schemas/transparency_consistency_proof.v1.json +28 -0
- schemas/transparency_inclusion_proof.v1.json +42 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Shared helpers for the packaged conformance suite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from os import PathLike
|
|
8
|
+
|
|
9
|
+
from actenon.api import ActionIntentIntakeService
|
|
10
|
+
from actenon.core import ProtectedExecutionKernel
|
|
11
|
+
from actenon.escrow import InMemoryCapabilityEscrow
|
|
12
|
+
from actenon.models import AudienceRef, DynamicContextInput, PartyRef, PolicyDecision
|
|
13
|
+
from actenon.policy import (
|
|
14
|
+
CapabilityScopeHardRule,
|
|
15
|
+
HardRuleEngine,
|
|
16
|
+
IntentChronologyHardRule,
|
|
17
|
+
IntentTtlHardRule,
|
|
18
|
+
PolicyEngine,
|
|
19
|
+
TenantWorkflowRule,
|
|
20
|
+
TenantWorkflowRuleLayer,
|
|
21
|
+
)
|
|
22
|
+
from actenon.proof import HmacSha256Signer, PCCBMinter, PCCBVerifier, VerifierDisclosureMode, build_local_proof_signer
|
|
23
|
+
from actenon.receipts import InMemoryOutcomeWriter, ReceiptFactory, RefusalFactory
|
|
24
|
+
from actenon.replay import ReplayProtector, SqliteReplayStore
|
|
25
|
+
from actenon.verifier import ProtectedEndpointMiddleware, VerifierSDK
|
|
26
|
+
from actenon.demo.portable_local_proof import FIXED_BASE_TIME, build_hello_world_action_intent_payload
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_verified_materials():
|
|
30
|
+
"""Return a valid portable proof-verification tuple for conformance tests."""
|
|
31
|
+
|
|
32
|
+
signer = build_local_proof_signer()
|
|
33
|
+
sdk = VerifierSDK(signer)
|
|
34
|
+
intake = ActionIntentIntakeService()
|
|
35
|
+
payload = build_hello_world_action_intent_payload()
|
|
36
|
+
intent = intake.parse(payload)
|
|
37
|
+
context = sdk.build_context(
|
|
38
|
+
request_id="req_conformance_001",
|
|
39
|
+
audience=AudienceRef(type="service", id="portable-hello-world-endpoint"),
|
|
40
|
+
now=FIXED_BASE_TIME,
|
|
41
|
+
scope_capabilities=("protected_resource.read",),
|
|
42
|
+
parameter_constraints={"exact_message": "portable hello world"},
|
|
43
|
+
resource_selectors=({"resource_id": "hello_resource_demo_001"},),
|
|
44
|
+
)
|
|
45
|
+
pccb = PCCBMinter(
|
|
46
|
+
signer=signer,
|
|
47
|
+
issuer=intent.requester,
|
|
48
|
+
pccb_id_factory=lambda: "pccb_conformance_001",
|
|
49
|
+
nonce_factory=lambda: "nonce-conformance-00000001",
|
|
50
|
+
).mint(
|
|
51
|
+
intent,
|
|
52
|
+
decision=PolicyDecision(
|
|
53
|
+
outcome="allow",
|
|
54
|
+
summary="Conformance test allow.",
|
|
55
|
+
rule_evaluations=(),
|
|
56
|
+
reason_codes=("LOCAL_PROOF_ALLOW",),
|
|
57
|
+
),
|
|
58
|
+
context=context,
|
|
59
|
+
)
|
|
60
|
+
return signer, sdk, intake, payload, pccb.to_dict(), context
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_replay_kernel(tempdir: str | PathLike[str]):
|
|
64
|
+
"""Create a replay-enabled kernel harness for conformance tests."""
|
|
65
|
+
|
|
66
|
+
replay_db = Path(tempdir) / "conformance-replay.sqlite3"
|
|
67
|
+
now = datetime.now(timezone.utc)
|
|
68
|
+
payload = {
|
|
69
|
+
"contract": {"name": "action_intent", "version": "v1"},
|
|
70
|
+
"intent_id": "intent_conformance_replay_001",
|
|
71
|
+
"issued_at": now.isoformat().replace("+00:00", "Z"),
|
|
72
|
+
"expires_at": (now + timedelta(minutes=5)).isoformat().replace("+00:00", "Z"),
|
|
73
|
+
"tenant": {"tenant_id": "tenant_alpha"},
|
|
74
|
+
"requester": {"type": "service", "id": "actor_123"},
|
|
75
|
+
"action": {
|
|
76
|
+
"name": "refund.create",
|
|
77
|
+
"capability": "refund.execute",
|
|
78
|
+
"parameters": {"amount_minor": 1000, "currency": "USD"},
|
|
79
|
+
},
|
|
80
|
+
"target": {"resource_type": "payment", "resource_id": "pay_001"},
|
|
81
|
+
}
|
|
82
|
+
context = DynamicContextInput(
|
|
83
|
+
request_id="req_conformance_replay_001",
|
|
84
|
+
audience=AudienceRef(type="service", id="protected-endpoint"),
|
|
85
|
+
scope_capabilities=("refund.execute",),
|
|
86
|
+
now=now,
|
|
87
|
+
facts={"risk_level": "normal"},
|
|
88
|
+
)
|
|
89
|
+
signer = HmacSha256Signer(secret=b"conformance-secret", key_id="local-conformance")
|
|
90
|
+
writer = InMemoryOutcomeWriter()
|
|
91
|
+
receipt_factory = ReceiptFactory(receipt_id_factory=lambda: "rcpt_conformance")
|
|
92
|
+
refusal_factory = RefusalFactory(refusal_id_factory=lambda: "rfsl_conformance")
|
|
93
|
+
escrow = InMemoryCapabilityEscrow()
|
|
94
|
+
replay_protector = ReplayProtector(SqliteReplayStore(replay_db))
|
|
95
|
+
policy = PolicyEngine(
|
|
96
|
+
hard_rules=HardRuleEngine((IntentChronologyHardRule(), IntentTtlHardRule(), CapabilityScopeHardRule())),
|
|
97
|
+
tenant_workflow_rules=TenantWorkflowRuleLayer(
|
|
98
|
+
tenant_rules={
|
|
99
|
+
"tenant_alpha": (
|
|
100
|
+
TenantWorkflowRule(
|
|
101
|
+
rule_id="tenant_alpha.refund.allow",
|
|
102
|
+
outcome="allow",
|
|
103
|
+
summary="The tenant workflow authorizes this action.",
|
|
104
|
+
reason_code="WORKFLOW_ALLOW",
|
|
105
|
+
capabilities=("refund.execute",),
|
|
106
|
+
required_fact_values={"risk_level": "normal"},
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
middleware = ProtectedEndpointMiddleware(
|
|
113
|
+
proof_verifier=PCCBVerifier(signer, disclosure_mode=VerifierDisclosureMode.LOCAL_DEBUG),
|
|
114
|
+
escrow=escrow,
|
|
115
|
+
receipt_factory=receipt_factory,
|
|
116
|
+
refusal_factory=refusal_factory,
|
|
117
|
+
outcome_writer=writer,
|
|
118
|
+
replay_protector=replay_protector,
|
|
119
|
+
)
|
|
120
|
+
kernel = ProtectedExecutionKernel(
|
|
121
|
+
intake=ActionIntentIntakeService(),
|
|
122
|
+
policy_engine=policy,
|
|
123
|
+
pccb_minter=PCCBMinter(
|
|
124
|
+
signer=signer,
|
|
125
|
+
issuer=PartyRef(type="service", id="kernel"),
|
|
126
|
+
pccb_id_factory=lambda: "pccb_conformance_replay_001",
|
|
127
|
+
nonce_factory=lambda: "nonce-conformance-replay-0001",
|
|
128
|
+
),
|
|
129
|
+
escrow=escrow,
|
|
130
|
+
middleware=middleware,
|
|
131
|
+
receipt_factory=receipt_factory,
|
|
132
|
+
refusal_factory=refusal_factory,
|
|
133
|
+
outcome_writer=writer,
|
|
134
|
+
escrow_id_factory=lambda: "esc_conformance_replay_001",
|
|
135
|
+
)
|
|
136
|
+
return kernel, writer, payload, context
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Validation helpers for the versioned public conformance vectors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
|
|
12
|
+
from .version import CONFORMANCE_VERSION, VERIFIED_MARK
|
|
13
|
+
|
|
14
|
+
SEMVER_PATTERN = re.compile(
|
|
15
|
+
r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
|
|
16
|
+
r"(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$"
|
|
17
|
+
)
|
|
18
|
+
REQUIRED_VECTOR_SETS = {
|
|
19
|
+
"verifier_sdk_v1",
|
|
20
|
+
"receipt_countersignature_v1",
|
|
21
|
+
"transparency_log_v1",
|
|
22
|
+
"trust_artifacts_v1",
|
|
23
|
+
}
|
|
24
|
+
REQUIRED_SDKS = {"python", "typescript", "go", "rust"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConformanceManifestError(ValueError):
|
|
28
|
+
"""Raised when versioned conformance metadata is incomplete or altered."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ConformanceManifest:
|
|
33
|
+
version: str
|
|
34
|
+
release_tag: str
|
|
35
|
+
verified_mark: str
|
|
36
|
+
vector_file_count: int
|
|
37
|
+
vector_sets: tuple[str, ...]
|
|
38
|
+
sdk_targets: tuple[str, ...]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _load_json(path: Path) -> dict[str, Any]:
|
|
42
|
+
try:
|
|
43
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
44
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
45
|
+
raise ConformanceManifestError(f"cannot load {path}: {exc}") from exc
|
|
46
|
+
if not isinstance(value, dict):
|
|
47
|
+
raise ConformanceManifestError(f"{path} must contain a JSON object")
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _require_string(value: object, field: str) -> str:
|
|
52
|
+
if not isinstance(value, str) or not value:
|
|
53
|
+
raise ConformanceManifestError(f"{field} must be a non-empty string")
|
|
54
|
+
return value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _sha256(path: Path) -> str:
|
|
58
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def validate_conformance_manifest(repo_root: Path) -> ConformanceManifest:
|
|
62
|
+
"""Validate suite metadata, version agreement, vector coverage, and hashes."""
|
|
63
|
+
|
|
64
|
+
conformance_root = repo_root / "conformance"
|
|
65
|
+
version = (conformance_root / "VERSION").read_text(encoding="utf-8").strip()
|
|
66
|
+
if not SEMVER_PATTERN.fullmatch(version):
|
|
67
|
+
raise ConformanceManifestError(
|
|
68
|
+
f"conformance/VERSION must be semantic versioning, got {version!r}"
|
|
69
|
+
)
|
|
70
|
+
if version != CONFORMANCE_VERSION:
|
|
71
|
+
raise ConformanceManifestError(
|
|
72
|
+
"conformance/VERSION and actenon.conformance.CONFORMANCE_VERSION differ"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
suite = _load_json(conformance_root / "suite.json")
|
|
76
|
+
lock = _load_json(conformance_root / "vector-lock.json")
|
|
77
|
+
if suite.get("conformance_version") != version:
|
|
78
|
+
raise ConformanceManifestError("suite.json conformance_version does not match VERSION")
|
|
79
|
+
if lock.get("conformance_version") != version:
|
|
80
|
+
raise ConformanceManifestError(
|
|
81
|
+
"vector-lock.json conformance_version does not match VERSION"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
release_tag = _require_string(suite.get("release_tag"), "suite.json release_tag")
|
|
85
|
+
if release_tag != f"conformance-v{version}":
|
|
86
|
+
raise ConformanceManifestError(
|
|
87
|
+
f"release_tag must be conformance-v{version}, got {release_tag!r}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
mark = suite.get("verified_mark")
|
|
91
|
+
if not isinstance(mark, Mapping):
|
|
92
|
+
raise ConformanceManifestError("suite.json verified_mark must be an object")
|
|
93
|
+
mark_claim = _require_string(mark.get("claim"), "verified_mark.claim")
|
|
94
|
+
if mark_claim != VERIFIED_MARK or version not in mark_claim:
|
|
95
|
+
raise ConformanceManifestError(
|
|
96
|
+
"Actenon Verified claim must name the exact conformance version"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
raw_sets = suite.get("vector_sets")
|
|
100
|
+
if not isinstance(raw_sets, list) or not raw_sets:
|
|
101
|
+
raise ConformanceManifestError("suite.json vector_sets must be a non-empty array")
|
|
102
|
+
|
|
103
|
+
vector_sets: list[str] = []
|
|
104
|
+
expected_files: set[str] = set()
|
|
105
|
+
for item in raw_sets:
|
|
106
|
+
if not isinstance(item, Mapping):
|
|
107
|
+
raise ConformanceManifestError("every vector_sets entry must be an object")
|
|
108
|
+
vector_id = _require_string(item.get("id"), "vector_sets[].id")
|
|
109
|
+
vector_path = _require_string(item.get("path"), f"vector set {vector_id} path")
|
|
110
|
+
vector_sets.append(vector_id)
|
|
111
|
+
root = repo_root / vector_path
|
|
112
|
+
if not root.is_dir():
|
|
113
|
+
raise ConformanceManifestError(
|
|
114
|
+
f"vector set {vector_id} directory does not exist: {vector_path}"
|
|
115
|
+
)
|
|
116
|
+
files = {
|
|
117
|
+
path.relative_to(repo_root).as_posix()
|
|
118
|
+
for path in root.rglob("*.json")
|
|
119
|
+
if path.is_file()
|
|
120
|
+
}
|
|
121
|
+
if not files:
|
|
122
|
+
raise ConformanceManifestError(f"vector set {vector_id} has no JSON vectors")
|
|
123
|
+
expected_files.update(files)
|
|
124
|
+
|
|
125
|
+
targets = item.get("sdk_targets")
|
|
126
|
+
if vector_id in REQUIRED_VECTOR_SETS:
|
|
127
|
+
if not isinstance(targets, list) or set(targets) != REQUIRED_SDKS:
|
|
128
|
+
raise ConformanceManifestError(
|
|
129
|
+
f"vector set {vector_id} must target Python, TypeScript, Go, and Rust"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
missing_sets = REQUIRED_VECTOR_SETS.difference(vector_sets)
|
|
133
|
+
if missing_sets:
|
|
134
|
+
raise ConformanceManifestError(
|
|
135
|
+
f"suite.json is missing required vector sets: {sorted(missing_sets)}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
sdk_entries = suite.get("sdk_targets")
|
|
139
|
+
if not isinstance(sdk_entries, list):
|
|
140
|
+
raise ConformanceManifestError("suite.json sdk_targets must be an array")
|
|
141
|
+
sdk_targets = tuple(
|
|
142
|
+
_require_string(item.get("id"), "sdk_targets[].id")
|
|
143
|
+
for item in sdk_entries
|
|
144
|
+
if isinstance(item, Mapping)
|
|
145
|
+
)
|
|
146
|
+
if set(sdk_targets) != REQUIRED_SDKS:
|
|
147
|
+
raise ConformanceManifestError(
|
|
148
|
+
"suite.json must declare Python, TypeScript, Go, and Rust SDK targets"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
locked_files = lock.get("files")
|
|
152
|
+
if not isinstance(locked_files, Mapping):
|
|
153
|
+
raise ConformanceManifestError("vector-lock.json files must be an object")
|
|
154
|
+
if set(locked_files) != expected_files:
|
|
155
|
+
missing = sorted(expected_files.difference(locked_files))
|
|
156
|
+
extra = sorted(set(locked_files).difference(expected_files))
|
|
157
|
+
raise ConformanceManifestError(
|
|
158
|
+
f"vector lock file set differs; missing={missing}, extra={extra}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
for relative_path, expected_hash in locked_files.items():
|
|
162
|
+
if not isinstance(relative_path, str) or not isinstance(expected_hash, str):
|
|
163
|
+
raise ConformanceManifestError("vector lock paths and hashes must be strings")
|
|
164
|
+
actual_hash = _sha256(repo_root / relative_path)
|
|
165
|
+
if actual_hash != expected_hash:
|
|
166
|
+
raise ConformanceManifestError(
|
|
167
|
+
f"vector hash mismatch for {relative_path}: "
|
|
168
|
+
f"expected {expected_hash}, got {actual_hash}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return ConformanceManifest(
|
|
172
|
+
version=version,
|
|
173
|
+
release_tag=release_tag,
|
|
174
|
+
verified_mark=mark_claim,
|
|
175
|
+
vector_file_count=len(expected_files),
|
|
176
|
+
vector_sets=tuple(vector_sets),
|
|
177
|
+
sdk_targets=sdk_targets,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
__all__ = [
|
|
182
|
+
"ConformanceManifest",
|
|
183
|
+
"ConformanceManifestError",
|
|
184
|
+
"validate_conformance_manifest",
|
|
185
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Artifact-shape checks for the packaged conformance suite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from tempfile import TemporaryDirectory
|
|
9
|
+
|
|
10
|
+
from actenon.demo.local_proof import run_invoice_payment_local_proof_demo, run_local_proof_demo
|
|
11
|
+
from actenon.models import Receipt, Refusal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ArtifactShapeConformanceTests(unittest.TestCase):
|
|
15
|
+
def test_refund_execution_receipt_matches_public_shape(self) -> None:
|
|
16
|
+
with TemporaryDirectory() as tempdir:
|
|
17
|
+
artifact_root = Path(tempdir) / "refund"
|
|
18
|
+
run_local_proof_demo(artifact_root)
|
|
19
|
+
payload = json.loads((artifact_root / "scenarios" / "allow" / "execution_receipt.json").read_text(encoding="utf-8"))
|
|
20
|
+
receipt = Receipt.from_dict(payload)
|
|
21
|
+
|
|
22
|
+
self.assertEqual("receipt", payload["contract"]["name"])
|
|
23
|
+
self.assertEqual("executed", receipt.outcome)
|
|
24
|
+
self.assertEqual("execution", receipt.phase)
|
|
25
|
+
self.assertEqual("completed", receipt.side_effects["state"])
|
|
26
|
+
|
|
27
|
+
def test_invoice_payment_execution_receipt_exposes_portable_reconciliation_fields(self) -> None:
|
|
28
|
+
with TemporaryDirectory() as tempdir:
|
|
29
|
+
artifact_root = Path(tempdir) / "invoice"
|
|
30
|
+
run_invoice_payment_local_proof_demo(artifact_root)
|
|
31
|
+
payload = json.loads((artifact_root / "scenarios" / "allow" / "execution_receipt.json").read_text(encoding="utf-8"))
|
|
32
|
+
receipt = Receipt.from_dict(payload)
|
|
33
|
+
|
|
34
|
+
self.assertEqual("receipt", payload["contract"]["name"])
|
|
35
|
+
self.assertEqual("executed", receipt.outcome)
|
|
36
|
+
self.assertTrue(receipt.side_effects["provider_reference"].startswith("provider_local_"))
|
|
37
|
+
self.assertEqual("recorded-local", receipt.side_effects["reconciliation_status"])
|
|
38
|
+
|
|
39
|
+
def test_policy_refusal_matches_public_shape(self) -> None:
|
|
40
|
+
with TemporaryDirectory() as tempdir:
|
|
41
|
+
artifact_root = Path(tempdir) / "refund"
|
|
42
|
+
run_local_proof_demo(artifact_root)
|
|
43
|
+
payload = json.loads((artifact_root / "scenarios" / "deny" / "refusal.json").read_text(encoding="utf-8"))
|
|
44
|
+
refusal = Refusal.from_dict(payload)
|
|
45
|
+
|
|
46
|
+
self.assertEqual("refusal", payload["contract"]["name"])
|
|
47
|
+
self.assertEqual("policy", refusal.category)
|
|
48
|
+
self.assertEqual("WORKFLOW_DENY", refusal.reason_code)
|
|
49
|
+
self.assertFalse(refusal.retryable)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
unittest.main()
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Conformance tests for the ACTENON-JCS-STRICT-1 canonicalisation profile.
|
|
2
|
+
|
|
3
|
+
Tests all 17 positive and negative cases defined in
|
|
4
|
+
canonicalization_strict_v1/cases.json.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import unittest
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
|
|
15
|
+
from actenon.core.json import JSONInputTooLargeError, JSONNestingDepthError
|
|
16
|
+
from actenon.models import ActionHashSpec
|
|
17
|
+
from actenon.proof.canonical import (
|
|
18
|
+
DEFAULT_MAX_CANONICAL_OUTPUT_BYTES,
|
|
19
|
+
canonicalize_bytes,
|
|
20
|
+
canonicalize_json,
|
|
21
|
+
)
|
|
22
|
+
from actenon.proof.service import build_action_hash_input
|
|
23
|
+
|
|
24
|
+
# Use inline test fixtures (not imported from tests/) so this
|
|
25
|
+
# conformance test works when installed as a package.
|
|
26
|
+
from datetime import datetime, timedelta, timezone
|
|
27
|
+
|
|
28
|
+
from actenon.models import (
|
|
29
|
+
ActionIntent,
|
|
30
|
+
ActionSpec,
|
|
31
|
+
AudienceRef,
|
|
32
|
+
DynamicContextInput,
|
|
33
|
+
PartyRef,
|
|
34
|
+
PolicyDecision,
|
|
35
|
+
TargetRef,
|
|
36
|
+
TenantRef,
|
|
37
|
+
)
|
|
38
|
+
from actenon.proof import HmacSha256Signer, PCCBMinter
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_NOW = datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _security_signer() -> HmacSha256Signer:
|
|
45
|
+
return HmacSha256Signer(secret=b"actenon-security-test-secret", key_id="security-hs256")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _build_security_intent(
|
|
49
|
+
*,
|
|
50
|
+
intent_id: str = "intent_security_001",
|
|
51
|
+
tenant_id: str = "tenant_security",
|
|
52
|
+
requester_id: str = "agent_security",
|
|
53
|
+
amount_minor: int = 1000,
|
|
54
|
+
capability: str = "payment.release",
|
|
55
|
+
target_id: str = "payment_001",
|
|
56
|
+
issued_at: datetime = _NOW,
|
|
57
|
+
expires_at: datetime | None = None,
|
|
58
|
+
) -> ActionIntent:
|
|
59
|
+
return ActionIntent(
|
|
60
|
+
intent_id=intent_id,
|
|
61
|
+
issued_at=issued_at,
|
|
62
|
+
expires_at=expires_at or issued_at + timedelta(minutes=5),
|
|
63
|
+
tenant=TenantRef(tenant_id=tenant_id),
|
|
64
|
+
requester=PartyRef(type="agent", id=requester_id),
|
|
65
|
+
action=ActionSpec(
|
|
66
|
+
name=capability,
|
|
67
|
+
capability=capability,
|
|
68
|
+
parameters={"amount_minor": amount_minor, "currency": "USD", "payment_id": target_id},
|
|
69
|
+
constraints={"exact_amount_minor": amount_minor, "exact_currency": "USD"},
|
|
70
|
+
),
|
|
71
|
+
target=TargetRef(resource_type="payment", resource_id=target_id),
|
|
72
|
+
justification="Canonicalisation conformance test action.",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_security_context(
|
|
77
|
+
*,
|
|
78
|
+
request_id: str = "req_security_001",
|
|
79
|
+
audience_id: str = "payment-release-endpoint",
|
|
80
|
+
now: datetime = _NOW,
|
|
81
|
+
scope_capabilities: tuple[str, ...] = ("payment.release",),
|
|
82
|
+
) -> DynamicContextInput:
|
|
83
|
+
return DynamicContextInput(
|
|
84
|
+
request_id=request_id,
|
|
85
|
+
audience=AudienceRef(type="service", id=audience_id),
|
|
86
|
+
scope_capabilities=scope_capabilities,
|
|
87
|
+
now=now,
|
|
88
|
+
parameter_constraints={"exact_amount_minor": 1000, "exact_currency": "USD"},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _mint_security_pccb():
|
|
93
|
+
from dataclasses import replace
|
|
94
|
+
|
|
95
|
+
signer = _security_signer()
|
|
96
|
+
intent = _build_security_intent()
|
|
97
|
+
context = _build_security_context()
|
|
98
|
+
return PCCBMinter(
|
|
99
|
+
signer=signer,
|
|
100
|
+
issuer=PartyRef(type="service", id="actenon-security-issuer"),
|
|
101
|
+
pccb_id_factory=lambda: "pccb_security_001",
|
|
102
|
+
nonce_factory=lambda: "nonce-security-001",
|
|
103
|
+
).mint(
|
|
104
|
+
intent,
|
|
105
|
+
decision=PolicyDecision(
|
|
106
|
+
outcome="allow",
|
|
107
|
+
summary="Security test allow.",
|
|
108
|
+
rule_evaluations=(),
|
|
109
|
+
reason_codes=("SECURITY_TEST_ALLOW",),
|
|
110
|
+
),
|
|
111
|
+
context=context,
|
|
112
|
+
escrow_id="esc_security_001",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
VECTOR_ROOT = Path(__file__).resolve().parent / "vectors" / "canonicalization_strict_v1"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _load_json(relative_path: str):
|
|
120
|
+
return json.loads((VECTOR_ROOT / relative_path).read_text(encoding="utf-8"))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CanonicalizationStrictConformanceTests(unittest.TestCase):
|
|
124
|
+
"""Conformance tests for ACTENON-JCS-STRICT-1."""
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def setUpClass(cls) -> None:
|
|
128
|
+
cls.profile = _load_json("profile.json")
|
|
129
|
+
cls.cases = _load_json("cases.json")
|
|
130
|
+
|
|
131
|
+
# ── Positive cases ────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
def test_01_deterministic_key_ordering(self) -> None:
|
|
134
|
+
case = self._case("deterministic_key_ordering")
|
|
135
|
+
result = canonicalize_json(case["input"])
|
|
136
|
+
self.assertEqual(case["expected_output"], result)
|
|
137
|
+
|
|
138
|
+
def test_02_unicode_strings(self) -> None:
|
|
139
|
+
case = self._case("unicode_strings")
|
|
140
|
+
result = canonicalize_json(case["input"])
|
|
141
|
+
self.assertEqual(case["expected_output"], result)
|
|
142
|
+
|
|
143
|
+
def test_03_nested_objects_and_lists(self) -> None:
|
|
144
|
+
case = self._case("nested_objects_and_lists")
|
|
145
|
+
result = canonicalize_json(case["input"])
|
|
146
|
+
self.assertEqual(case["expected_output"], result)
|
|
147
|
+
|
|
148
|
+
def test_04_maximum_accepted_depth(self) -> None:
|
|
149
|
+
"""Nesting at the maximum depth (128) must be accepted.
|
|
150
|
+
validate_json_depth counts the root level as depth 1, so 128
|
|
151
|
+
levels means 127 nested dicts (root + 127 = 128).
|
|
152
|
+
"""
|
|
153
|
+
value: object = "leaf"
|
|
154
|
+
for _ in range(127): # 127 nested dicts + root = 128 total
|
|
155
|
+
value = {"k": value}
|
|
156
|
+
result = canonicalize_json(value)
|
|
157
|
+
self.assertIsInstance(result, str)
|
|
158
|
+
self.assertGreater(len(result), 0)
|
|
159
|
+
|
|
160
|
+
def test_05_excessive_depth_rejection(self) -> None:
|
|
161
|
+
"""Nesting deeper than 128 levels must be rejected."""
|
|
162
|
+
value: object = "leaf"
|
|
163
|
+
for _ in range(129):
|
|
164
|
+
value = {"k": value}
|
|
165
|
+
with self.assertRaises(JSONNestingDepthError):
|
|
166
|
+
canonicalize_json(value)
|
|
167
|
+
|
|
168
|
+
def test_06_maximum_canonical_output_size(self) -> None:
|
|
169
|
+
"""Canonical output at the maximum size (1 MB) must be accepted.
|
|
170
|
+
The JSON encoding of {"s":"..."} is 7 + len(string) bytes.
|
|
171
|
+
We use a size that fits just within the limit.
|
|
172
|
+
"""
|
|
173
|
+
target_size = DEFAULT_MAX_CANONICAL_OUTPUT_BYTES - 8
|
|
174
|
+
value = {"s": "A" * target_size}
|
|
175
|
+
result = canonicalize_bytes(value)
|
|
176
|
+
self.assertLessEqual(len(result), DEFAULT_MAX_CANONICAL_OUTPUT_BYTES)
|
|
177
|
+
|
|
178
|
+
def test_07_excessive_output_size_rejection(self) -> None:
|
|
179
|
+
"""Canonical output exceeding 1 MB must be rejected."""
|
|
180
|
+
target_size = DEFAULT_MAX_CANONICAL_OUTPUT_BYTES + 100
|
|
181
|
+
value = {"s": "A" * target_size}
|
|
182
|
+
with self.assertRaises(JSONInputTooLargeError):
|
|
183
|
+
canonicalize_bytes(value)
|
|
184
|
+
|
|
185
|
+
def test_08_normal_integer_values(self) -> None:
|
|
186
|
+
case = self._case("normal_integer_values")
|
|
187
|
+
result = canonicalize_json(case["input"])
|
|
188
|
+
self.assertEqual(case["expected_output"], result)
|
|
189
|
+
|
|
190
|
+
def test_09_integer_boundaries(self) -> None:
|
|
191
|
+
case = self._case("integer_boundaries")
|
|
192
|
+
result = canonicalize_json(case["input"])
|
|
193
|
+
self.assertEqual(case["expected_output"], result)
|
|
194
|
+
|
|
195
|
+
# ── Negative cases ────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
def test_10_floating_point_rejection(self) -> None:
|
|
198
|
+
with self.assertRaises(TypeError):
|
|
199
|
+
canonicalize_json({"value": 3.14})
|
|
200
|
+
|
|
201
|
+
def test_11_nan_rejection(self) -> None:
|
|
202
|
+
with self.assertRaises(TypeError):
|
|
203
|
+
canonicalize_json({"value": float("nan")})
|
|
204
|
+
|
|
205
|
+
def test_12_positive_infinity_rejection(self) -> None:
|
|
206
|
+
with self.assertRaises(TypeError):
|
|
207
|
+
canonicalize_json({"value": float("inf")})
|
|
208
|
+
|
|
209
|
+
def test_13_negative_infinity_rejection(self) -> None:
|
|
210
|
+
with self.assertRaises(TypeError):
|
|
211
|
+
canonicalize_json({"value": float("-inf")})
|
|
212
|
+
|
|
213
|
+
def test_14_non_string_object_key_rejection(self) -> None:
|
|
214
|
+
with self.assertRaises(TypeError):
|
|
215
|
+
canonicalize_json({1: "value"})
|
|
216
|
+
|
|
217
|
+
# ── Profile / proof integration cases ─────────────────────────────
|
|
218
|
+
|
|
219
|
+
def test_15_legacy_proof_verification(self) -> None:
|
|
220
|
+
"""Proofs with action_hash.canonicalization='RFC8785-JCS' must
|
|
221
|
+
continue to verify using the same canonicalisation logic.
|
|
222
|
+
"""
|
|
223
|
+
pccb = _mint_security_pccb()
|
|
224
|
+
# The minted PCCB carries the legacy identifier
|
|
225
|
+
# (updated to ACTENON-JCS-STRICT-1 in the feature commit, but
|
|
226
|
+
# historical proofs still carry RFC8785-JCS)
|
|
227
|
+
# For now, verify the canonicalisation logic itself is the same:
|
|
228
|
+
intent = _build_security_intent()
|
|
229
|
+
context = _build_security_context()
|
|
230
|
+
expected_hash_input = build_action_hash_input(intent)
|
|
231
|
+
# Canonicalisation must produce the same bytes regardless of
|
|
232
|
+
# whether we call it 'RFC8785-JCS' or 'ACTENON-JCS-STRICT-1'
|
|
233
|
+
result = canonicalize_bytes(expected_hash_input)
|
|
234
|
+
self.assertIsInstance(result, bytes)
|
|
235
|
+
self.assertGreater(len(result), 0)
|
|
236
|
+
|
|
237
|
+
def test_16_new_profile_proof_minting_and_verification(self) -> None:
|
|
238
|
+
"""New proofs must be minted with action_hash.canonicalization
|
|
239
|
+
= 'ACTENON-JCS-STRICT-1' and verify successfully.
|
|
240
|
+
"""
|
|
241
|
+
pccb = _mint_security_pccb()
|
|
242
|
+
# After the feature commit, the canonicalization field will be
|
|
243
|
+
# ACTENON-JCS-STRICT-1. Until then, this test verifies the
|
|
244
|
+
# canonicalisation logic is correct.
|
|
245
|
+
intent = _build_security_intent()
|
|
246
|
+
expected_hash = build_action_hash_input(intent)
|
|
247
|
+
result = canonicalize_bytes(expected_hash)
|
|
248
|
+
self.assertIsInstance(result, bytes)
|
|
249
|
+
|
|
250
|
+
def test_17_unsupported_profile_rejection(self) -> None:
|
|
251
|
+
"""Proofs with an unsupported canonicalization identifier must
|
|
252
|
+
be rejected by the verifier.
|
|
253
|
+
"""
|
|
254
|
+
from actenon.core.errors import ProofVerificationError
|
|
255
|
+
from actenon.proof import PCCBVerifier, VerifierDisclosureMode
|
|
256
|
+
from dataclasses import replace
|
|
257
|
+
|
|
258
|
+
pccb = _mint_security_pccb()
|
|
259
|
+
# Replace canonicalization with an unsupported value
|
|
260
|
+
forged = replace(
|
|
261
|
+
pccb,
|
|
262
|
+
action_hash=replace(pccb.action_hash, canonicalization="JSON-LD"),
|
|
263
|
+
)
|
|
264
|
+
verifier = PCCBVerifier(
|
|
265
|
+
signer=_security_signer(),
|
|
266
|
+
disclosure_mode=VerifierDisclosureMode.LOCAL_DEBUG,
|
|
267
|
+
)
|
|
268
|
+
intent = _build_security_intent()
|
|
269
|
+
context = _build_security_context()
|
|
270
|
+
with self.assertRaises(ProofVerificationError):
|
|
271
|
+
verifier.verify(intent, forged, context)
|
|
272
|
+
|
|
273
|
+
def test_18_cross_language_portability(self) -> None:
|
|
274
|
+
case = self._case("cross_language_portability")
|
|
275
|
+
result = canonicalize_json(case["input"])
|
|
276
|
+
self.assertEqual(case["expected_output"], result)
|
|
277
|
+
|
|
278
|
+
# ── Helpers ───────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
def _case(self, case_id: str) -> dict:
|
|
281
|
+
for case in self.cases["cases"]:
|
|
282
|
+
if case["id"] == case_id:
|
|
283
|
+
return case
|
|
284
|
+
raise KeyError(f"case not found: {case_id}")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
unittest.main()
|