]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Rework QemuFirmware= 2478/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Mon, 11 Mar 2024 13:57:58 +0000 (14:57 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 13 Mar 2024 10:11:29 +0000 (11:11 +0100)
- 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.

13 files changed:
mkosi.conf
mkosi.conf.d/20-arch.conf
mkosi.conf.d/20-opensuse.conf
mkosi.conf.d/30-centos-fedora/mkosi.conf.d/20-uefi.conf
mkosi.conf.d/30-debian-ubuntu/mkosi.conf.d/20-x86-64.conf
mkosi/__init__.py
mkosi/config.py
mkosi/qemu.py
mkosi/resources/mkosi-tools/mkosi.conf.d/10-arch.conf
mkosi/resources/mkosi-tools/mkosi.conf.d/10-centos-fedora/mkosi.conf
mkosi/resources/mkosi.md
mkosi/vmspawn.py
tests/test_boot.py

index 8b663591827b43befe7cfb739a6ad0774ed24b84..9d39f7a341f8fa627a23f8467db4c1a7d15144d5 100644 (file)
@@ -9,8 +9,8 @@
 
 [Content]
 Autologin=yes
-@ShimBootloader=signed
 @SELinuxRelabel=no
+@ShimBootloader=unsigned
 BuildSources=.
 BuildSourcesEphemeral=yes
 
index 9b74f61bf4296780153e873861bfc58cd73022ad..660dae526a17d217a59a3dd6d174fa88ef7ceda2 100644 (file)
@@ -4,7 +4,6 @@
 Distribution=arch
 
 [Content]
-ShimBootloader=unsigned
 Packages=
         apt
         archlinux-keyring
index cfc37d9b112422c95579f4a5fe92961beda469d1..15d8f741e0ba39acab24973f5dd61174d5f14317 100644 (file)
@@ -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
index 400758906a225422b96b4c91d973bf2ebedc3718..895a4b18a990b815dba63af9a22a2c8b4c4506b2 100644 (file)
@@ -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
index 48b054758ab0fc97ecfb8817e8de37a5332380be..7c3bcfe4ad291b6551bdc316beb0b8bd87d29154 100644 (file)
@@ -6,6 +6,7 @@ Architecture=x86-64
 [Content]
 Packages=
         amd64-microcode
-        grub-pc-bin
+        grub-efi
         grub-efi-amd64
+        grub-pc-bin
         intel-microcode
index 0dd22f300cbcb5a8287f85c7d60bc03cc62db802..e3c6befb50572112b27d441eba7fda55c81fc06c 100644 (file)
@@ -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/<distribution>/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/<distribution>/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)
index ad01d44a5bcd7406503b20ec415f740d125eb3d7..ee8fda78a526be64e34e079fbb86bb230610efe6 100644 (file)
@@ -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(
index 1ce7dbe330ade9806e1c5e29d398f9fc555b501e..7e40946ebe548b359e9c40d49661f0489e32e834 100644 (file)
@@ -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
         ):
index 6edab862ed95ab0d3cb6f3cc46725a34e1a2b5ab..058b85f6e5a823297536417ee52cd71fe7922914 100644 (file)
@@ -25,4 +25,5 @@ Packages=
         squashfs-tools
         systemd-ukify
         ubuntu-keyring
+        virt-firmware
         xz
index 157f868c7742cc97188708c70671cc78b2d5166a..53974278863dd734a9e4e11aceb23411d93ea0d0 100644 (file)
@@ -26,4 +26,5 @@ Packages=
         systemd-container
         systemd-udev
         ubu-keyring
+        virt-firmware
         xz
index 2ca99abda3637e6da096b5bc2e34d25c1ae3ff65..bf7294d244134e68db3edbbe7cde0a427cf8feff 100644 (file)
@@ -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        |
index 4ec5b4709f87025c1e19fa5c31e6ec94de1c8e92..d4fa3716d9dbd2daca036708eb1032340d065933 100644 (file)
@@ -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()]
 
index 85709a1d4efcea626e9c7766f652e0737e6e2f41..9086aea9481ebbdc28331d170f92716f3986db79 100644 (file)
@@ -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,