"""Read-only validation of recovered campaign authorization evidence.""" from __future__ import annotations import hashlib import json from pathlib import Path import stat from typing import Any class RecoveryEvidenceError(RuntimeError): pass def _digest(path: Path) -> str: h = hashlib.sha256() with path.open("rb") as stream: for block in iter(lambda: stream.read(1024 * 1024), b""): h.update(block) return h.hexdigest() def _secure_json(path: Path) -> dict[str, Any]: if path.is_symlink() or not path.is_file(): raise RecoveryEvidenceError("RECOVERY_EVIDENCE_MISSING") info = path.stat(follow_symlinks=False) if info.st_uid != 0 or stat.S_IMODE(info.st_mode) & 0o022: raise RecoveryEvidenceError("RECOVERY_EVIDENCE_PERMISSIONS") try: value = json.loads(path.read_text(encoding="utf-8")) except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: raise RecoveryEvidenceError("RECOVERY_EVIDENCE_INVALID") from exc if not isinstance(value, dict): raise RecoveryEvidenceError("RECOVERY_EVIDENCE_OBJECT_REQUIRED") return value def load_recovered_authorization(path: str | Path) -> dict[str, Any]: """Verify the recovery receipt, sidecar, and all referenced immutable files.""" receipt_path = Path(path) receipt = _secure_json(receipt_path) sidecar = receipt_path.with_suffix(receipt_path.suffix + ".sha256") if sidecar.is_symlink() or not sidecar.is_file(): raise RecoveryEvidenceError("RECOVERY_DIGEST_SIDECAR_MISSING") expected = sidecar.read_text(encoding="ascii").split()[0] actual = _digest(receipt_path) if expected != actual: raise RecoveryEvidenceError("RECOVERY_DIGEST_MISMATCH") required = { "schema", "campaign_id", "original_event_id", "original_authorization_digest", "signed_grant_id", "signed_grant_digest", "task_id", "repository", "original_base_oid", "branch", "expected_branch_oid", "recovered_from_existing_evidence", "new_owner_interaction", "recovery_state", "production_side_effect_executed", "global_writes_enabled", "grant_consumed", "owner_authorization_consumed", "original_receipts_immutable", "recovered_at", "evidence_files", } if set(receipt) != required or receipt["schema"] != "aios.owner-authorization-recovery.v1": raise RecoveryEvidenceError("RECOVERY_SCHEMA_INVALID") if receipt["recovered_from_existing_evidence"] is not True or receipt["new_owner_interaction"] is not False: raise RecoveryEvidenceError("RECOVERY_NOT_APPEND_ONLY") if receipt["production_side_effect_executed"] is not False or receipt["global_writes_enabled"] is not False: raise RecoveryEvidenceError("RECOVERY_SCOPE_INVALID") if receipt["recovery_state"] != "RECOVERED_PENDING_CONTINUATION_LEASE": raise RecoveryEvidenceError("RECOVERY_STATE_INVALID") evidence = receipt["evidence_files"] if not isinstance(evidence, list) or not evidence: raise RecoveryEvidenceError("RECOVERY_EVIDENCE_LIST_INVALID") for item in evidence: if not isinstance(item, dict) or set(item) != {"path", "sha256"}: raise RecoveryEvidenceError("RECOVERY_EVIDENCE_ENTRY_INVALID") target = Path(item["path"]) if _digest(target) != item["sha256"]: raise RecoveryEvidenceError("RECOVERY_EVIDENCE_DIGEST_MISMATCH") return { "schema": receipt["schema"], "state": receipt["recovery_state"], "campaign_id": receipt["campaign_id"], "original_event_id": receipt["original_event_id"], "original_authorization_digest": receipt["original_authorization_digest"], "signed_grant_id": receipt["signed_grant_id"], "task_id": receipt["task_id"], "repository": receipt["repository"], "original_base_oid": receipt["original_base_oid"], "branch": receipt["branch"], "expected_branch_oid": receipt["expected_branch_oid"], "digest": actual, "new_owner_interaction": False, }