From: Daan De Meyer Date: Thu, 25 May 2023 19:33:09 +0000 (+0200) Subject: Add support for pesign to sign secure boot binaries X-Git-Tag: v15~146 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=42285f0c766017941a07412c8fbee475de2f1a45;p=thirdparty%2Fmkosi.git Add support for pesign to sign secure boot binaries 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. --- diff --git a/mkosi.md b/mkosi.md index 53417c532..3aa70c2a5 100644 --- 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 diff --git a/mkosi/__init__.py b/mkosi/__init__.py index a0157a26c..11c406a92 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -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)} diff --git a/mkosi/config.py b/mkosi/config.py index 1a9779f3d..16f6914a7 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -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",