]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add support for pesign to sign secure boot binaries
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 25 May 2023 19:33:09 +0000 (21:33 +0200)
committerJörg Behrmann <behrmann@physik.fu-berlin.de>
Fri, 26 May 2023 07:51:47 +0000 (09:51 +0200)
sbsign is not packaged on CentOS so let's add support for pesign as
well as support for pesign was recently added to ukify as well.

mkosi.md
mkosi/__init__.py
mkosi/config.py

index 53417c532c5613c4d9fb3a8ea5118617c6ae9427..3aa70c2a551926b747b984615cde6388bee27575 100644 (file)
--- a/mkosi.md
+++ b/mkosi.md
@@ -898,6 +898,12 @@ a boolean argument: either "1", "yes", or "true" to enable, or "0",
 : Path to the X.509 file containing the certificate for the signed
   UEFI kernel image, if `SecureBoot=` is used.
 
+`SecureBootSignTool=`, `--secure-boot-sign-tool`
+
+: Tool to use to sign secure boot PE binaries. Takes one of `sbsign`, `pesign` or `auto`. Defaults to `auto`.
+  If set to `auto`, either sbsign or pesign are used if available, with sbsign being preferred if both are
+  installed.
+
 `VerityKey=`, `--verity-key=`
 
 : Path to the PEM file containing the secret key for signing the verity signature, if a verity signature
index a0157a26c692cafe5cc53e71026264743ed2fdc0..11c406a92cbe2096a6d688058ea96725eb229cdb 100644 (file)
@@ -28,6 +28,7 @@ from mkosi.config import (
     MkosiArgs,
     MkosiConfig,
     MkosiConfigParser,
+    SecureBootSignTool,
 )
 from mkosi.install import add_dropin_config_from_resource, copy_path, flock
 from mkosi.log import Style, color_error, complete_step, die, log_step
@@ -370,6 +371,60 @@ def run_finalize_script(state: MkosiState) -> None:
             env={**state.environment, "BUILDROOT": str(state.root), "OUTPUTDIR": str(state.config.output_dir)})
 
 
+def certificate_common_name(certificate: Path) -> str:
+    output = run([
+        "openssl",
+        "x509",
+        "-noout",
+        "-subject",
+        "-nameopt", "multiline",
+        "-in", certificate,
+    ], text=True, stdout=subprocess.PIPE).stdout
+
+    for line in output.splitlines():
+        if not line.strip().startswith("commonName"):
+            continue
+
+        _, sep, value = line.partition("=")
+        if not sep:
+            die("Missing '=' delimiter in openssl output")
+
+        return cast(str, value.strip())
+
+    die(f"Certificate {certificate} is missing Common Name")
+
+
+def pesign_prepare(state: MkosiState) -> None:
+    assert state.config.secure_boot_key
+    assert state.config.secure_boot_certificate
+
+    if (state.workspace / "pesign").exists():
+        return
+
+    (state.workspace / "pesign").mkdir()
+
+    # pesign takes a certificate directory and a certificate common name as input arguments, so we have
+    # to transform our input key and cert into that format. Adapted from
+    # https://www.mankier.com/1/pesign#Examples-Signing_with_the_certificate_and_private_key_in_individual_files
+    run(["openssl",
+         "pkcs12",
+         "-export",
+         # Arcane incantation to create a pkcs12 certificate without a password.
+         "-keypbe", "NONE",
+         "-certpbe", "NONE",
+         "-nomaciter",
+         "-passout", "pass:",
+         "-out", state.workspace / "secure-boot.p12",
+         "-inkey", state.config.secure_boot_key,
+         "-in", state.config.secure_boot_certificate])
+
+    run(["pk12util",
+         "-K", "",
+         "-W", "",
+         "-i", state.workspace / "secure-boot.p12",
+         "-d", state.workspace / "pesign"])
+
+
 def install_boot_loader(state: MkosiState) -> None:
     if state.config.bootable == ConfigFeature.disabled:
         return
@@ -397,12 +452,30 @@ def install_boot_loader(state: MkosiState) -> None:
         assert state.config.secure_boot_certificate
 
         with complete_step("Signing systemd-boot binaries…"):
-            for f in itertools.chain(directory.glob('*.efi'), directory.glob('*.EFI')):
-                run(["sbsign",
-                     "--key", state.config.secure_boot_key,
-                     "--cert", state.config.secure_boot_certificate,
-                     "--output", f"{f}.signed",
-                     f])
+            for input in itertools.chain(directory.glob('*.efi'), directory.glob('*.EFI')):
+                output = directory / f"{input}.signed"
+
+                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…"):
         run(["bootctl", "install", "--root", state.root], env={"SYSTEMD_ESP_PATH": "/efi"})
@@ -854,11 +927,21 @@ def install_unified_kernel(state: MkosiState, roothash: Optional[str]) -> None:
                 assert state.config.secure_boot_key
                 assert state.config.secure_boot_certificate
 
-                cmd += [
-                    "--secureboot-private-key", state.config.secure_boot_key,
-                    "--secureboot-certificate", state.config.secure_boot_certificate,
-                    "--sign-kernel",
-                ]
+                cmd += ["--sign-kernel"]
+
+                if state.config.secure_boot_sign_tool != SecureBootSignTool.pesign:
+                    cmd += [
+                        "--signtool", "sbsign",
+                        "--secureboot-private-key", state.config.secure_boot_key,
+                        "--secureboot-certificate", state.config.secure_boot_certificate,
+                    ]
+                else:
+                    pesign_prepare(state)
+                    cmd += [
+                        "--signtool", "pesign",
+                        "--secureboot-certificate-dir", state.workspace / "pesign",
+                        "--secureboot-certificate-name", certificate_common_name(state.config.secure_boot_certificate),
+                    ]
 
                 sign_expected_pcr = (state.config.sign_expected_pcr == ConfigFeature.enabled or
                                     (state.config.sign_expected_pcr == ConfigFeature.auto and
@@ -1279,6 +1362,7 @@ def summary(args: MkosiArgs, config: MkosiConfig) -> str:
                UEFI SecureBoot: {yes_no(config.secure_boot)}
         SecureBoot Signing Key: {none_to_none(config.secure_boot_key)}
         SecureBoot Certificate: {none_to_none(config.secure_boot_certificate)}
+          SecureBoot Sign Tool: {config.secure_boot_sign_tool}
             Verity Signing Key: {none_to_none(config.verity_key)}
             Verity Certificate: {none_to_none(config.verity_certificate)}
                       Checksum: {yes_no(config.checksum)}
index 1a9779f3da555495497da1d8a9866d00d5ee9d98..16f6914a7fbaacbf11986ea4d1ef3536b716f44c 100644 (file)
@@ -58,6 +58,15 @@ class ConfigFeature(enum.Enum):
         return str(self.value).lower()
 
 
+class SecureBootSignTool(enum.Enum):
+    auto = "auto"
+    sbsign = "sbsign"
+    pesign = "pesign"
+
+    def __str__(self) -> str:
+        return str(self.value).lower()
+
+
 def parse_boolean(s: str) -> bool:
     "Parse 1/true/yes/y/t/on as true and 0/false/no/n/f/off/None as false"
     s_l = s.lower()
@@ -594,6 +603,7 @@ class MkosiConfig:
     secure_boot: bool
     secure_boot_key: Optional[Path]
     secure_boot_certificate: Optional[Path]
+    secure_boot_sign_tool: SecureBootSignTool
     verity_key: Optional[Path]
     verity_certificate: Optional[Path]
     sign_expected_pcr: ConfigFeature
@@ -1097,6 +1107,12 @@ class MkosiConfigParser:
             parse=config_make_path_parser(),
             paths=("mkosi.crt",),
         ),
+        MkosiConfigSetting(
+            dest="secure_boot_sign_tool",
+            section="Validation",
+            parse=config_make_enum_parser(SecureBootSignTool),
+            default=SecureBootSignTool.auto,
+        ),
         MkosiConfigSetting(
             dest="verity_key",
             section="Validation",
@@ -1794,6 +1810,12 @@ class MkosiConfigParser:
             help="UEFI SecureBoot certificate in X509 format",
             action=action,
         )
+        group.add_argument(
+            "--secure-boot-sign-tool",
+            metavar="TOOL",
+            help="Tool to use for signing PE binaries for secure boot",
+            action=action,
+        )
         group.add_argument(
             "--verity-key",
             metavar="PATH",