xref: /aosp_15_r20/external/vboot_reference/scripts/image_signing/sign_uefi.py (revision 8617a60d3594060b7ecbd21bc622a7c14f3cf2bc)
1#!/usr/bin/env python3
2# Copyright 2022 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Sign the UEFI binaries in the target directory.
7
8The target directory can be either the root of ESP or /boot of root filesystem.
9"""
10
11import argparse
12import dataclasses
13import logging
14import os
15from pathlib import Path
16import shutil
17import subprocess
18import sys
19import tempfile
20from typing import List, Optional
21
22
23def ensure_executable_available(name):
24    """Exit non-zero if the given executable isn't in $PATH.
25
26    Args:
27        name: An executable's file name.
28    """
29    if not shutil.which(name):
30        sys.exit(f"Cannot sign UEFI binaries ({name} not found)")
31
32
33def ensure_file_exists(path, message):
34    """Exit non-zero if the given file doesn't exist.
35
36    Args:
37        path: Path to a file.
38        message: Error message that will be printed if the file doesn't exist.
39    """
40    if not path.is_file():
41        sys.exit(f"{message}: {path}")
42
43
44def is_pkcs11_key_path(path: os.PathLike) -> bool:
45    """Check if the key path is a PKCS#11 URI.
46
47    If the key path starts with "pkcs11:", it should be treated as a
48    PKCS#11 URI instead of a local file path.
49    """
50    return str(path).startswith("pkcs11:")
51
52
53@dataclasses.dataclass(frozen=True)
54class Keys:
55    """Public and private keys paths.
56
57    Attributes:
58        private_key: Path of the private signing key
59        sign_cert: Path of the signing certificate
60        verify_cert: Path of the verification certificate
61        kernel_subkey_vbpubk: Path of the kernel subkey public key
62        crdyshim_private_key: Path of the private crdyshim key
63    """
64
65    private_key: os.PathLike
66    sign_cert: os.PathLike
67    verify_cert: os.PathLike
68    kernel_subkey_vbpubk: os.PathLike
69    crdyshim_private_key: os.PathLike
70
71
72class Signer:
73    """EFI file signer.
74
75    Attributes:
76        temp_dir: Path of a temporary directory used as a workspace.
77        keys: An instance of Keys.
78    """
79
80    def __init__(self, temp_dir: os.PathLike, keys: Keys):
81        self.temp_dir = temp_dir
82        self.keys = keys
83
84    def sign_efi_file(self, target):
85        """Sign an EFI binary file, if possible.
86
87        Args:
88            target: Path of the file to sign.
89        """
90        logging.info("signing efi file %s", target)
91
92        # Remove any existing signatures, in case the file being signed
93        # was signed previously. Allow this to fail, as there may not be
94        # any signatures.
95        subprocess.run(["sudo", "sbattach", "--remove", target], check=False)
96
97        signed_file = self.temp_dir / target.name
98        sign_cmd = [
99            "sbsign",
100            "--key",
101            self.keys.private_key,
102            "--cert",
103            self.keys.sign_cert,
104            "--output",
105            signed_file,
106            target,
107        ]
108        if is_pkcs11_key_path(self.keys.private_key):
109            sign_cmd += ["--engine", "pkcs11"]
110
111        try:
112            logging.info("running sbsign: %r", sign_cmd)
113            subprocess.run(sign_cmd, check=True)
114        except subprocess.CalledProcessError:
115            logging.warning("cannot sign %s", target)
116            return
117
118        subprocess.run(
119            ["sudo", "cp", "--force", signed_file, target], check=True
120        )
121        try:
122            subprocess.run(
123                ["sbverify", "--cert", self.keys.verify_cert, target],
124                check=True,
125            )
126        except subprocess.CalledProcessError:
127            sys.exit("Verification failed")
128
129    def create_detached_signature(self, input_path: os.PathLike):
130        """Create a detached signature using the crdyshim private key.
131
132        The signature file will be created at the same location as
133        |efi_file|, but with the extension changed to ".sig".
134
135        Args:
136            input_path: Path of the file to sign.
137        """
138        sig_name = input_path.stem + ".sig"
139
140        # Create the signature in the temporary dir so that openssl
141        # doesn't have to run as root.
142        temp_sig_path = self.temp_dir / sig_name
143        cmd = [
144            "openssl",
145            "pkeyutl",
146            "-sign",
147            "-rawin",
148            "-in",
149            input_path,
150            "-inkey",
151            self.keys.crdyshim_private_key,
152            "-out",
153            temp_sig_path,
154        ]
155        if is_pkcs11_key_path(self.keys.private_key):
156            cmd += ["--engine", "pkcs11"]
157
158        logging.info("creating signature: %r", cmd)
159        subprocess.run(cmd, check=True)
160
161        output_path = input_path.parent / sig_name
162        subprocess.run(["sudo", "cp", temp_sig_path, output_path], check=True)
163
164
165def inject_vbpubk(efi_file: os.PathLike, keys: Keys):
166    """Update a UEFI executable's vbpubk section.
167
168    The crdyboot bootloader contains an embedded public key in the
169    ".vbpubk" section. This function replaces the data in the existing
170    section (normally containing a dev key) with the real key.
171
172    Args:
173        efi_file: Path of a UEFI file.
174        keys: An instance of Keys.
175    """
176    section_name = ".vbpubk"
177    logging.info("updating section %s in %s", section_name, efi_file.name)
178    subprocess.run(
179        [
180            "sudo",
181            "objcopy",
182            "--update-section",
183            f"{section_name}={keys.kernel_subkey_vbpubk}",
184            efi_file,
185        ],
186        check=True,
187    )
188
189
190def check_keys(keys: Keys):
191    """Checks existence of the keys used for signing.
192
193    Exits the process if the check fails and a key is
194    not present.
195
196    Args:
197        keys: The keys to check.
198    """
199
200    # Check for the existence of the key files.
201    ensure_file_exists(keys.verify_cert, "No verification cert")
202    ensure_file_exists(keys.sign_cert, "No signing cert")
203    ensure_file_exists(keys.kernel_subkey_vbpubk, "No kernel subkey public key")
204    # Only check the private keys if they are local paths rather than a
205    # PKCS#11 URI.
206    if not is_pkcs11_key_path(keys.private_key):
207        ensure_file_exists(keys.private_key, "No signing key")
208    # Do not check |keys.crdyshim_private_key| here, as it is not
209    # present in all key set versions.
210
211
212def sign_target_dir(target_dir: os.PathLike, keys: Keys, efi_glob: str):
213    """Sign various EFI files under |target_dir|.
214
215    Args:
216        target_dir: Path of a boot directory. This can be either the
217            root of the ESP or /boot of the root filesystem.
218        keys: An instance of Keys.
219        efi_glob: Glob pattern of EFI files to sign, e.g. "*.efi".
220    """
221    bootloader_dir = target_dir / "efi/boot"
222    syslinux_dir = target_dir / "syslinux"
223    kernel_dir = target_dir
224
225    # Verify all keys are present for signing.
226    check_keys(keys)
227
228    with tempfile.TemporaryDirectory() as working_dir:
229        working_dir = Path(working_dir)
230        signer = Signer(working_dir, keys)
231
232        for efi_file in sorted(bootloader_dir.glob(efi_glob)):
233            if efi_file.is_file():
234                signer.sign_efi_file(efi_file)
235
236        for efi_file in sorted(bootloader_dir.glob("crdyboot*.efi")):
237            # This key is required to create the detached signature.
238            ensure_file_exists(
239                keys.crdyshim_private_key, "No crdyshim private key"
240            )
241
242            if efi_file.is_file():
243                inject_vbpubk(efi_file, keys)
244                signer.create_detached_signature(efi_file)
245
246        for syslinux_kernel_file in sorted(syslinux_dir.glob("vmlinuz.?")):
247            if syslinux_kernel_file.is_file():
248                signer.sign_efi_file(syslinux_kernel_file)
249
250        kernel_file = (kernel_dir / "vmlinuz").resolve()
251        if kernel_file.is_file():
252            signer.sign_efi_file(kernel_file)
253
254
255def sign_target_file(target_file: os.PathLike, keys: Keys):
256    """Signs a single EFI file.
257
258    Args:
259        target_file: Path a file to sign.
260        keys: An instance of Keys.
261    """
262
263    # Verify all keys are present for signing.
264    check_keys(keys)
265
266    with tempfile.TemporaryDirectory() as working_dir:
267        working_dir = Path(working_dir)
268        signer = Signer(working_dir, keys)
269
270        if target_file.is_file():
271            signer.sign_efi_file(target_file)
272        else:
273            sys.exit("File not found")
274
275
276def get_parser() -> argparse.ArgumentParser:
277    """Get CLI parser."""
278    parser = argparse.ArgumentParser(description=__doc__)
279    parser.add_argument(
280        "--target-dir",
281        type=Path,
282        help="Path of a boot directory, either the root of the ESP or "
283        "/boot of the root filesystem",
284        required=False,
285    )
286    parser.add_argument(
287        "--target-file",
288        type=Path,
289        help="Path of an EFI binary file to sign",
290        required=False,
291    )
292    parser.add_argument(
293        "--private-key",
294        type=Path,
295        help="Path of the private signing key",
296        required=True,
297    )
298    parser.add_argument(
299        "--sign-cert",
300        type=Path,
301        help="Path of the signing certificate",
302        required=True,
303    )
304    parser.add_argument(
305        "--verify-cert",
306        type=Path,
307        help="Path of the verification certificate",
308        required=True,
309    )
310    parser.add_argument(
311        "--kernel-subkey-vbpubk",
312        type=Path,
313        help="Path of the kernel subkey public key",
314        required=True,
315    )
316    parser.add_argument(
317        "--crdyshim-private-key",
318        type=Path,
319        help="Path of the crdyshim private key",
320        required=True,
321    )
322    parser.add_argument(
323        "--efi-glob",
324        help="Glob pattern of EFI files to sign, e.g. '*.efi'",
325        required=False,
326    )
327    return parser
328
329
330def main(argv: Optional[List[str]] = None) -> Optional[int]:
331    """Sign UEFI binaries.
332
333    Args:
334        argv: Command-line arguments.
335    """
336    logging.basicConfig(level=logging.INFO)
337
338    parser = get_parser()
339    opts = parser.parse_args(argv)
340
341    for tool in (
342        "objcopy",
343        "sbattach",
344        "sbsign",
345        "sbverify",
346    ):
347        ensure_executable_available(tool)
348
349    keys = Keys(
350        private_key=opts.private_key,
351        sign_cert=opts.sign_cert,
352        verify_cert=opts.verify_cert,
353        kernel_subkey_vbpubk=opts.kernel_subkey_vbpubk,
354        crdyshim_private_key=opts.crdyshim_private_key,
355    )
356
357    if opts.target_dir:
358        if not opts.efi_glob:
359            sys.exit("Unable to run: specify '--efi-glob'")
360        sign_target_dir(opts.target_dir, keys, opts.efi_glob)
361    elif opts.target_file:
362        sign_target_file(opts.target_file, keys)
363    else:
364        sys.exit(
365            "Unable to run, either provide '--target-dir' or '--target-file'"
366        )
367
368
369if __name__ == "__main__":
370    sys.exit(main(sys.argv[1:]))
371