diff options
author | Nicholas Bishop <nicholasbishop@google.com> | 2022-12-06 18:19:16 -0500 |
---|---|---|
committer | Chromeos LUCI <chromeos-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2022-12-15 10:22:41 +0000 |
commit | 47594a266056d8fc0acc94b3fdf39c261086a0f3 (patch) | |
tree | e41c43989d7e74e147c243ee8417b666a9b74a32 /scripts | |
parent | 167d3873450dcd5d735a07d647462829f16b2061 (diff) | |
download | vboot-47594a266056d8fc0acc94b3fdf39c261086a0f3.tar.gz |
Port sign_uefi.sh to Python
Shell scripts are hard to modify and hard to test, so port sign_uefi.sh
to Python. This is a fairly direct port that attempts to keep all the
behavior the same. In particular, there are no hard errors for missing
EFI/kernel files, or for failing to sign one of those files if it does
exist. It might be good to make the script more strict in the future,
but for now try to match the existing behavior.
Nothing actually calls the new script yet.
Also enable `black_check` in `PRESUBMIT.cfg` to enforce formatting.
BRANCH=none
BUG=b:261631233
TEST=make runtests
TEST=cros lint scripts/image_signing/sign_uefi*.py
Change-Id: I4b9b86607cc403779b0504758dd097b0d7237fef
Signed-off-by: Nicholas Bishop <nicholasbishop@google.com>
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/vboot_reference/+/4083506
Reviewed-by: Yu-Ping Wu <yupingso@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
Diffstat (limited to 'scripts')
-rwxr-xr-x | scripts/image_signing/sign_uefi.py | 175 | ||||
-rwxr-xr-x | scripts/image_signing/sign_uefi_unittest.py | 82 |
2 files changed, 257 insertions, 0 deletions
diff --git a/scripts/image_signing/sign_uefi.py b/scripts/image_signing/sign_uefi.py new file mode 100755 index 00000000..53ac87c6 --- /dev/null +++ b/scripts/image_signing/sign_uefi.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# Copyright 2022 The ChromiumOS Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Sign the UEFI binaries in the target directory. + +The target directory can be either the root of ESP or /boot of root filesystem. +""" + +import argparse +import logging +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +from typing import List, Optional + + +def ensure_executable_available(name): + """Exit non-zero if the given executable isn't in $PATH. + + Args: + name: An executable's file name. + """ + if not shutil.which(name): + sys.exit(f"Cannot sign UEFI binaries ({name} not found)") + + +def ensure_file_exists(path, message): + """Exit non-zero if the given file doesn't exist. + + Args: + path: Path to a file. + message: Error message that will be printed if the file doesn't exist. + """ + if not path.is_file(): + sys.exit(f"{message}: {path}") + + +class Signer: + """EFI file signer. + + Attributes: + temp_dir: Path of a temporary directory used as a workspace. + priv_key: Path of the private key. + sign_cert: Path of the signing certificate. + verify_cert: Path of the certificate used to verify the signature. + """ + + def __init__(self, temp_dir, priv_key, sign_cert, verify_cert): + self.temp_dir = temp_dir + self.priv_key = priv_key + self.sign_cert = sign_cert + self.verify_cert = verify_cert + + def sign_efi_file(self, target): + """Sign an EFI binary file, if possible. + + Args: + target: Path of the file to sign. + """ + logging.info("signing efi file %s", target) + + # Allow this to fail, as there maybe no current signature. + subprocess.run(["sudo", "sbattach", "--remove", target], check=False) + + signed_file = self.temp_dir / target.name + try: + subprocess.run( + [ + "sbsign", + "--key", + self.priv_key, + "--cert", + self.sign_cert, + "--output", + signed_file, + target, + ], + check=True, + ) + except subprocess.CalledProcessError: + logging.warning("cannot sign %s", target) + return + + subprocess.run( + ["sudo", "cp", "--force", signed_file, target], check=True + ) + try: + subprocess.run( + ["sbverify", "--cert", self.verify_cert, target], check=True + ) + except subprocess.CalledProcessError: + sys.exit("Verification failed") + + +def sign_target_dir(target_dir, key_dir, efi_glob): + """Sign various EFI files under |target_dir|. + + Args: + target_dir: Path of a boot directory. This can be either the + root of the ESP or /boot of the root filesystem. + key_dir: Path of a directory containing the key and cert files. + efi_glob: Glob pattern of EFI files to sign, e.g. "*.efi". + """ + bootloader_dir = target_dir / "efi/boot" + syslinux_dir = target_dir / "syslinux" + kernel_dir = target_dir + + verify_cert = key_dir / "db/db.pem" + ensure_file_exists(verify_cert, "No verification cert") + + sign_cert = key_dir / "db/db.children/db_child.pem" + ensure_file_exists(sign_cert, "No signing cert") + + sign_key = key_dir / "db/db.children/db_child.rsa" + ensure_file_exists(sign_key, "No signing key") + + with tempfile.TemporaryDirectory() as working_dir: + signer = Signer(Path(working_dir), sign_key, sign_cert, verify_cert) + + for efi_file in sorted(bootloader_dir.glob(efi_glob)): + if efi_file.is_file(): + signer.sign_efi_file(efi_file) + + for syslinux_kernel_file in sorted(syslinux_dir.glob("vmlinuz.?")): + if syslinux_kernel_file.is_file(): + signer.sign_efi_file(syslinux_kernel_file) + + kernel_file = (kernel_dir / "vmlinuz").resolve() + if kernel_file.is_file(): + signer.sign_efi_file(kernel_file) + + +def get_parser() -> argparse.ArgumentParser: + """Get CLI parser.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "target_dir", + type=Path, + help="Path of a boot directory, either the root of the ESP or " + "/boot of the root filesystem", + ) + parser.add_argument( + "key_dir", + type=Path, + help="Path of a directory containing the key and cert files", + ) + parser.add_argument( + "efi_glob", help="Glob pattern of EFI files to sign, e.g. '*.efi'" + ) + return parser + + +def main(argv: Optional[List[str]] = None) -> Optional[int]: + """Sign UEFI binaries. + + Args: + argv: Command-line arguments. + """ + logging.basicConfig(level=logging.INFO) + + parser = get_parser() + opts = parser.parse_args(argv) + + for tool in ("sbattach", "sbsign", "sbverify"): + ensure_executable_available(tool) + + sign_target_dir(opts.target_dir, opts.key_dir, opts.efi_glob) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/scripts/image_signing/sign_uefi_unittest.py b/scripts/image_signing/sign_uefi_unittest.py new file mode 100755 index 00000000..2cd4342c --- /dev/null +++ b/scripts/image_signing/sign_uefi_unittest.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# Copyright 2022 The ChromiumOS Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for sign_uefi.py. + +This is run as part of `make runtests`, or `make runtestscripts` if you +want something a little faster. +""" + +from pathlib import Path +import tempfile +import unittest +from unittest import mock + +import sign_uefi + + +class Test(unittest.TestCase): + """Test sign_uefi.py.""" + + @mock.patch.object(sign_uefi.Signer, "sign_efi_file") + def test_successful_sign(self, mock_sign): + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_dir = Path(tmp_dir) + + # Get key paths. + key_dir = tmp_dir / "keys" + db_dir = key_dir / "db" + db_children_dir = db_dir / "db.children" + + # Get target paths. + target_dir = tmp_dir / "boot" + syslinux_dir = target_dir / "syslinux" + efi_boot_dir = target_dir / "efi/boot" + + # Make test dirs. + syslinux_dir.mkdir(parents=True) + efi_boot_dir.mkdir(parents=True) + db_children_dir.mkdir(parents=True) + + # Make key files. + (db_dir / "db.pem").touch() + (db_children_dir / "db_child.pem").touch() + (db_children_dir / "db_child.rsa").touch() + + # Make EFI files. + (efi_boot_dir / "bootia32.efi").touch() + (efi_boot_dir / "bootx64.efi").touch() + (efi_boot_dir / "testia32.efi").touch() + (efi_boot_dir / "testx64.efi").touch() + (syslinux_dir / "vmlinuz.A").touch() + (syslinux_dir / "vmlinuz.B").touch() + (target_dir / "vmlinuz-5.10.156").touch() + (target_dir / "vmlinuz").symlink_to(target_dir / "vmlinuz-5.10.156") + + # Set an EFI glob that matches only some of the EFI files. + efi_glob = "test*.efi" + + # Sign, but with the actual signing mocked out. + sign_uefi.sign_target_dir(target_dir, key_dir, efi_glob) + + # Check that the correct list of files got signed. + self.assertEqual( + mock_sign.call_args_list, + [ + # The test*.efi files match the glob, + # the boot*.efi files don't. + mock.call(efi_boot_dir / "testia32.efi"), + mock.call(efi_boot_dir / "testx64.efi"), + # Two syslinux kernels. + mock.call(syslinux_dir / "vmlinuz.A"), + mock.call(syslinux_dir / "vmlinuz.B"), + # One kernel in the target dir. + mock.call(target_dir / "vmlinuz-5.10.156"), + ], + ) + + +if __name__ == "__main__": + unittest.main() |