102 lines
7.9 KiB
Python
102 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Canonical, fail-closed projection for trusted release execution plans.
|
|
|
|
A release plan is the only authority for candidate, source, artifact, policy,
|
|
rollback, and release identity. Callers may not provide executable paths or
|
|
replace individual fields after the plan has been accepted.
|
|
"""
|
|
from __future__ import annotations
|
|
import hashlib, json, re, stat
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Mapping
|
|
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
|
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
PLAN_ID_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{2,255}$")
|
|
TOP_LEVEL_KEYS = frozenset({"schema","version","candidate_id","source_commit","tree_oid","artifact_sha256","manifest_sha256","package_closure_sha256","deployment_plan_sha256","policy_sha256","release_plan_id","approval_plan_id","evidence_closure_oid","campaign_authorization_sha256","campaign_generation","artifact","rollback","baseline","adapter_policy","release_plan_sha256"})
|
|
FORBIDDEN_KEYS = frozenset({"command","commands","executable","exec","shell","argv","adapter_command","tag_command","deploy_command","registry_command","baseline_command","sudo","force_push","direct_main_push"})
|
|
class PlanError(ValueError):
|
|
"""A release plan is malformed, inconsistent, or unsafe."""
|
|
def canonical_json(value: object) -> bytes:
|
|
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
|
def _sha(value: Any, label: str) -> str:
|
|
if not isinstance(value, str) or not SHA_RE.fullmatch(value): raise PlanError(f"{label}_invalid")
|
|
return value
|
|
def _oid(value: Any, label: str) -> str:
|
|
if not isinstance(value, str) or not OID_RE.fullmatch(value): raise PlanError(f"{label}_invalid")
|
|
return value
|
|
def _walk_forbidden(value: Any) -> None:
|
|
if isinstance(value, Mapping):
|
|
for key, item in value.items():
|
|
if str(key).lower() in FORBIDDEN_KEYS: raise PlanError(f"forbidden_plan_key:{key}")
|
|
_walk_forbidden(item)
|
|
elif isinstance(value, list):
|
|
for item in value: _walk_forbidden(item)
|
|
def _exact_mapping(value: Any, required: set[str], label: str) -> Mapping[str, Any]:
|
|
if not isinstance(value, Mapping) or set(value) != required: raise PlanError(f"{label}_fields_invalid")
|
|
return value
|
|
def _secure_json(path: Path) -> dict[str, Any]:
|
|
try: st = path.lstat()
|
|
except OSError as exc: raise PlanError(f"plan_unreadable:{path}") from exc
|
|
if path.is_symlink() or not stat.S_ISREG(st.st_mode): raise PlanError("plan_must_be_regular_file")
|
|
if st.st_uid != 0 or st.st_mode & 0o022: raise PlanError("plan_ownership_or_mode_invalid")
|
|
try: value = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError) as exc: raise PlanError("plan_json_invalid") from exc
|
|
if not isinstance(value, dict): raise PlanError("plan_object_required")
|
|
return value
|
|
@dataclass(frozen=True)
|
|
class TrustedReleasePlan:
|
|
value: Mapping[str, Any]
|
|
release_plan_sha256: str
|
|
@classmethod
|
|
def from_mapping(cls, raw: Mapping[str, Any]) -> "TrustedReleasePlan":
|
|
if not isinstance(raw, Mapping) or set(raw) - TOP_LEVEL_KEYS: raise PlanError("release_plan_fields_invalid")
|
|
required = TOP_LEVEL_KEYS - {"release_plan_sha256"}
|
|
if set(raw) not in (required, TOP_LEVEL_KEYS): raise PlanError("release_plan_fields_incomplete")
|
|
_walk_forbidden(raw)
|
|
if raw.get("schema") != "aios.trusted-release-execution-plan.v1": raise PlanError("release_plan_schema_invalid")
|
|
version = raw.get("version")
|
|
if not isinstance(version, str) or not re.fullmatch(r"V[0-9]+R[0-9]+C[0-9]+", version): raise PlanError("version_invalid")
|
|
if not isinstance(raw.get("candidate_id"), str) or not IDENTIFIER_RE.fullmatch(raw["candidate_id"]): raise PlanError("candidate_id_invalid")
|
|
for key in ("source_commit","tree_oid","evidence_closure_oid"): _oid(raw.get(key), key)
|
|
for key in ("artifact_sha256","manifest_sha256","package_closure_sha256","deployment_plan_sha256","policy_sha256","campaign_authorization_sha256"): _sha(raw.get(key), key)
|
|
for key in ("release_plan_id","approval_plan_id"):
|
|
if not isinstance(raw.get(key), str) or not PLAN_ID_RE.fullmatch(raw[key]): raise PlanError(f"{key}_invalid")
|
|
if not isinstance(raw.get("campaign_generation"), int) or raw["campaign_generation"] < 1: raise PlanError("campaign_generation_invalid")
|
|
artifact = _exact_mapping(raw.get("artifact"), {"path","size"}, "artifact")
|
|
if not isinstance(artifact["path"], str) or not artifact["path"].startswith("/") or "\\" in artifact["path"]: raise PlanError("artifact_path_invalid")
|
|
if not isinstance(artifact["size"], int) or artifact["size"] <= 0: raise PlanError("artifact_size_invalid")
|
|
rollback = _exact_mapping(raw.get("rollback"), {"version","target_oid","artifact_sha256"}, "rollback")
|
|
if not isinstance(rollback["version"], str) or not rollback["version"] or rollback["version"] == version: raise PlanError("rollback_version_invalid")
|
|
_oid(rollback["target_oid"], "rollback_target_oid"); _sha(rollback["artifact_sha256"], "rollback_artifact_sha256")
|
|
baseline = _exact_mapping(raw.get("baseline"), {"version","target_oid"}, "baseline")
|
|
if baseline["version"] != rollback["version"]: raise PlanError("baseline_rollback_version_mismatch")
|
|
_oid(baseline["target_oid"], "baseline_target_oid")
|
|
adapter = _exact_mapping(raw.get("adapter_policy"), {"policy_id","service","port"}, "adapter_policy")
|
|
if adapter["policy_id"] != "aios.production-adapter.allowlisted.v1": raise PlanError("adapter_policy_invalid")
|
|
if not isinstance(adapter["service"], str) or not IDENTIFIER_RE.fullmatch(adapter["service"]): raise PlanError("adapter_service_invalid")
|
|
if not isinstance(adapter["port"], int) or not 1024 <= adapter["port"] <= 65535: raise PlanError("adapter_port_invalid")
|
|
payload = {key: raw[key] for key in TOP_LEVEL_KEYS if key != "release_plan_sha256"}
|
|
digest = hashlib.sha256(canonical_json(payload)).hexdigest()
|
|
if raw.get("release_plan_sha256") is not None and raw["release_plan_sha256"] != digest: raise PlanError("release_plan_sha256_mismatch")
|
|
return cls(value=dict(payload), release_plan_sha256=digest)
|
|
@classmethod
|
|
def load(cls, path: Path) -> "TrustedReleasePlan": return cls.from_mapping(_secure_json(path))
|
|
@property
|
|
def candidate_binding_sha256(self) -> str:
|
|
value = {key: self.value[key] for key in ("candidate_id","source_commit","tree_oid","artifact_sha256","manifest_sha256")}
|
|
return hashlib.sha256(canonical_json(value)).hexdigest()
|
|
def as_dict(self) -> dict[str, Any]: return {**dict(self.value),"release_plan_sha256":self.release_plan_sha256,"candidate_binding_sha256":self.candidate_binding_sha256}
|
|
def assert_binding(self, binding: Mapping[str, Any]) -> None:
|
|
expected={"version":self.value["version"],"target_oid":self.value["source_commit"],"evidence_closure_oid":self.value["evidence_closure_oid"],"artifact_sha256":self.value["artifact_sha256"],"artifact_size":self.value["artifact"]["size"],"approval_plan_id":self.value["approval_plan_id"]}
|
|
for key,value in expected.items():
|
|
if binding.get(key) != value: raise PlanError(f"binding_plan_mismatch:{key}")
|
|
if binding.get("candidate_id") is not None and binding.get("candidate_id") != self.value["candidate_id"]:
|
|
raise PlanError("binding_plan_mismatch:candidate_id")
|
|
def assert_artifact(self, artifact: Mapping[str, Any]) -> None:
|
|
expected=self.value["artifact"]
|
|
if artifact.get("sha256") != self.value["artifact_sha256"] or artifact.get("size") != expected["size"]: raise PlanError("artifact_plan_mismatch")
|
|
if artifact.get("manifest_sha256") != self.value["manifest_sha256"]: raise PlanError("manifest_plan_mismatch")
|
|
if artifact.get("package_closure_sha256") != self.value["package_closure_sha256"]: raise PlanError("package_closure_plan_mismatch")
|