114 lines
3.6 KiB
Python
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)
|