Files
CachyOS-updater/lib/secure_boot.py

114 lines
3.6 KiB
Python

"""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)