"""Intégration Secure Boot pour les transactions qui modifient le démarrage.""" import logging import os import subprocess from dataclasses import dataclass from pathlib import Path from typing import Iterable logger = logging.getLogger(__name__) SIGNER_PATH = Path("/usr/local/sbin/secureboot-sign-cachyos") @dataclass(frozen=True) class SecureBootResult: success: bool configured: bool error: str | None = None def boot_artifacts_may_change(package_names: Iterable[str]) -> bool: """Indique si la transaction peut remplacer un noyau ou un chargeur EFI.""" for name in package_names: if name in {"grub", "shim-signed", "systemd"}: return True if name == "linux" or name.startswith("linux-"): return True return False def _run(command: list[str], timeout: int = 120) -> subprocess.CompletedProcess: return subprocess.run( command, capture_output=True, text=True, timeout=timeout, ) def _error_message(result: subprocess.CompletedProcess) -> str: output = (result.stderr or result.stdout).strip() return output[-800:] if output else f"code de sortie {result.returncode}" def prepare_secure_boot(package_names: Iterable[str]) -> SecureBootResult: """Garantit que l'ESP est montée avant une transaction sensible. L'absence du signer signifie simplement que Secure Boot n'est pas géré par cette installation. En revanche, une configuration présente mais inutilisable bloque la transaction pour ne pas installer un noyau non signé. """ names = list(package_names) if not boot_artifacts_may_change(names) or not SIGNER_PATH.is_file(): return SecureBootResult(success=True, configured=False) if os.geteuid() != 0: return SecureBootResult( success=False, configured=True, error="la préparation Secure Boot doit être exécutée en root", ) if not os.access(SIGNER_PATH, os.X_OK): return SecureBootResult( success=False, configured=True, error=f"le signer Secure Boot n'est pas exécutable : {SIGNER_PATH}", ) try: result = _run([str(SIGNER_PATH), "--prepare"]) except (OSError, subprocess.TimeoutExpired) as error: return SecureBootResult( success=False, configured=True, error=f"impossible d'exécuter la pré-vérification : {error}", ) if result.returncode != 0: return SecureBootResult( success=False, configured=True, error=f"pré-vérification du signer échouée : {_error_message(result)}", ) return SecureBootResult(success=True, configured=True) def refresh_secure_boot(package_names: Iterable[str]) -> SecureBootResult: """Reconstruit et signe la chaîne CachyOS après la transaction pacman.""" names = list(package_names) if not boot_artifacts_may_change(names) or not SIGNER_PATH.is_file(): return SecureBootResult(success=True, configured=False) try: result = _run([str(SIGNER_PATH)], timeout=180) except (OSError, subprocess.TimeoutExpired) as error: return SecureBootResult( success=False, configured=True, error=f"impossible d'exécuter le signer : {error}", ) if result.returncode != 0: return SecureBootResult( success=False, configured=True, error=f"signature Secure Boot échouée : {_error_message(result)}", ) logger.info("Chaîne de démarrage Secure Boot reconstruite et vérifiée") return SecureBootResult(success=True, configured=True)