From b68a3ff10fb95c835c541897fc807e312e7351bc Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Mon, 11 Mar 2024 14:57:58 +0100 Subject: [PATCH] Rework QemuFirmware= - Use the qemu official firmware descriptions to look up OVMF firmware instead of having our own homegrown logic. - Add QemuFirmware=uefi-secure-boot to explicitly look for firmware with secure boot support - Add QemuFirmwareVariables=microsoft to use OVMF variables with Microsoft keys enrolled - Add QemuFirmwareVariables=custom to enroll the certificate from SecureBootCertificate= into the OVMF variables This commit also contains the changes from a second commit that was accidentally rebased into this one: Only use already signed binaries when ShimBootloader=signed When we're using signed shim, we need to make sure we use already signed bootloaders, kernel images and UKIs. Anything we sign ourselves will cause security violations in shim. --- mkosi.conf | 2 +- mkosi.conf.d/20-arch.conf | 1 - mkosi.conf.d/20-opensuse.conf | 3 + .../mkosi.conf.d/20-uefi.conf | 9 +- .../mkosi.conf.d/20-x86-64.conf | 3 +- mkosi/__init__.py | 155 ++++++++--- mkosi/config.py | 16 +- mkosi/qemu.py | 251 +++++++++--------- .../mkosi-tools/mkosi.conf.d/10-arch.conf | 1 + .../mkosi.conf.d/10-centos-fedora/mkosi.conf | 1 + mkosi/resources/mkosi.md | 33 ++- mkosi/vmspawn.py | 18 +- tests/test_boot.py | 2 +- 13 files changed, 306 insertions(+), 189 deletions(-) diff --git a/mkosi.conf b/mkosi.conf index 8b6635918..9d39f7a34 100644 --- a/mkosi.conf +++ b/mkosi.conf @@ -9,8 +9,8 @@ [Content] Autologin=yes -@ShimBootloader=signed @SELinuxRelabel=no +@ShimBootloader=unsigned BuildSources=. BuildSourcesEphemeral=yes diff --git a/mkosi.conf.d/20-arch.conf b/mkosi.conf.d/20-arch.conf index 9b74f61bf..660dae526 100644 --- a/mkosi.conf.d/20-arch.conf +++ b/mkosi.conf.d/20-arch.conf @@ -4,7 +4,6 @@ Distribution=arch [Content] -ShimBootloader=unsigned Packages= apt archlinux-keyring diff --git a/mkosi.conf.d/20-opensuse.conf b/mkosi.conf.d/20-opensuse.conf index cfc37d9b1..15d8f741e 100644 --- a/mkosi.conf.d/20-opensuse.conf +++ b/mkosi.conf.d/20-opensuse.conf @@ -7,6 +7,8 @@ Distribution=opensuse @Release=tumbleweed [Content] +# OpenSUSE does not ship an unsigned shim +@ShimBootloader=none Packages= bash btrfs-progs @@ -21,6 +23,7 @@ Packages= e2fsprogs erofs-utils grep + grub2-efi grub2-i386-pc grub2-x86_64-efi iproute diff --git a/mkosi.conf.d/30-centos-fedora/mkosi.conf.d/20-uefi.conf b/mkosi.conf.d/30-centos-fedora/mkosi.conf.d/20-uefi.conf index 400758906..895a4b18a 100644 --- a/mkosi.conf.d/30-centos-fedora/mkosi.conf.d/20-uefi.conf +++ b/mkosi.conf.d/30-centos-fedora/mkosi.conf.d/20-uefi.conf @@ -6,8 +6,11 @@ Architecture=|arm64 [Content] Packages= - systemd-boot - pesign edk2-ovmf - shim + grub2-efi grub2-efi-x64-modules + kernel-uki-virt + pesign + shim + shim-unsigned-x64 + systemd-boot diff --git a/mkosi.conf.d/30-debian-ubuntu/mkosi.conf.d/20-x86-64.conf b/mkosi.conf.d/30-debian-ubuntu/mkosi.conf.d/20-x86-64.conf index 48b054758..7c3bcfe4a 100644 --- a/mkosi.conf.d/30-debian-ubuntu/mkosi.conf.d/20-x86-64.conf +++ b/mkosi.conf.d/30-debian-ubuntu/mkosi.conf.d/20-x86-64.conf @@ -6,6 +6,7 @@ Architecture=x86-64 [Content] Packages= amd64-microcode - grub-pc-bin + grub-efi grub-efi-amd64 + grub-pc-bin intel-microcode diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 0dd22f300..e3c6befb5 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -914,13 +914,14 @@ def install_systemd_boot(context: Context) -> None: return directory = context.root / "usr/lib/systemd/boot/efi" - if not directory.exists() or not any(directory.iterdir()): + signed = context.config.shim_bootloader == ShimBootloader.signed + if not directory.glob("*.efi.signed" if signed else "*.efi"): if context.config.bootable == ConfigFeature.enabled: - die("A EFI bootable image with systemd-boot was requested but systemd-boot was not found at " - f"{directory.relative_to(context.root)}") + die(f"An EFI bootable image with systemd-boot was requested but a {'signed ' if signed else ''}" + f"systemd-boot binary was not found at {directory.relative_to(context.root)}") return - if context.config.secure_boot: + if context.config.secure_boot and not signed: with complete_step("Signing systemd-boot binaries…"): for input in itertools.chain(directory.glob('*.efi'), directory.glob('*.EFI')): output = directory / f"{input}.signed" @@ -1129,9 +1130,10 @@ def want_grub_efi(context: Context) -> bool: if context.config.bootloader != Bootloader.grub: return False - have = find_grub_directory(context, target="x86_64-efi") is not None - if not have and context.config.bootable == ConfigFeature.enabled: - die("An EFI bootable image with grub was requested but grub for EFI is not installed") + if context.config.shim_bootloader != ShimBootloader.signed: + have = find_grub_directory(context, target="x86_64-efi") is not None + if not have and context.config.bootable == ConfigFeature.enabled: + die("An EFI bootable image with grub was requested but grub for EFI is not installed") return True @@ -1191,9 +1193,13 @@ def prepare_grub_config(context: Context) -> Optional[Path]: f.write("set timeout=0\n") if want_grub_efi(context): - # Signed EFI grub shipped by distributions reads its configuration from /EFI//grub.cfg in - # the ESP so let's put a shim there to redirect to the actual configuration file. - earlyconfig = context.root / "efi/EFI" / context.config.distribution.name / "grub.cfg" + # Signed EFI grub shipped by distributions reads its configuration from /EFI//grub.cfg (except + # in OpenSUSE) in the ESP so let's put a shim there to redirect to the actual configuration file. + if context.config.distribution == Distribution.opensuse: + earlyconfig = context.root / "efi/EFI/BOOT/grub.cfg" + else: + earlyconfig = context.root / "efi/EFI" / context.config.distribution.name / "grub.cfg" + with umask(~0o700): earlyconfig.parent.mkdir(parents=True, exist_ok=True) @@ -1203,7 +1209,14 @@ def prepare_grub_config(context: Context) -> Optional[Path]: return config -def grub_mkimage(context: Context, *, target: str, modules: Sequence[str] = (), output: Optional[Path] = None) -> None: +def grub_mkimage( + context: Context, + *, + target: str, + modules: Sequence[str] = (), + output: Optional[Path] = None, + sbat: Optional[Path] = None, +) -> None: mkimage = find_grub_binary("mkimage", root=context.config.tools()) assert mkimage @@ -1233,23 +1246,60 @@ def grub_mkimage(context: Context, *, target: str, modules: Sequence[str] = (), "--prefix", f"/{context.config.distribution.grub_prefix()}", "--output", output or (directory / "core.img"), "--format", target, + *(["--sbat", str(sbat)] if sbat else []), + *(["--disable-shim-lock"] if context.config.shim_bootloader == ShimBootloader.none else []), + "cat", + "cmp", + "div", + "echo", "fat", + "hello", + "help", + "keylayouts", + "linux", + "loadenv", + "ls", + "normal", "part_gpt", - "search", + "read", + "reboot", "search_fs_file", - "normal", - "linux", + "search", + "sleep", + "test", + "tr", + "true", *modules, ], sandbox=context.sandbox( options=[ "--bind", context.root, context.root, "--ro-bind", earlyconfig.name, earlyconfig.name, + *(["--ro-bind", str(sbat), str(sbat)] if sbat else []), ], ), ) +def find_signed_grub_image(context: Context) -> Optional[Path]: + arch = context.config.architecture.to_efi() + + patterns = [ + f"usr/lib/grub/*-signed/grub{arch}.efi.signed", # Debian/Ubuntu + f"boot/efi/EFI/*/grub{arch}.efi", # Fedora/CentOS + "usr/share/efi/*/grub.efi", # OpenSUSE + ] + + for p in flatten(context.root.glob(pattern) for pattern in patterns): + if p.is_symlink() and p.readlink().is_absolute(): + logging.warning(f"Ignoring signed grub EFI binary which is an absolute path to {p.readlink()}") + continue + + return p + + return None + + def install_grub(context: Context) -> None: if not want_grub_bios(context) and not want_grub_efi(context): return @@ -1266,9 +1316,28 @@ def install_grub(context: Context) -> None: with umask(~0o700): output.parent.mkdir(parents=True, exist_ok=True) - grub_mkimage(context, target="x86_64-efi", output=output, modules=("chain",)) - if context.config.secure_boot: - sign_efi_binary(context, output, output) + if context.config.shim_bootloader == ShimBootloader.signed: + if not (signed := find_signed_grub_image(context)): + if context.config.bootable == ConfigFeature.enabled: + die("Couldn't find a signed grub EFI binary installed in the image") + + return + + rel = output.relative_to(context.root) + log_step(f"Installing signed grub EFI binary from /{signed.relative_to(context.root)} to /{rel}") + shutil.copy2(signed, output) + else: + if context.config.secure_boot and context.config.shim_bootloader != ShimBootloader.none: + if not (signed := find_signed_grub_image(context)): + die("Couldn't find a signed grub EFI binary installed in the image to extract SBAT from") + + sbat = extract_pe_section(context, signed, ".sbat", context.workspace / "sbat") + else: + sbat = None + + grub_mkimage(context, target="x86_64-efi", output=output, modules=("chain",), sbat=sbat) + if context.config.secure_boot: + sign_efi_binary(context, output, output) dst = context.root / "efi" / context.config.distribution.grub_prefix() / "fonts" with umask(~0o700): @@ -1754,7 +1823,7 @@ def python_binary(config: Config) -> str: return "python3" if config.tools_tree else os.getenv("MKOSI_INTERPRETER", "python3") -def extract_pe_section(context: Context, binary: Path, section: str, output: Path) -> None: +def extract_pe_section(context: Context, binary: Path, section: str, output: Path) -> Path: # When using a tools tree, we want to use the pefile module from the tools tree instead of requiring that # python-pefile is installed on the host. So we execute python as a subprocess to make sure we load # pefile from the tools tree if one is used. @@ -1779,6 +1848,8 @@ def extract_pe_section(context: Context, binary: Path, section: str, output: Pat sandbox=context.sandbox(options=["--ro-bind", binary, binary]) ) + return output + def want_signed_pcrs(config: Config) -> bool: return ( @@ -1983,6 +2054,7 @@ def install_type1( if ( want_efi(context.config) and context.config.secure_boot and + context.config.shim_bootloader != ShimBootloader.signed and KernelType.identify(context.config, kimg) == KernelType.pe ): kimg = sign_efi_binary(context, kimg, dst / "vmlinuz") @@ -2063,29 +2135,40 @@ def install_uki(context: Context, kver: str, kimg: Path, token: str, partitions: else: boot_binary = context.root / f"boot/EFI/Linux/{token}-{kver}{boot_count}.efi" - microcode = build_microcode_initrd(context) - - initrds = [microcode] if microcode else [] - initrds += context.config.initrds or [build_default_initrd(context)] - - if context.config.kernel_modules_initrd: - initrds += [build_kernel_modules_initrd(context, kver)] - # Make sure the parent directory where we'll be writing the UKI exists. with umask(~0o700): boot_binary.parent.mkdir(parents=True, exist_ok=True) - build_uki( - context, - systemd_stub_binary(context), - kver, - context.root / kimg, - initrds, - finalize_cmdline(context, roothash), - boot_binary, - ) + if context.config.shim_bootloader == ShimBootloader.signed: + for p in (context.root / "usr/lib/modules" / kver).glob("*.efi"): + log_step(f"Installing prebuilt UKI at {p} to {boot_binary}") + shutil.copy2(p, boot_binary) + break + else: + if context.config.bootable == ConfigFeature.enabled: + die(f"Couldn't find a signed UKI binary installed at /usr/lib/modules/{kver} in the image") + + return + else: + microcode = build_microcode_initrd(context) + + initrds = [microcode] if microcode else [] + initrds += context.config.initrds or [build_default_initrd(context)] + + if context.config.kernel_modules_initrd: + initrds += [build_kernel_modules_initrd(context, kver)] + + build_uki( + context, + systemd_stub_binary(context), + kver, + context.root / kimg, + initrds, + finalize_cmdline(context, roothash), + boot_binary, + ) - print_output_size(boot_binary) + print_output_size(boot_binary) if want_grub_efi(context): config = prepare_grub_config(context) diff --git a/mkosi/config.py b/mkosi/config.py index ad01d44a5..ee8fda78a 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -249,10 +249,14 @@ class Cacheonly(StrEnum): class QemuFirmware(StrEnum): - auto = enum.auto() - linux = enum.auto() - uefi = enum.auto() - bios = enum.auto() + auto = enum.auto() + linux = enum.auto() + uefi = enum.auto() + uefi_secure_boot = enum.auto() + bios = enum.auto() + + def is_uefi(self) -> bool: + return self in (QemuFirmware.uefi, QemuFirmware.uefi_secure_boot) class Network(StrEnum): @@ -384,7 +388,7 @@ class Architecture(StrEnum): if self.is_x86_variant(): return True - return self.is_arm_variant() and firmware == QemuFirmware.uefi + return self.is_arm_variant() and firmware.is_uefi() def supports_fw_cfg(self) -> bool: return self.is_x86_variant() or self.is_arm_variant() @@ -2689,7 +2693,7 @@ SETTINGS = ( dest="qemu_firmware_variables", metavar="PATH", section="Host", - parse=config_make_path_parser(), + parse=config_make_path_parser(constants=("custom", "microsoft")), help="Set the path to the qemu firmware variables file to use", ), ConfigSetting( diff --git a/mkosi/qemu.py b/mkosi/qemu.py index 1ce7dbe33..7e40946eb 100644 --- a/mkosi/qemu.py +++ b/mkosi/qemu.py @@ -7,6 +7,7 @@ import enum import errno import fcntl import hashlib +import json import logging import os import random @@ -19,10 +20,9 @@ import tempfile import uuid from collections.abc import Iterator from pathlib import Path -from typing import Optional +from typing import NamedTuple, Optional from mkosi.config import ( - Architecture, Args, Config, ConfigFeature, @@ -40,7 +40,7 @@ from mkosi.run import AsyncioThread, find_binary, fork_and_wait, run, spawn from mkosi.tree import copy_tree, rmtree from mkosi.types import PathString from mkosi.user import INVOKING_USER, become_root -from mkosi.util import StrEnum +from mkosi.util import StrEnum, flatten from mkosi.versioncomp import GenericVersion QEMU_KVM_DEVICE_VERSION = GenericVersion("9.0") @@ -172,119 +172,80 @@ def find_qemu_binary(config: Config) -> str: die("Couldn't find QEMU/KVM binary") -def find_ovmf_firmware(config: Config) -> tuple[Path, bool]: - FIRMWARE_LOCATIONS = { - Architecture.x86_64: [ - "usr/share/ovmf/x64/OVMF_CODE.secboot.fd", - "usr/share/qemu/ovmf-x86_64.smm.bin", - "usr/share/edk2/x64/OVMF_CODE.secboot.4m.fd", - "usr/share/edk2/x64/OVMF_CODE.secboot.fd", - ], - Architecture.x86: [ - "usr/share/edk2/ovmf-ia32/OVMF_CODE.secboot.fd", - "usr/share/OVMF/OVMF32_CODE_4M.secboot.fd", - "usr/share/edk2/ia32/OVMF_CODE.secboot.4m.fd", - "usr/share/edk2/ia32/OVMF_CODE.secboot.fd", - ], - }.get(config.architecture, []) - - for firmware in FIRMWARE_LOCATIONS: - if (config.tools() / firmware).exists(): - return Path("/") / firmware, True - - FIRMWARE_LOCATIONS = { - Architecture.x86_64: [ - "usr/share/ovmf/ovmf_code_x64.bin", - "usr/share/ovmf/x64/OVMF_CODE.fd", - "usr/share/qemu/ovmf-x86_64.bin", - "usr/share/edk2/x64/OVMF_CODE.4m.fd", - "usr/share/edk2/x64/OVMF_CODE.fd", - ], - Architecture.x86: [ - "usr/share/ovmf/ovmf_code_ia32.bin", - "usr/share/edk2/ovmf-ia32/OVMF_CODE.fd", - "usr/share/edk2/ia32/OVMF_CODE.4m.fd", - "usr/share/edk2/ia32/OVMF_CODE.fd", - ], - Architecture.arm64: ["usr/share/AAVMF/AAVMF_CODE.fd"], - Architecture.arm: ["usr/share/AAVMF/AAVMF32_CODE.fd"], - }.get(config.architecture, []) - - for firmware in FIRMWARE_LOCATIONS: - if (config.tools() / firmware).exists(): - logging.warning("Couldn't find OVMF firmware blob with secure boot support, " - "falling back to OVMF firmware blobs without secure boot support.") - return Path("/") / firmware, False - - # If we can't find an architecture specific path, fall back to some generic paths that might also work. - - FIRMWARE_LOCATIONS = [ - "usr/share/edk2/ovmf/OVMF_CODE.secboot.fd", - "usr/share/edk2-ovmf/OVMF_CODE.secboot.fd", - "usr/share/qemu/OVMF_CODE.secboot.fd", - "usr/share/ovmf/OVMF.secboot.fd", - "usr/share/OVMF/OVMF_CODE_4M.secboot.fd", - "usr/share/OVMF/OVMF_CODE.secboot.fd", - ] +class OvmfConfig(NamedTuple): + description: Path + firmware: Path + format: str + vars: Path + vars_format: str - for firmware in FIRMWARE_LOCATIONS: - if (config.tools() / firmware).exists(): - return Path("/") / firmware, True - - FIRMWARE_LOCATIONS = [ - "usr/share/edk2/ovmf/OVMF_CODE.fd", - "usr/share/edk2-ovmf/OVMF_CODE.fd", - "usr/share/qemu/OVMF_CODE.fd", - "usr/share/ovmf/OVMF.fd", - "usr/share/OVMF/OVMF_CODE_4M.fd", - "usr/share/OVMF/OVMF_CODE.fd", - ] - for firmware in FIRMWARE_LOCATIONS: - if (config.tools() / firmware).exists(): - logging.warn("Couldn't find OVMF firmware blob with secure boot support, " - "falling back to OVMF firmware blobs without secure boot support.") - return Path("/") / firmware, False +def find_ovmf_firmware(config: Config, firmware: QemuFirmware) -> Optional[OvmfConfig]: + if not firmware.is_uefi(): + return None + + desc = flatten( + p.glob("*") + for p in ( + config.tools() / "etc/qemu/firmware", + config.tools() / "usr/share/qemu/firmware", + ) + ) - die("Couldn't find OVMF UEFI firmware blob.") + arch = config.architecture.to_qemu() + machine = config.architecture.default_qemu_machine() + for p in sorted(desc): + if p.is_dir(): + continue -def find_ovmf_vars(config: Config) -> Path: - OVMF_VARS_LOCATIONS = [] + j = json.loads(p.read_text()) - if config.architecture == Architecture.x86_64: - OVMF_VARS_LOCATIONS += [ - "usr/share/ovmf/x64/OVMF_VARS.fd", - "usr/share/qemu/ovmf-x86_64-vars.bin", - "usr/share/edk2/x64/OVMF_VARS.4m.fd", - "usr/share/edk2/x64/OVMF_VARS.fd", - ] - elif config.architecture == Architecture.x86: - OVMF_VARS_LOCATIONS += [ - "usr/share/edk2/ovmf-ia32/OVMF_VARS.fd", - "usr/share/OVMF/OVMF32_VARS_4M.fd", - "usr/share/edk2/ia32/OVMF_VARS.4m.fd", - "usr/share/edk2/ia32/OVMF_VARS.fd", - ] - elif config.architecture == Architecture.arm: - OVMF_VARS_LOCATIONS += ["usr/share/AAVMF/AAVMF32_VARS.fd"] - elif config.architecture == Architecture.arm64: - OVMF_VARS_LOCATIONS += ["usr/share/AAVMF/AAVMF_VARS.fd"] - - OVMF_VARS_LOCATIONS += [ - "usr/share/edk2/ovmf/OVMF_VARS.fd", - "usr/share/edk2-ovmf/OVMF_VARS.fd", - "usr/share/qemu/OVMF_VARS.fd", - "usr/share/ovmf/OVMF_VARS.fd", - "usr/share/OVMF/OVMF_VARS_4M.fd", - "usr/share/OVMF/OVMF_VARS.fd", - ] + if "uefi" not in j["interface-types"]: + logging.debug(f"{p.name} firmware description does not target UEFI, skipping") + continue - for location in OVMF_VARS_LOCATIONS: - if (config.tools() / location).exists(): - return config.tools() / location + for target in j["targets"]: + if target["architecture"] != arch: + continue - die("Couldn't find OVMF UEFI variables file.") + # We cannot use fnmatch as for example our default machine for x86-64 is q35 and the firmware description + # lists "pc-q35-*" so we use a substring check instead. + if any(machine in glob for glob in target["machines"]): + break + else: + logging.debug( + f"{p.name} firmware description does not target architecture {arch} or machine {machine}, skipping" + ) + continue + + if firmware == QemuFirmware.uefi_secure_boot and "secure-boot" not in j["features"]: + logging.debug(f"{p.name} firmware description does not include secure boot, skipping") + continue + + if firmware != QemuFirmware.uefi_secure_boot and "secure-boot" in j["features"]: + logging.debug(f"{p.name} firmware description includes secure boot, skipping") + continue + + if config.qemu_firmware_variables == Path("microsoft") and "enrolled-keys" not in j["features"]: + logging.debug(f"{p.name} firmware description does not have enrolled Microsoft keys, skipping") + continue + + if config.qemu_firmware_variables != Path("microsoft") and "enrolled-keys" in j["features"]: + logging.debug(f"{p.name} firmware description has enrolled Microsoft keys, skipping") + continue + + logging.debug(f"Using {p.name} firmware description") + + return OvmfConfig( + description=Path("/") / p.relative_to(config.tools()), + firmware=Path(j["mapping"]["executable"]["filename"]), + format=j["mapping"]["executable"]["format"], + vars=Path(j["mapping"]["nvram-template"]["filename"]), + vars_format=j["mapping"]["nvram-template"]["format"], + ) + + die("Couldn't find matching OVMF UEFI firmware description") @contextlib.contextmanager @@ -493,7 +454,7 @@ def finalize_qemu_firmware(config: Config, kernel: Optional[Path]) -> QemuFirmwa if config.qemu_firmware == QemuFirmware.auto: if kernel: return ( - QemuFirmware.uefi + QemuFirmware.uefi_secure_boot if KernelType.identify(config, kernel) != KernelType.unknown else QemuFirmware.linux ) @@ -503,11 +464,49 @@ def finalize_qemu_firmware(config: Config, kernel: Optional[Path]) -> QemuFirmwa ): return QemuFirmware.linux else: - return QemuFirmware.uefi + return QemuFirmware.uefi_secure_boot else: return config.qemu_firmware +def finalize_firmware_variables(config: Config, ovmf: OvmfConfig, stack: contextlib.ExitStack) -> tuple[Path, str]: + ovmf_vars = stack.enter_context(tempfile.NamedTemporaryFile(prefix="mkosi-ovmf-vars")) + if config.qemu_firmware_variables in (None, Path("custom"), Path("microsoft")): + ovmf_vars_format = ovmf.vars_format + else: + ovmf_vars_format = "raw" + + if config.qemu_firmware_variables == Path("custom"): + assert config.secure_boot_certificate + run( + [ + "virt-fw-vars", + "--input", ovmf.vars, + "--output", ovmf_vars.name, + "--enroll-cert", config.secure_boot_certificate, + "--add-db", "OvmfEnrollDefaultKeys", config.secure_boot_certificate, + "--no-microsoft", + "--secure-boot", + "--loglevel", "WARNING", + ], + sandbox=config.sandbox( + options=[ + "--bind", ovmf_vars.name, ovmf_vars.name, + "--ro-bind", config.secure_boot_certificate, config.secure_boot_certificate, + ], + ), + ) + else: + vars = ( + config.tools() / ovmf.vars.relative_to("/") + if config.qemu_firmware_variables == Path("microsoft") or not config.qemu_firmware_variables + else config.qemu_firmware_variables + ) + shutil.copy2(vars, Path(ovmf_vars.name)) + + return Path(ovmf_vars.name), ovmf_vars_format + + def run_qemu(args: Args, config: Config) -> None: if config.output_format not in ( OutputFormat.disk, @@ -520,7 +519,8 @@ def run_qemu(args: Args, config: Config) -> None: if ( config.output_format in (OutputFormat.cpio, OutputFormat.uki, OutputFormat.esp) and - config.qemu_firmware not in (QemuFirmware.auto, QemuFirmware.linux, QemuFirmware.uefi) + config.qemu_firmware not in (QemuFirmware.auto, QemuFirmware.linux) and + not config.qemu_firmware.is_uefi() ): die(f"{config.output_format} images cannot be booted with the '{config.qemu_firmware}' firmware") @@ -530,6 +530,9 @@ def run_qemu(args: Args, config: Config) -> None: if config.qemu_kvm == ConfigFeature.enabled and not config.architecture.is_native(): die(f"KVM acceleration requested but {config.architecture} does not match the native host architecture") + if config.qemu_firmware_variables == Path("custom") and not config.secure_boot_certificate: + die("SecureBootCertificate= must be configured to use QemuFirmwareVariables=custom") + # After we unshare the user namespace to sandbox qemu, we might not have access to /dev/kvm or related device nodes # anymore as access to these might be gated behind the kvm group and we won't be part of the kvm group anymore # after unsharing the user namespace. To get around this, open all those device nodes early can pass them as file @@ -573,7 +576,7 @@ def run_qemu(args: Args, config: Config) -> None: config.output_format in (OutputFormat.cpio, OutputFormat.directory, OutputFormat.uki) ) ): - if firmware == QemuFirmware.uefi: + if firmware.is_uefi(): name = config.output if config.output_format == OutputFormat.uki else config.output_split_uki kernel = config.output_dir_or_cwd() / name else: @@ -584,7 +587,7 @@ def run_qemu(args: Args, config: Config) -> None: "or provide a -kernel argument to mkosi qemu" ) - ovmf, ovmf_supports_sb = find_ovmf_firmware(config) if firmware == QemuFirmware.uefi else (None, False) + ovmf = find_ovmf_firmware(config, firmware) # A shared memory backend might increase ram usage so only add one if actually necessary for virtiofsd. shm = [] @@ -592,8 +595,8 @@ def run_qemu(args: Args, config: Config) -> None: shm = ["-object", f"memory-backend-memfd,id=mem,size={config.qemu_mem},share=on"] machine = f"type={config.architecture.default_qemu_machine()}" - if firmware == QemuFirmware.uefi and config.architecture.supports_smm(): - machine += f",smm={'on' if ovmf_supports_sb else 'off'}" + if firmware.is_uefi() and config.architecture.supports_smm(): + machine += f",smm={'on' if firmware == QemuFirmware.uefi_secure_boot else 'off'}" if shm: machine += ",memory-backend=mem" @@ -660,16 +663,18 @@ def run_qemu(args: Args, config: Config) -> None: ] # QEMU has built-in logic to look for the BIOS firmware so we don't need to do anything special for that. - if firmware == QemuFirmware.uefi: - cmdline += ["-drive", f"if=pflash,format=raw,readonly=on,file={ovmf}"] + if firmware.is_uefi(): + assert ovmf + cmdline += ["-drive", f"if=pflash,format={ovmf.format},readonly=on,file={ovmf.firmware}"] notifications: dict[str, str] = {} with contextlib.ExitStack() as stack: - if firmware == QemuFirmware.uefi: - ovmf_vars = stack.enter_context(tempfile.NamedTemporaryFile(prefix="mkosi-ovmf-vars")) - shutil.copy2(config.qemu_firmware_variables or find_ovmf_vars(config), Path(ovmf_vars.name)) - cmdline += ["-drive", f"file={ovmf_vars.name},if=pflash,format=raw"] - if ovmf_supports_sb: + if firmware.is_uefi(): + assert ovmf + ovmf_vars, ovmf_vars_format = finalize_firmware_variables(config, ovmf, stack) + + cmdline += ["-drive", f"file={ovmf_vars},if=pflash,format={ovmf_vars_format}"] + if firmware == QemuFirmware.uefi_secure_boot: cmdline += [ "-global", "ICH9-LPC.disable_s3=1", "-global", "driver=cfi.pflash01,property=secure,value=on", @@ -819,7 +824,7 @@ def run_qemu(args: Args, config: Config) -> None: "-device", f"scsi-{'cd' if config.qemu_cdrom else 'hd'},drive=mkosi,bootindex=1"] if ( - firmware == QemuFirmware.uefi and + firmware.is_uefi() and config.qemu_swtpm != ConfigFeature.disabled and find_binary("swtpm", root=config.tools()) is not None ): diff --git a/mkosi/resources/mkosi-tools/mkosi.conf.d/10-arch.conf b/mkosi/resources/mkosi-tools/mkosi.conf.d/10-arch.conf index 6edab862e..058b85f6e 100644 --- a/mkosi/resources/mkosi-tools/mkosi.conf.d/10-arch.conf +++ b/mkosi/resources/mkosi-tools/mkosi.conf.d/10-arch.conf @@ -25,4 +25,5 @@ Packages= squashfs-tools systemd-ukify ubuntu-keyring + virt-firmware xz diff --git a/mkosi/resources/mkosi-tools/mkosi.conf.d/10-centos-fedora/mkosi.conf b/mkosi/resources/mkosi-tools/mkosi.conf.d/10-centos-fedora/mkosi.conf index 157f868c7..539742788 100644 --- a/mkosi/resources/mkosi-tools/mkosi.conf.d/10-centos-fedora/mkosi.conf +++ b/mkosi/resources/mkosi-tools/mkosi.conf.d/10-centos-fedora/mkosi.conf @@ -26,4 +26,5 @@ Packages= systemd-container systemd-udev ubu-keyring + virt-firmware xz diff --git a/mkosi/resources/mkosi.md b/mkosi/resources/mkosi.md index 2ca99abda..bf7294d24 100644 --- a/mkosi/resources/mkosi.md +++ b/mkosi/resources/mkosi.md @@ -1190,6 +1190,11 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, on UEFI firmware is requested using other options (`Bootable=`, `Bootloader=`). +: Note that when this option is enabled, mkosi will only install already + signed bootloader binaries, kernel image files and unified kernel + images as self-signed binaries would not be accepted by the signed + version of shim. + `UnifiedKernelImages=`, `--unified-kernel-images=` : Specifies whether to use unified kernel images or not when @@ -1498,32 +1503,35 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, `QemuFirmware=`, `--qemu-firmware=` : When used with the `qemu` verb, this option specifies which firmware - to use. Takes one of `uefi`, `bios`, `linux`, or `auto`. Defaults to - `auto`. When set to `uefi`, the OVMF firmware is used. When set to + to use. Takes one of `uefi`, `uefi-secure-boot`, `bios`, `linux`, or + `auto`. Defaults to `auto`. When set to `uefi`, the OVMF firmware + without secure boot support is used. When set to `uefi-secure-boot`, + the OVMF firmware with secure boot support is used. When set to `bios`, the default SeaBIOS firmware is used. When set to `linux`, direct kernel boot is used. See the `QemuKernel=` option for more details on which kernel image is used with direct kernel boot. When - set to `auto`, `linux` is used if a cpio image is being booted, `uefi` + set to `auto`, `uefi-secure-boot` is used if possible and `linux` otherwise. `QemuFirmwareVariables=`, `--qemu-firmware-variables=` : When used with the `qemu` verb, this option specifies the path to the the firmware variables file to use. Currently, this option is only - taken into account when the `uefi` firmware is used. If not specified, - mkosi will search for the default variables file and use that instead. + taken into account when the `uefi` or `uefi-secure-boot` firmware is + used. If not specified, mkosi will search for the default variables + file and use that instead. + +: When set to `microsoft`, a firmware variables file with the Microsoft + secure boot certificates already enrolled will be used. + +: When set to `custom`, the secure boot certificate from + `SecureBootCertificate=` will be enrolled into the default firmware + variables file. : `virt-fw-vars` from the [virt-firmware](https://gitlab.com/kraxel/virt-firmware) project can be used to customize OVMF variable files. -: Some distributions also provide variable files which already have - Microsoft's certificates for secure boot enrolled. For Fedora - and Debian these are `OVMF_VARS.secboot.fd` and `OVMF_VARS_4M.ms.fd` - under `/usr/share/OVMF` respectively. You can use `locate` and look - under `/usr/share/qemu/firmware` for hints on where to find these - files if your distribution ships them. - `QemuKernel=`, `--qemu-kernel=` : Set the kernel image to use for qemu direct kernel boot. If not @@ -1660,6 +1668,7 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, | `ubuntu-keyring` | X | X | X | X | X | | | `util-linux` | X | X | X | X | X | X | | `virtiofsd` | X | X | | | X | X | + | `virt-firmware` | X | X | | | X | | | `xfsprogs` | X | X | X | X | X | X | | `xz` | X | X | X | X | X | X | | `zstd` | X | X | X | X | X | X | diff --git a/mkosi/vmspawn.py b/mkosi/vmspawn.py index 4ec5b4709..d4fa3716d 100644 --- a/mkosi/vmspawn.py +++ b/mkosi/vmspawn.py @@ -17,6 +17,7 @@ from mkosi.log import die from mkosi.qemu import ( copy_ephemeral, finalize_qemu_firmware, + find_ovmf_firmware, ) from mkosi.run import run from mkosi.types import PathString @@ -32,6 +33,9 @@ def run_vmspawn(args: Args, config: Config) -> None: if config.qemu_cdrom: die("systemd-vmspawn does not support CD-ROM images") + if config.qemu_firmware_variables and config.qemu_firmware_variables != Path("microsoft"): + die("mkosi vmspawn does not support QemuFirmwareVariables=") + kernel = config.qemu_kernel if kernel and not kernel.exists(): @@ -49,10 +53,10 @@ def run_vmspawn(args: Args, config: Config) -> None: cmdline: list[PathString] = [ "systemd-vmspawn", - "--qemu-smp", config.qemu_smp, - "--qemu-mem", config.qemu_mem, - "--qemu-kvm", config.qemu_kvm.to_tristate(), - "--qemu-vsock", config.qemu_vsock.to_tristate(), + "--cpus", config.qemu_smp, + "--ram", config.qemu_mem, + "--kvm", config.qemu_kvm.to_tristate(), + "--vsock", config.qemu_vsock.to_tristate(), "--tpm", config.qemu_swtpm.to_tristate(), "--secure-boot", yes_no(config.secure_boot), ] @@ -63,7 +67,11 @@ def run_vmspawn(args: Args, config: Config) -> None: cmdline += ["--network-tap"] if config.qemu_gui: - cmdline += ["--qemu-gui"] + cmdline += ["--console=gui"] + + ovmf = find_ovmf_firmware(config, firmware) + if ovmf: + cmdline += ["--firmware", ovmf.description] cmdline += [f"--set-credential={k}:{v}" for k, v in config.credentials.items()] diff --git a/tests/test_boot.py b/tests/test_boot.py index 85709a1d4..9086aea94 100644 --- a/tests/test_boot.py +++ b/tests/test_boot.py @@ -79,7 +79,7 @@ def test_bootloader(config: Image.Config, bootloader: Bootloader) -> None: if config.distribution == Distribution.rhel_ubi: return - firmware = QemuFirmware.linux if bootloader == Bootloader.none else QemuFirmware.uefi + firmware = QemuFirmware.linux if bootloader == Bootloader.none else QemuFirmware.auto with Image( config, -- 2.47.2