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 # Only check the private keys if they are local paths rather than a 239 # PKCS#11 URI. 240 if not is_pkcs11_key_path(keys.crdyshim_private_key): 241 ensure_file_exists( 242 keys.crdyshim_private_key, "No crdyshim private key" 243 ) 244 245 if efi_file.is_file(): 246 inject_vbpubk(efi_file, keys) 247 signer.create_detached_signature(efi_file) 248 249 for syslinux_kernel_file in sorted(syslinux_dir.glob("vmlinuz.?")): 250 if syslinux_kernel_file.is_file(): 251 signer.sign_efi_file(syslinux_kernel_file) 252 253 kernel_file = (kernel_dir / "vmlinuz").resolve() 254 if kernel_file.is_file(): 255 signer.sign_efi_file(kernel_file) 256 257 258def sign_target_file(target_file: os.PathLike, keys: Keys): 259 """Signs a single EFI file. 260 261 Args: 262 target_file: Path a file to sign. 263 keys: An instance of Keys. 264 """ 265 266 # Verify all keys are present for signing. 267 check_keys(keys) 268 269 with tempfile.TemporaryDirectory() as working_dir: 270 working_dir = Path(working_dir) 271 signer = Signer(working_dir, keys) 272 273 if target_file.is_file(): 274 signer.sign_efi_file(target_file) 275 else: 276 sys.exit("File not found") 277 278 279def get_parser() -> argparse.ArgumentParser: 280 """Get CLI parser.""" 281 parser = argparse.ArgumentParser(description=__doc__) 282 parser.add_argument( 283 "--target-dir", 284 type=Path, 285 help="Path of a boot directory, either the root of the ESP or " 286 "/boot of the root filesystem", 287 required=False, 288 ) 289 parser.add_argument( 290 "--target-file", 291 type=Path, 292 help="Path of an EFI binary file to sign", 293 required=False, 294 ) 295 parser.add_argument( 296 "--private-key", 297 type=Path, 298 help="Path of the private signing key", 299 required=True, 300 ) 301 parser.add_argument( 302 "--sign-cert", 303 type=Path, 304 help="Path of the signing certificate", 305 required=True, 306 ) 307 parser.add_argument( 308 "--verify-cert", 309 type=Path, 310 help="Path of the verification certificate", 311 required=True, 312 ) 313 parser.add_argument( 314 "--kernel-subkey-vbpubk", 315 type=Path, 316 help="Path of the kernel subkey public key", 317 required=True, 318 ) 319 parser.add_argument( 320 "--crdyshim-private-key", 321 type=Path, 322 help="Path of the crdyshim private key", 323 required=True, 324 ) 325 parser.add_argument( 326 "--efi-glob", 327 help="Glob pattern of EFI files to sign, e.g. '*.efi'", 328 required=False, 329 ) 330 return parser 331 332 333def main(argv: Optional[List[str]] = None) -> Optional[int]: 334 """Sign UEFI binaries. 335 336 Args: 337 argv: Command-line arguments. 338 """ 339 logging.basicConfig(level=logging.INFO) 340 341 parser = get_parser() 342 opts = parser.parse_args(argv) 343 344 for tool in ( 345 "objcopy", 346 "sbattach", 347 "sbsign", 348 "sbverify", 349 ): 350 ensure_executable_available(tool) 351 352 keys = Keys( 353 private_key=opts.private_key, 354 sign_cert=opts.sign_cert, 355 verify_cert=opts.verify_cert, 356 kernel_subkey_vbpubk=opts.kernel_subkey_vbpubk, 357 crdyshim_private_key=opts.crdyshim_private_key, 358 ) 359 360 if opts.target_dir: 361 if not opts.efi_glob: 362 sys.exit("Unable to run: specify '--efi-glob'") 363 sign_target_dir(opts.target_dir, keys, opts.efi_glob) 364 elif opts.target_file: 365 sign_target_file(opts.target_file, keys) 366 else: 367 sys.exit( 368 "Unable to run, either provide '--target-dir' or '--target-file'" 369 ) 370 371 372if __name__ == "__main__": 373 sys.exit(main(sys.argv[1:])) 374