105 lines
4.1 KiB
Python
105 lines
4.1 KiB
Python
"""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,
|
|
}
|