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