]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add support for installing shim 2137/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 7 Dec 2023 22:11:19 +0000 (23:11 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Sat, 9 Dec 2023 11:19:47 +0000 (12:19 +0100)
Unfortunately shim is a necessary evil that we have to support. We
add a new option that allows choosing either a signed version, an
unsigned version or none at all (the default).

We also stop redirecting /boot/efi to /efi so that /efi is our pristine
directory for populating the ESP whereas /boot is unclaimed wasteland
free for package managers to write all kinds of stuff to.

mkosi.conf
mkosi.conf.d/20-arch.conf
mkosi.conf.d/20-centos-fedora.conf
mkosi.conf.d/20-debian-ubuntu.conf
mkosi.conf.d/20-opensuse.conf
mkosi/__init__.py
mkosi/config.py
mkosi/resources/mkosi.md
tests/test_json.py

index 529ab7c3cc41adada1eb05005f107fd1dcd07d84..68652b89837d39e21f2a32ec4d66a69c0d234c89 100644 (file)
@@ -9,6 +9,7 @@
 [Content]
 Autologin=yes
 BiosBootloader=grub
+ShimBootloader=signed
 BuildSourcesEphemeral=yes
 
 Packages=
index 6b100db265839b22002e29ee75515151dcc57eea..bf2aaf7d0835618461ce2cb817356e0431c5d999 100644 (file)
@@ -4,6 +4,7 @@
 Distribution=arch
 
 [Content]
+ShimBootloader=unsigned
 Packages=
         apt
         archlinux-keyring
@@ -32,6 +33,7 @@ Packages=
         qemu-base
         sbsigntools
         shadow
+        shim
         socat
         squashfs-tools
         strace
index 21d39cb082be7a4ec49e2e720913ad848026b450..9d7874047210721c343cbc8120748b2be4b225a9 100644 (file)
@@ -32,6 +32,7 @@ Packages=
         python3-cryptography
         qemu-kvm-core
         shadow-utils
+        shim
         socat
         squashfs-tools
         strace
index 2aa934627a95263b50e5d571763a340013908e1f..f4a519bb3121afd2ac907afc6c551b9cc2e204f3 100644 (file)
@@ -35,6 +35,7 @@ Packages=
         qemu-system
         sbsigntool
         shim-signed
+        shim-signed
         socat
         squashfs-tools
         strace
index 968978dd86666bfe6ce070625ac7a7f1bb536ca7..409fc5920d5d54f812a3b47e71b09b766e05a96d 100644 (file)
@@ -32,6 +32,7 @@ Packages=
         qemu-headless
         sbsigntools
         shadow
+        shim
         socat
         squashfs
         strace
index 8c4ce9b3ae1867857548d12bcd185660bd19008a..e5e8e8722b82b505cc37638b1ed645353ede33f4 100644 (file)
@@ -36,6 +36,7 @@ from mkosi.config import (
     MkosiJsonEncoder,
     OutputFormat,
     SecureBootSignTool,
+    ShimBootloader,
     Verb,
     format_bytes,
     format_tree,
@@ -152,12 +153,6 @@ def install_distribution(state: MkosiState) -> None:
                 with umask(~0o500):
                     (state.root / "efi").mkdir(exist_ok=True)
 
-                # Some distributions install EFI binaries directly to /boot/efi. Let's redirect them to /efi
-                # instead.
-                rmtree(state.root / "boot/efi")
-                (state.root / "boot").mkdir(exist_ok=True)
-                (state.root / "boot/efi").symlink_to("../efi")
-
             if state.config.packages:
                 state.config.distribution.install_packages(state, state.config.packages)
 
@@ -722,24 +717,61 @@ def pesign_prepare(state: MkosiState) -> None:
          "-d", state.workspace / "pesign"])
 
 
-def install_systemd_boot(state: MkosiState) -> None:
-    if state.config.bootable == ConfigFeature.disabled:
-        return
+def efi_boot_binary(state: MkosiState) -> Path:
+    arch = state.config.architecture.to_efi()
+    assert arch
+    return Path(f"efi/EFI/BOOT/BOOT{arch.upper()}.EFI")
 
-    if state.config.bootloader != Bootloader.systemd_boot:
-        return
+
+def shim_second_stage_binary(state: MkosiState) -> Path:
+    arch = state.config.architecture.to_efi()
+    assert arch
+    if state.config.distribution == Distribution.opensuse:
+        return Path("efi/EFI/BOOT/grub.EFI")
+    else:
+        return Path(f"efi/EFI/BOOT/grub{arch}.EFI")
+
+
+def sign_efi_binary(state: MkosiState, input: Path, output: Path) -> None:
+    assert state.config.secure_boot_key
+    assert state.config.secure_boot_certificate
 
     if (
-        (
-            state.config.output_format == OutputFormat.cpio or
-            state.config.output_format.is_extension_image() or
-            state.config.overlay
-        )
-        and state.config.bootable == ConfigFeature.auto
+        state.config.secure_boot_sign_tool == SecureBootSignTool.sbsign or
+        state.config.secure_boot_sign_tool == SecureBootSignTool.auto and
+        shutil.which("sbsign") is not None
     ):
+        run([
+            "sbsign",
+            "--key", state.config.secure_boot_key,
+            "--cert", state.config.secure_boot_certificate,
+            "--output", output,
+            input,
+        ])
+    elif (
+        state.config.secure_boot_sign_tool == SecureBootSignTool.pesign or
+        state.config.secure_boot_sign_tool == SecureBootSignTool.auto and
+        shutil.which("pesign") is not None
+    ):
+        pesign_prepare(state)
+        run([
+            "pesign",
+            "--certdir", state.workspace / "pesign",
+            "--certificate", certificate_common_name(state.config.secure_boot_certificate),
+            "--sign",
+            "--force",
+            "--in", input,
+            "--out", output,
+        ])
+    else:
+        die("One of sbsign or pesign is required to use SecureBoot=")
+
+
+def install_systemd_boot(state: MkosiState) -> None:
+    if not want_efi(state.config):
         return
 
-    if state.config.architecture.to_efi() is None and state.config.bootable == ConfigFeature.auto:
+    if state.config.bootloader != Bootloader.systemd_boot:
         return
 
     if not any(gen_kernel_images(state)) and state.config.bootable == ConfigFeature.auto:
@@ -758,38 +790,20 @@ def install_systemd_boot(state: MkosiState) -> None:
         return
 
     if state.config.secure_boot:
-        assert state.config.secure_boot_key
-        assert state.config.secure_boot_certificate
-
         with complete_step("Signing systemd-boot binaries…"):
             for input in itertools.chain(directory.glob('*.efi'), directory.glob('*.EFI')):
                 output = directory / f"{input}.signed"
+                sign_efi_binary(state, input, output)
 
-                if (state.config.secure_boot_sign_tool == SecureBootSignTool.sbsign or
-                    state.config.secure_boot_sign_tool == SecureBootSignTool.auto and
-                    shutil.which("sbsign") is not None):
-                    run(["sbsign",
-                         "--key", state.config.secure_boot_key,
-                         "--cert", state.config.secure_boot_certificate,
-                         "--output", output,
-                         input])
-                elif (state.config.secure_boot_sign_tool == SecureBootSignTool.pesign or
-                      state.config.secure_boot_sign_tool == SecureBootSignTool.auto and
-                      shutil.which("pesign") is not None):
-                    pesign_prepare(state)
-                    run(["pesign",
-                         "--certdir", state.workspace / "pesign",
-                         "--certificate", certificate_common_name(state.config.secure_boot_certificate),
-                         "--sign",
-                         "--force",
-                         "--in", input,
-                         "--out", output])
-                else:
-                    die("One of sbsign or pesign is required to use SecureBoot=")
-
-    with complete_step("Installing boot loader…"):
+    with complete_step("Installing systemd-boot…"):
         run(["bootctl", "install", "--root", state.root, "--all-architectures", "--no-variables"],
-            env={"SYSTEMD_ESP_PATH": "/efi"})
+            env={"SYSTEMD_ESP_PATH": "/efi", "SYSTEMD_LOG_LEVEL": "debug"})
+
+        if state.config.shim_bootloader != ShimBootloader.none:
+            shutil.copy2(
+                state.root / f"efi/EFI/systemd/systemd-boot{state.config.architecture.to_efi()}.efi",
+                state.root / shim_second_stage_binary(state),
+            )
 
     if state.config.secure_boot:
         assert state.config.secure_boot_key
@@ -823,6 +837,85 @@ def install_systemd_boot(state: MkosiState) -> None:
                      state.workspace / "mkosi.esl"])
 
 
+def find_and_install_shim_binary(
+    state: MkosiState,
+    name: str,
+    signed: Sequence[str],
+    unsigned: Sequence[str],
+    output: Path,
+) -> None:
+    if state.config.shim_bootloader == ShimBootloader.signed:
+        for pattern in signed:
+            for p in state.root.glob(pattern):
+                log_step(f"Installing signed {name} EFI binary from /{p.relative_to(state.root)} to /{output}")
+                shutil.copy2(p, state.root / output)
+                return
+
+        if state.config.bootable == ConfigFeature.enabled:
+            die(f"Couldn't find signed {name} EFI binary installed in the image")
+    else:
+        for pattern in unsigned:
+            for p in state.root.glob(pattern):
+                if state.config.secure_boot:
+                    log_step(f"Signing and installing unsigned {name} EFI binary from /{p.relative_to(state.root)} to /{output}")
+                    sign_efi_binary(state, p, state.root / output)
+                else:
+                    log_step(f"Installing unsigned {name} EFI binary /{p.relative_to(state.root)} to /{output}")
+                    shutil.copy2(p, state.root / output)
+
+                return
+
+        if state.config.bootable == ConfigFeature.enabled:
+            die(f"Couldn't find unsigned {name} EFI binary installed in the image")
+
+
+def install_shim(state: MkosiState) -> None:
+    if not want_efi(state.config):
+        return
+
+    if state.config.shim_bootloader == ShimBootloader.none:
+        return
+
+    if not any(gen_kernel_images(state)) and state.config.bootable == ConfigFeature.auto:
+        return
+
+    dst = efi_boot_binary(state)
+    with umask(~0o700):
+        (state.root / dst).parent.mkdir(parents=True, exist_ok=True)
+
+    arch = state.config.architecture.to_efi()
+
+    signed = [
+        f"usr/lib/shim/shim{arch}.efi.signed", # Debian
+        f"usr/lib/shim/shim{arch}.efi.signed.latest", # Ubuntu
+        f"boot/efi/EFI/*/shim{arch}.efi", # Fedora/CentOS
+        "usr/share/efi/*/shim.efi", # OpenSUSE
+    ]
+
+    unsigned = [
+        f"usr/lib/shim/shim{arch}.efi", # Debian/Ubuntu
+        f"usr/share/shim/*/*/shim{arch}.efi", # Fedora/CentOS
+        f"usr/share/shim/shim{arch}.efi", # Arch
+    ]
+
+    find_and_install_shim_binary(state, "shim", signed, unsigned, dst)
+
+    signed = [
+        f"usr/lib/shim/mm{arch}.efi.signed", # Debian
+        f"usr/lib/shim/mm{arch}.efi", # Ubuntu
+        f"boot/efi/EFI/*/mm{arch}.efi", # Fedora/CentOS
+        "usr/share/efi/*/MokManager.efi", # OpenSUSE
+    ]
+
+    unsigned = [
+        f"usr/lib/shim/mm{arch}.efi", # Debian/Ubuntu
+        f"usr/share/shim/*/*/mm{arch}.efi", # Fedora/CentOS
+        f"usr/share/shim/mm{arch}.efi", # Arch
+    ]
+
+    find_and_install_shim_binary(state, "mok", signed, unsigned, dst.parent)
+
+
 def find_grub_bios_directory(state: MkosiState) -> Optional[Path]:
     for d in ("usr/lib/grub/i386-pc", "usr/share/grub2/i386-pc"):
         if (p := state.root / d).exists() and any(p.iterdir()):
@@ -1402,10 +1495,11 @@ def build_uki(
         run(cmd)
 
 
-def want_uki(config: MkosiConfig) -> bool:
-    # Do we want to build an UKI according to config?
+def want_efi(config: MkosiConfig) -> bool:
+    # Do we want to make the image bootable on EFI firmware?
     # Note that this returns True also in the case where autodetection might later
-    # cause the UKI not to be installed after the file system has been populated.
+    # cause the system to not be made bootable on EFI firmware after the filesystem
+    # has been populated.
 
     if config.output_format in (OutputFormat.uki, OutputFormat.esp):
         return True
@@ -1422,7 +1516,10 @@ def want_uki(config: MkosiConfig) -> bool:
     ):
         return False
 
-    if config.architecture.to_efi() is None and config.bootable == ConfigFeature.auto:
+    if config.architecture.to_efi() is None:
+        if config.bootable == ConfigFeature.enabled:
+            die(f"Cannot make image bootable on UEFI on {config.architecture} architecture")
+
         return False
 
     return True
@@ -1435,7 +1532,7 @@ def install_uki(state: MkosiState, partitions: Sequence[Partition]) -> None:
     # benefit that they can be signed like normal EFI binaries, and can encode everything necessary to boot a
     # specific root device, including the root hash.
 
-    if not want_uki(state.config) or state.config.output_format in (OutputFormat.uki, OutputFormat.esp):
+    if not want_efi(state.config) or state.config.output_format in (OutputFormat.uki, OutputFormat.esp):
         return
 
     arch = state.config.architecture.to_efi()
@@ -1454,7 +1551,10 @@ def install_uki(state: MkosiState, partitions: Sequence[Partition]) -> None:
             boot_count = f'+{(state.root / "etc/kernel/tries").read_text().strip()}'
 
         if state.config.bootloader == Bootloader.uki:
-            boot_binary = state.root / "efi/EFI/BOOT/BOOTX64.EFI"
+            if state.config.shim_bootloader != ShimBootloader.none:
+                boot_binary = state.root / shim_second_stage_binary(state)
+            else:
+                boot_binary = state.root / efi_boot_binary(state)
         elif state.config.image_version:
             boot_binary = (
                 state.root / f"efi/EFI/Linux/{image_id}_{state.config.image_version}-{kver}{boot_count}.efi"
@@ -1767,7 +1867,7 @@ def check_systemd_tool(*tools: PathString, version: str, reason: str, hint: Opti
 
 
 def check_tools(args: MkosiArgs, config: MkosiConfig) -> None:
-    if want_uki(config):
+    if want_efi(config):
         check_systemd_tool(
             "ukify", "/usr/lib/systemd/ukify",
             version="254",
@@ -2378,6 +2478,7 @@ def build_image(args: MkosiArgs, config: MkosiConfig) -> None:
             configure_clock(state)
 
             install_systemd_boot(state)
+            install_shim(state)
             run_sysusers(state)
             run_preset(state)
             run_depmod(state)
index 92670f803e5f2ad2aa33ed0489007de02189cdb8..dbdd068808bd815ac45757d6354e47be3654b35a 100644 (file)
@@ -209,6 +209,12 @@ class BiosBootloader(StrEnum):
     grub = enum.auto()
 
 
+class ShimBootloader(StrEnum):
+    none     = enum.auto()
+    signed   = enum.auto()
+    unsigned = enum.auto()
+
+
 class QemuFirmware(StrEnum):
     auto   = enum.auto()
     linux  = enum.auto()
@@ -922,6 +928,7 @@ class MkosiConfig:
     bootable: ConfigFeature
     bootloader: Bootloader
     bios_bootloader: BiosBootloader
+    shim_bootloader: ShimBootloader
     initrds: list[Path]
     initrd_packages: list[str]
     kernel_command_line: list[str]
@@ -1663,6 +1670,15 @@ SETTINGS = (
         default=BiosBootloader.none,
         help="Specify which BIOS bootloader to use",
     ),
+    MkosiConfigSetting(
+        dest="shim_bootloader",
+        metavar="BOOTLOADER",
+        section="Content",
+        parse=config_make_enum_parser(ShimBootloader),
+        choices=ShimBootloader.values(),
+        default=ShimBootloader.none,
+        help="Specify whether to use shim",
+    ),
     MkosiConfigSetting(
         dest="initrds",
         long="--initrd",
@@ -2993,6 +3009,7 @@ def summary(config: MkosiConfig) -> str:
                            Bootable: {yes_no_auto(config.bootable)}
                          Bootloader: {config.bootloader}
                     BIOS Bootloader: {config.bios_bootloader}
+                    Shim Bootloader: {config.shim_bootloader}
                             Initrds: {line_join_list(config.initrds)}
                     Initrd Packages: {line_join_list(config.initrd_packages)}
                 Kernel Command Line: {line_join_list(config.kernel_command_line)}
@@ -3165,6 +3182,7 @@ def json_type_transformer(refcls: Union[type[MkosiArgs], type[MkosiConfig]]) ->
         tuple[str, ...]: str_tuple_transformer,
         Architecture: enum_transformer,
         BiosBootloader: enum_transformer,
+        ShimBootloader: enum_transformer,
         Bootloader: enum_transformer,
         Compression: enum_transformer,
         ConfigFeature: enum_transformer,
index 770191bd203495c8ebf1b34cb0e93b03456e267c..6efc52750c6c8cecf3575d9112a57fc4569c3cd3 100644 (file)
@@ -1021,6 +1021,21 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
 : Even if no EFI bootloader is installed, we still need an ESP for BIOS
   boot as that's where we store the kernel, initrd and grub modules.
 
+`ShimBootloader=`, `--shim-bootloader=`
+
+: Takes one of `none`, `unsigned`, or `signed`. Defaults to `none`. If
+  set to `none`, shim and MokManager will not be installed to the ESP.
+  If set to `unsigned`, mkosi will search for unsigned shim and
+  MokManager EFI binaries and install them. If `SecureBoot=` is enabled,
+  mkosi will sign the unsigned EFI binaries before installing thel. If
+  set to `signed`, mkosi will search for signed EFI binaries and install
+  those. Even if `SecureBoot=` is enabled, mkosi won't sign these
+  binaries again.
+
+: Note that this option only takes effect when an image that is bootable
+  on UEFI firmware is requested using other options
+  (`Bootable=`, `Bootloader=`).
+
 `Initrds=`, `--initrd`
 
 : Use user-provided initrd(s). Takes a comma separated list of paths to
index 35eaed1d109a3e8d886b72ddd95e95844405c763..2b13e6a7b04f445345974874c06c16b5ee50d076 100644 (file)
@@ -24,6 +24,7 @@ from mkosi.config import (
     QemuFirmware,
     QemuVsockCID,
     SecureBootSignTool,
+    ShimBootloader,
     Verb,
 )
 from mkosi.distributions import Distribution
@@ -243,6 +244,7 @@ def test_config() -> None:
             "SecureBootKey": "/path/to/keyfile",
             "SecureBootSignTool": "pesign",
             "Seed": "7496d7d8-7f08-4a2b-96c6-ec8c43791b60",
+            "ShimBootloader": "none",
             "Sign": false,
             "SignExpectedPcr": "disabled",
             "SkeletonTrees": [
@@ -367,6 +369,7 @@ def test_config() -> None:
         secure_boot_key = Path("/path/to/keyfile"),
         secure_boot_sign_tool = SecureBootSignTool.pesign,
         seed = uuid.UUID("7496d7d8-7f08-4a2b-96c6-ec8c43791b60"),
+        shim_bootloader = ShimBootloader.none,
         sign = False,
         sign_expected_pcr = ConfigFeature.disabled,
         skeleton_trees = [ConfigTree(Path("/foo/bar"), Path("/")), ConfigTree(Path("/bar/baz"), Path("/qux"))],