"""Canonical CASAN wire-contract builders and lightweight validation.""" from __future__ import annotations import hashlib import json from datetime import datetime, timezone from typing import Any, Mapping CONTRACT_VERSION = "1.0.0" def utc_now() -> str: return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") def actor(actor_id: str, actor_type: str, issuer: str, trust_level: str, auth_evidence_ref: str | None = None) -> dict[str, Any]: return { "actor_id": actor_id, "actor_type": actor_type, "issuer": issuer, "trust_level": trust_level, "authentication_evidence_ref": auth_evidence_ref, } def outcome(execution: str, assurance: str, certification: str, business: str = "not_evaluated") -> dict[str, str]: return { "execution_result": execution, "assurance_result": assurance, "certification_result": certification, "business_result": business, } def evidence_digest(evidence: Mapping[str, Any]) -> str: """Digest the claim-bearing evidence metadata, excluding its digest field.""" material = { key: evidence.get(key) for key in ( "evidence_id", "claim_refs", "producer_identity", "timestamp", "artifact_ref", "validation_status", "retention_class", "category", ) } payload = json.dumps(material, sort_keys=True, separators=(",", ":"), ensure_ascii=True) return hashlib.sha256(payload.encode("utf-8")).hexdigest() def verify_evidence_integrity(evidence: Mapping[str, Any]) -> bool: integrity = evidence.get("integrity") if not isinstance(integrity, Mapping) or integrity.get("algorithm") != "sha256": return False digest = integrity.get("digest") return isinstance(digest, str) and digest == evidence_digest(evidence) def validate_bundle(bundle: Mapping[str, Any]) -> list[str]: """Return deterministic contract errors; an empty list means valid. JSON Schema is canonical. This small validator keeps runtime adapters stdlib- only and catches the invariants needed at enforcement boundaries. """ errors: list[str] = [] if bundle.get("schema_version") != CONTRACT_VERSION: errors.append("unsupported_schema_version") run = bundle.get("run") if not isinstance(run, Mapping): errors.append("missing_execution_run") return errors for key in ("run_id", "runtime", "mode", "status", "correlation_id", "outcome"): if not run.get(key): errors.append(f"run.{key}_required") actors = bundle.get("actors") actor_ids: set[object] = set() if not isinstance(actors, list) or not actors: errors.append("actors_required") else: actor_ids = {item.get("actor_id") for item in actors if isinstance(item, Mapping)} if "" in actor_ids or None in actor_ids: errors.append("actor_identity_required") steps = bundle.get("steps") if not isinstance(steps, list): errors.append("steps_required") else: for index, step in enumerate(steps): if not isinstance(step, Mapping): errors.append(f"steps.{index}_invalid") continue for key in ("step_id", "run_id", "sequence", "actor_ref", "action", "outcome"): if step.get(key) is None: errors.append(f"steps.{index}.{key}_required") if step.get("actor_ref") not in actor_ids: errors.append(f"steps.{index}.actor_ref_unknown") evidence = bundle.get("evidence") evidence_ids: set[object] = set() if not isinstance(evidence, list): errors.append("evidence_required") else: for index, item in enumerate(evidence): if not isinstance(item, Mapping): errors.append(f"evidence.{index}_invalid") continue evidence_ids.add(item.get("evidence_id")) if not verify_evidence_integrity(item): errors.append(f"evidence.{index}.integrity_invalid") claims = bundle.get("claims") claim_ids = { item.get("claim_id") for item in claims or [] if isinstance(item, Mapping) } if isinstance(claims, list) else set() if isinstance(claims, list): for index, claim in enumerate(claims): if not isinstance(claim, Mapping): errors.append(f"claims.{index}_invalid") continue for evidence_ref in claim.get("evidence_refs") or []: if evidence_ref not in evidence_ids: errors.append(f"claims.{index}.evidence_ref_unknown") if isinstance(evidence, list): for index, item in enumerate(evidence): if isinstance(item, Mapping): for claim_ref in item.get("claim_refs") or []: if claim_ref not in claim_ids: errors.append(f"evidence.{index}.claim_ref_unknown") delegations = bundle.get("delegations") if delegations is not None: errors.extend(_validate_delegations(delegations, actor_ids)) if run.get("mode") == "observe" and isinstance(run.get("outcome"), Mapping): if run["outcome"].get("certification_result") == "certified": errors.append("observed_only_cannot_be_certified") return errors def _validate_delegations(delegations: object, actor_ids: set[object]) -> list[str]: if not isinstance(delegations, list): return ["delegations_invalid"] errors: list[str] = [] records = { item.get("delegation_id"): item for item in delegations if isinstance(item, Mapping) and item.get("delegation_id") } for index, item in enumerate(delegations): if not isinstance(item, Mapping): errors.append(f"delegations.{index}_invalid") continue for actor_key in ("delegator_ref", "delegate_ref"): if item.get(actor_key) not in actor_ids: errors.append(f"delegations.{index}.{actor_key}_unknown") current: Mapping[str, Any] = item visited: set[object] = set() depth = 0 while current.get("parent_delegation_ref"): parent_ref = current.get("parent_delegation_ref") if parent_ref in visited: errors.append(f"delegations.{index}.cycle") break visited.add(parent_ref) parent = records.get(parent_ref) if not isinstance(parent, Mapping): errors.append(f"delegations.{index}.parent_unknown") break depth += 1 current = parent maximum_depth = item.get("maximum_depth") if isinstance(maximum_depth, int) and depth > maximum_depth: errors.append(f"delegations.{index}.maximum_depth_exceeded") return errors