summaryrefslogtreecommitdiff
path: root/scripts/image_signing/sign_uefi.py
blob: 53ac87c6c1822ddfca408610233a073c80778180 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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:]))