"""Opaque, workspace-scoped handles for generated catalog candidates.""" from __future__ import annotations from dataclasses import dataclass import hashlib import hmac import json import os from pathlib import Path import re import stat from typing import Iterable from .catalog import canonical_hash MAX_CATALOG_BYTES = 16 * 1024 * 1024 _CANDIDATE_ID = re.compile(r"^candidate-[0-9a-f]{32}$") _CHANNEL = re.compile(r"^[a-z][a-z0-9_-]{0,63}$") class CandidateArtifactError(ValueError): """A candidate handle or the artifact behind it is unsafe or inconsistent.""" @dataclass(frozen=True, slots=True) class CandidateArtifactReceipt: candidate_id: str catalog_sha256: str def issue_candidate_id(*seed_parts: str) -> str: """Derive an opaque, deterministic candidate basename before output exists.""" if not seed_parts or any( not isinstance(part, str) or not part or len(part) > 512 for part in seed_parts ): raise CandidateArtifactError("candidate ID seeds must be non-empty bounded strings") encoded = json.dumps( list(seed_parts), sort_keys=False, separators=(",", ":"), ensure_ascii=True ).encode("utf-8") return f"candidate-{hashlib.sha256(encoded).hexdigest()[:32]}" def validate_release_channel(value: str) -> str: """Return one bounded channel basename or reject path-capable input.""" if not isinstance(value, str) or _CHANNEL.fullmatch(value) is None: raise CandidateArtifactError( "release channel must be a bounded lowercase identifier" ) return value def candidate_output_path(root: Path | str, candidate_id: str) -> Path: """Resolve a not-yet-created candidate path below a trusted configured root.""" checked_id = _checked_candidate_id(candidate_id) root_path = Path(root).expanduser() ensure_private_candidate_root(root_path, create=True) candidate = root_path / checked_id try: mode = candidate.lstat().st_mode except FileNotFoundError: return candidate except OSError as exc: raise CandidateArtifactError("candidate output path cannot be inspected") from exc if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode): raise CandidateArtifactError("candidate output path must be a real directory") _require_private_owner_mode(candidate, directory=True, label="candidate output path") return candidate def ensure_private_candidate_root( root: Path | str, *, create: bool = False ) -> Path: """Admit only an operator-owned candidate root inaccessible to other users.""" root_path = Path(root).expanduser() _reject_symlink_components(root_path, allow_missing=create) if create: try: root_path.mkdir(mode=0o700, parents=True, exist_ok=True) except OSError as exc: raise CandidateArtifactError("candidate root cannot be created") from exc _reject_symlink_components(root_path, allow_missing=False) _require_private_owner_mode(root_path, directory=True, label="candidate root") return root_path def harden_private_candidate_tree(path: Path | str) -> Path: """Seal a newly generated, operator-owned candidate tree before a receipt.""" root = Path(path).expanduser() _reject_symlink_components(root, allow_missing=False) _require_current_owner(root, label="candidate directory") try: os.chmod(root, 0o700, follow_symlinks=False) for current, directory_names, file_names in os.walk(root, followlinks=False): current_path = Path(current) _require_current_owner(current_path, label="candidate directory") os.chmod(current_path, 0o700, follow_symlinks=False) for name in (*directory_names, *file_names): child = current_path / name mode = child.lstat().st_mode if stat.S_ISLNK(mode): raise CandidateArtifactError("candidate tree contains a symlink") _require_current_owner(child, label="candidate tree member") if stat.S_ISDIR(mode): os.chmod(child, 0o700, follow_symlinks=False) elif stat.S_ISREG(mode): os.chmod(child, 0o600, follow_symlinks=False) else: raise CandidateArtifactError( "candidate tree contains a non-file member" ) except OSError as exc: raise CandidateArtifactError("candidate tree cannot be sealed") from exc return root def issue_candidate_receipt( *, root: Path | str, candidate_id: str, channel: str ) -> CandidateArtifactReceipt: """Hash the signed catalog at a generated candidate handle.""" candidate = _existing_candidate_path(root, candidate_id) payload = _read_signed_catalog(candidate, channel=channel) return CandidateArtifactReceipt( candidate_id=candidate_id, catalog_sha256=canonical_hash(payload), ) def verify_candidate_receipt( *, root: Path | str, candidate_id: str, catalog_sha256: str, channel: str, ) -> Path: """Re-resolve and re-hash a persisted receipt before candidate consumption.""" if re.fullmatch(r"[0-9a-f]{64}", catalog_sha256) is None: raise CandidateArtifactError("candidate catalog digest is malformed") candidate = _existing_candidate_path(root, candidate_id) payload = _read_signed_catalog(candidate, channel=channel) if not hmac.compare_digest(canonical_hash(payload), catalog_sha256): raise CandidateArtifactError("candidate catalog no longer matches its receipt") return candidate def _checked_candidate_id(candidate_id: str) -> str: if not isinstance(candidate_id, str) or _CANDIDATE_ID.fullmatch(candidate_id) is None: raise CandidateArtifactError("candidate ID is not an issued opaque basename") return candidate_id def _existing_candidate_path(root: Path | str, candidate_id: str) -> Path: candidate = candidate_output_path(root, candidate_id) try: mode = candidate.lstat().st_mode except OSError as exc: raise CandidateArtifactError("candidate directory is unavailable") from exc if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode): raise CandidateArtifactError("candidate directory must be a real directory") _reject_symlink_components(candidate, allow_missing=False) return candidate def _read_signed_catalog(candidate: Path, *, channel: str) -> dict[str, object]: channel = validate_release_channel(channel) channels = candidate / "channels" catalog_path = channels / f"{channel}.json" _require_real_directory(channels, label="candidate channels directory") _require_regular_file(catalog_path, label="candidate catalog") try: with catalog_path.open("rb") as handle: encoded = handle.read(MAX_CATALOG_BYTES + 1) except OSError as exc: raise CandidateArtifactError("candidate catalog cannot be read") from exc if len(encoded) > MAX_CATALOG_BYTES: raise CandidateArtifactError("candidate catalog exceeds its size limit") try: payload = json.loads(encoded.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise CandidateArtifactError("candidate catalog is not valid JSON") from exc if not isinstance(payload, dict): raise CandidateArtifactError("candidate catalog must be a JSON object") if payload.get("channel") != channel: raise CandidateArtifactError( "candidate catalog channel does not match its requested handle" ) signatures = payload.get("signatures") if not isinstance(signatures, list) or not signatures: raise CandidateArtifactError("candidate catalog has no signature envelope") return payload def _reject_symlink_components(path: Path, *, allow_missing: bool) -> None: absolute = path.absolute() parts: Iterable[Path] = reversed((absolute, *absolute.parents)) for component in parts: try: mode = component.lstat().st_mode except FileNotFoundError: if allow_missing: continue raise CandidateArtifactError("candidate path has a missing component") except OSError as exc: raise CandidateArtifactError("candidate path cannot be inspected") from exc if stat.S_ISLNK(mode): raise CandidateArtifactError("candidate path must not traverse symlinks") def _require_real_directory(path: Path, *, label: str) -> None: try: mode = path.lstat().st_mode except OSError as exc: raise CandidateArtifactError(f"{label} is unavailable") from exc if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode): raise CandidateArtifactError(f"{label} must be a real directory") _require_private_owner_mode(path, directory=True, label=label) def _require_regular_file(path: Path, *, label: str) -> None: try: mode = path.lstat().st_mode except OSError as exc: raise CandidateArtifactError(f"{label} is unavailable") from exc if stat.S_ISLNK(mode) or not stat.S_ISREG(mode): raise CandidateArtifactError(f"{label} must be a regular file") _require_private_owner_mode(path, directory=False, label=label) def _require_current_owner(path: Path, *, label: str) -> os.stat_result: try: observed = path.lstat() except OSError as exc: raise CandidateArtifactError(f"{label} cannot be inspected") from exc if observed.st_uid != os.geteuid(): raise CandidateArtifactError(f"{label} is not owned by the current operator") return observed def _require_private_owner_mode( path: Path, *, directory: bool, label: str ) -> None: observed = _require_current_owner(path, label=label) expected_type = stat.S_ISDIR if directory else stat.S_ISREG if not expected_type(observed.st_mode) or stat.S_IMODE(observed.st_mode) & 0o077: raise CandidateArtifactError(f"{label} is accessible to another user")