1# Copyright (c) 2018 Foundries.io 2# 3# SPDX-License-Identifier: Apache-2.0 4 5import abc 6import argparse 7import os 8import pathlib 9import pickle 10import platform 11import shutil 12import shlex 13import subprocess 14import sys 15 16from west import log 17from west import manifest 18from west.util import quote_sh_list 19 20from build_helpers import find_build_dir, is_zephyr_build, \ 21 FIND_BUILD_DIR_DESCRIPTION 22from runners.core import BuildConfiguration 23from zcmake import CMakeCache 24from zephyr_ext_common import Forceable, ZEPHYR_SCRIPTS 25 26# This is needed to load edt.pickle files. 27sys.path.insert(0, str(ZEPHYR_SCRIPTS / 'dts' / 'python-devicetree' / 'src')) 28 29SIGN_DESCRIPTION = '''\ 30This command automates some of the drudgery of creating signed Zephyr 31binaries for chain-loading by a bootloader. 32 33In the simplest usage, run this from your build directory: 34 35 west sign -t your_tool -- ARGS_FOR_YOUR_TOOL 36 37The "ARGS_FOR_YOUR_TOOL" value can be any additional arguments you want to 38pass to the tool, such as the location of a signing key etc. Depending on 39which sort of ARGS_FOR_YOUR_TOOLS you use, the `--` separator/sentinel may 40not always be required. To avoid ambiguity and having to find and 41understand POSIX 12.2 Guideline 10, always use `--`. 42 43See tool-specific help below for details.''' 44 45SIGN_EPILOG = '''\ 46imgtool 47------- 48 49To build a signed binary you can load with MCUboot using imgtool, 50run this from your build directory: 51 52 west sign -t imgtool -- --key YOUR_SIGNING_KEY.pem 53 54For this to work, either imgtool must be installed (e.g. using pip3), 55or you must pass the path to imgtool.py using the -p option. 56 57Assuming your binary was properly built for processing and handling by 58imgtool, this creates zephyr.signed.bin and zephyr.signed.hex 59files which are ready for use by your bootloader. 60 61The version number, image header size, alignment, and slot sizes are 62determined from the build directory using .config and the device tree. 63As shown above, extra arguments after a '--' are passed to imgtool 64directly. 65 66rimage 67------ 68 69To create a signed binary with the rimage tool, run this from your build 70directory: 71 72 west sign -t rimage -- -k YOUR_SIGNING_KEY.pem 73 74For this to work, either rimage must be installed or you must pass 75the path to rimage using the -p option. 76 77You can also pass additional arguments to rimage thanks to [sign] and 78[rimage] sections in your west config file(s); this is especially useful 79when invoking west sign _indirectly_ through CMake/ninja. See how at 80https://docs.zephyrproject.org/latest/develop/west/sign.html 81''' 82 83 84def config_get_words(west_config, section_key, fallback=None): 85 unparsed = west_config.get(section_key) 86 log.dbg(f'west config {section_key}={unparsed}') 87 return fallback if unparsed is None else shlex.split(unparsed) 88 89 90def config_get(west_config, section_key, fallback=None): 91 words = config_get_words(west_config, section_key) 92 if words is None: 93 return fallback 94 if len(words) != 1: 95 log.die(f'Single word expected for: {section_key}={words}. Use quotes?') 96 return words[0] 97 98 99class ToggleAction(argparse.Action): 100 101 def __call__(self, parser, args, ignored, option): 102 setattr(args, self.dest, not option.startswith('--no-')) 103 104 105class Sign(Forceable): 106 def __init__(self): 107 super(Sign, self).__init__( 108 'sign', 109 # Keep this in sync with the string in west-commands.yml. 110 'sign a Zephyr binary for bootloader chain-loading', 111 SIGN_DESCRIPTION, 112 accepts_unknown_args=False) 113 114 def do_add_parser(self, parser_adder): 115 parser = parser_adder.add_parser( 116 self.name, 117 epilog=SIGN_EPILOG, 118 help=self.help, 119 formatter_class=argparse.RawDescriptionHelpFormatter, 120 description=self.description) 121 122 parser.add_argument('-d', '--build-dir', 123 help=FIND_BUILD_DIR_DESCRIPTION) 124 parser.add_argument('-q', '--quiet', action='store_true', 125 help='suppress non-error output') 126 self.add_force_arg(parser) 127 128 # general options 129 group = parser.add_argument_group('tool control options') 130 group.add_argument('-t', '--tool', choices=['imgtool', 'rimage'], 131 help='''image signing tool name; imgtool and rimage 132 are currently supported''') 133 group.add_argument('-p', '--tool-path', default=None, 134 help='''path to the tool itself, if needed''') 135 group.add_argument('-D', '--tool-data', default=None, 136 help='''path to a tool-specific data/configuration directory, if needed''') 137 group.add_argument('--if-tool-available', action='store_true', 138 help='''Do not fail if the rimage tool is not found or the rimage signing 139schema (rimage "target") is not defined in board.cmake.''') 140 group.add_argument('tool_args', nargs='*', metavar='tool_opt', 141 help='extra option(s) to pass to the signing tool') 142 143 # bin file options 144 group = parser.add_argument_group('binary (.bin) file options') 145 group.add_argument('--bin', '--no-bin', dest='gen_bin', nargs=0, 146 action=ToggleAction, 147 help='''produce a signed .bin file? 148 (default: yes, if supported and unsigned bin 149 exists)''') 150 group.add_argument('-B', '--sbin', metavar='BIN', 151 help='''signed .bin file name 152 (default: zephyr.signed.bin in the build 153 directory, next to zephyr.bin)''') 154 155 # hex file options 156 group = parser.add_argument_group('Intel HEX (.hex) file options') 157 group.add_argument('--hex', '--no-hex', dest='gen_hex', nargs=0, 158 action=ToggleAction, 159 help='''produce a signed .hex file? 160 (default: yes, if supported and unsigned hex 161 exists)''') 162 group.add_argument('-H', '--shex', metavar='HEX', 163 help='''signed .hex file name 164 (default: zephyr.signed.hex in the build 165 directory, next to zephyr.hex)''') 166 167 return parser 168 169 def do_run(self, args, ignored): 170 self.args = args # for check_force 171 172 # Find the build directory and parse .config and DT. 173 build_dir = find_build_dir(args.build_dir) 174 self.check_force(os.path.isdir(build_dir), 175 'no such build directory {}'.format(build_dir)) 176 self.check_force(is_zephyr_build(build_dir), 177 "build directory {} doesn't look like a Zephyr build " 178 'directory'.format(build_dir)) 179 build_conf = BuildConfiguration(build_dir) 180 181 if not args.tool: 182 args.tool = config_get(self.config, 'sign.tool') 183 184 # Decide on output formats. 185 formats = [] 186 bin_exists = build_conf.getboolean('CONFIG_BUILD_OUTPUT_BIN') 187 if args.gen_bin: 188 self.check_force(bin_exists, 189 '--bin given but CONFIG_BUILD_OUTPUT_BIN not set ' 190 "in build directory's ({}) .config". 191 format(build_dir)) 192 formats.append('bin') 193 elif args.gen_bin is None and bin_exists: 194 formats.append('bin') 195 196 hex_exists = build_conf.getboolean('CONFIG_BUILD_OUTPUT_HEX') 197 if args.gen_hex: 198 self.check_force(hex_exists, 199 '--hex given but CONFIG_BUILD_OUTPUT_HEX not set ' 200 "in build directory's ({}) .config". 201 format(build_dir)) 202 formats.append('hex') 203 elif args.gen_hex is None and hex_exists: 204 formats.append('hex') 205 206 # Delegate to the signer. 207 if args.tool == 'imgtool': 208 if args.if_tool_available: 209 log.die('imgtool does not support --if-tool-available') 210 signer = ImgtoolSigner() 211 elif args.tool == 'rimage': 212 signer = RimageSigner() 213 # (Add support for other signers here in elif blocks) 214 else: 215 if args.tool is None: 216 log.die('one --tool is required') 217 else: 218 log.die(f'invalid tool: {args.tool}') 219 220 signer.sign(self, build_dir, build_conf, formats) 221 222 223class Signer(abc.ABC): 224 '''Common abstract superclass for signers. 225 226 To add support for a new tool, subclass this and add support for 227 it in the Sign.do_run() method.''' 228 229 @abc.abstractmethod 230 def sign(self, command, build_dir, build_conf, formats): 231 '''Abstract method to perform a signature; subclasses must implement. 232 233 :param command: the Sign instance 234 :param build_dir: the build directory 235 :param build_conf: BuildConfiguration for build directory 236 :param formats: list of formats to generate ('bin', 'hex') 237 ''' 238 239 240class ImgtoolSigner(Signer): 241 242 def sign(self, command, build_dir, build_conf, formats): 243 if not formats: 244 return 245 246 args = command.args 247 b = pathlib.Path(build_dir) 248 249 imgtool = self.find_imgtool(command, args) 250 # The vector table offset and application version are set in Kconfig: 251 appver = self.get_cfg(command, build_conf, 'CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION') 252 vtoff = self.get_cfg(command, build_conf, 'CONFIG_ROM_START_OFFSET') 253 # Flash device write alignment and the partition's slot size 254 # come from devicetree: 255 flash = self.edt_flash_node(b, args.quiet) 256 align, addr, size = self.edt_flash_params(flash) 257 258 if not build_conf.getboolean('CONFIG_BOOTLOADER_MCUBOOT'): 259 log.wrn("CONFIG_BOOTLOADER_MCUBOOT is not set to y in " 260 f"{build_conf.path}; this probably won't work") 261 262 kernel = build_conf.get('CONFIG_KERNEL_BIN_NAME', 'zephyr') 263 264 if 'bin' in formats: 265 in_bin = b / 'zephyr' / f'{kernel}.bin' 266 if not in_bin.is_file(): 267 log.die(f"no unsigned .bin found at {in_bin}") 268 in_bin = os.fspath(in_bin) 269 else: 270 in_bin = None 271 if 'hex' in formats: 272 in_hex = b / 'zephyr' / f'{kernel}.hex' 273 if not in_hex.is_file(): 274 log.die(f"no unsigned .hex found at {in_hex}") 275 in_hex = os.fspath(in_hex) 276 else: 277 in_hex = None 278 279 if not args.quiet: 280 log.banner('image configuration:') 281 log.inf('partition offset: {0} (0x{0:x})'.format(addr)) 282 log.inf('partition size: {0} (0x{0:x})'.format(size)) 283 log.inf('rom start offset: {0} (0x{0:x})'.format(vtoff)) 284 285 # Base sign command. 286 sign_base = imgtool + ['sign', 287 '--version', str(appver), 288 '--align', str(align), 289 '--header-size', str(vtoff), 290 '--slot-size', str(size)] 291 sign_base.extend(args.tool_args) 292 293 if not args.quiet: 294 log.banner('signing binaries') 295 if in_bin: 296 out_bin = args.sbin or str(b / 'zephyr' / 'zephyr.signed.bin') 297 sign_bin = sign_base + [in_bin, out_bin] 298 if not args.quiet: 299 log.inf(f'unsigned bin: {in_bin}') 300 log.inf(f'signed bin: {out_bin}') 301 log.dbg(quote_sh_list(sign_bin)) 302 subprocess.check_call(sign_bin, stdout=subprocess.PIPE if args.quiet else None) 303 if in_hex: 304 out_hex = args.shex or str(b / 'zephyr' / 'zephyr.signed.hex') 305 sign_hex = sign_base + [in_hex, out_hex] 306 if not args.quiet: 307 log.inf(f'unsigned hex: {in_hex}') 308 log.inf(f'signed hex: {out_hex}') 309 log.dbg(quote_sh_list(sign_hex)) 310 subprocess.check_call(sign_hex, stdout=subprocess.PIPE if args.quiet else None) 311 312 @staticmethod 313 def find_imgtool(command, args): 314 if args.tool_path: 315 imgtool = args.tool_path 316 if not os.path.isfile(imgtool): 317 log.die(f'--tool-path {imgtool}: no such file') 318 else: 319 imgtool = shutil.which('imgtool') or shutil.which('imgtool.py') 320 if not imgtool: 321 log.die('imgtool not found; either install it', 322 '(e.g. "pip3 install imgtool") or provide --tool-path') 323 324 if platform.system() == 'Windows' and imgtool.endswith('.py'): 325 # Windows users may not be able to run .py files 326 # as executables in subprocesses, regardless of 327 # what the mode says. Always run imgtool as 328 # 'python path/to/imgtool.py' instead of 329 # 'path/to/imgtool.py' in these cases. 330 # https://github.com/zephyrproject-rtos/zephyr/issues/31876 331 return [sys.executable, imgtool] 332 333 return [imgtool] 334 335 @staticmethod 336 def get_cfg(command, build_conf, item): 337 try: 338 return build_conf[item] 339 except KeyError: 340 command.check_force( 341 False, "build .config is missing a {} value".format(item)) 342 return None 343 344 @staticmethod 345 def edt_flash_node(b, quiet=False): 346 # Get the EDT Node corresponding to the zephyr,flash chosen DT 347 # node; 'b' is the build directory as a pathlib object. 348 349 # Ensure the build directory has a compiled DTS file 350 # where we expect it to be. 351 dts = b / 'zephyr' / 'zephyr.dts' 352 if not quiet: 353 log.dbg('DTS file:', dts, level=log.VERBOSE_VERY) 354 edt_pickle = b / 'zephyr' / 'edt.pickle' 355 if not edt_pickle.is_file(): 356 log.die("can't load devicetree; expected to find:", edt_pickle) 357 358 # Load the devicetree. 359 with open(edt_pickle, 'rb') as f: 360 edt = pickle.load(f) 361 362 # By convention, the zephyr,flash chosen node contains the 363 # partition information about the zephyr image to sign. 364 flash = edt.chosen_node('zephyr,flash') 365 if not flash: 366 log.die('devicetree has no chosen zephyr,flash node;', 367 "can't infer flash write block or slot0_partition slot sizes") 368 369 return flash 370 371 @staticmethod 372 def edt_flash_params(flash): 373 # Get the flash device's write alignment and offset from the 374 # slot0_partition and the size from slot1_partition , out of the 375 # build directory's devicetree. slot1_partition size is used, 376 # when available, because in swap-move mode it can be one sector 377 # smaller. When not available, fallback to slot0_partition (single slot dfu). 378 379 # The node must have a "partitions" child node, which in turn 380 # must have child nodes with label slot0_partition and may have a child node 381 # with label slot1_partition. By convention, the slots for consumption by 382 # imgtool are linked into these partitions. 383 if 'partitions' not in flash.children: 384 log.die("DT zephyr,flash chosen node has no partitions,", 385 "can't find partitions for MCUboot slots") 386 387 partitions = flash.children['partitions'] 388 slots = { 389 label: node for node in partitions.children.values() 390 for label in node.labels 391 if label in set(['slot0_partition', 'slot1_partition']) 392 } 393 394 if 'slot0_partition' not in slots: 395 log.die("DT zephyr,flash chosen node has no slot0_partition partition,", 396 "can't determine its address") 397 398 # Die on missing or zero alignment or slot_size. 399 if "write-block-size" not in flash.props: 400 log.die('DT zephyr,flash node has no write-block-size;', 401 "can't determine imgtool write alignment") 402 align = flash.props['write-block-size'].val 403 if align == 0: 404 log.die('expected nonzero flash alignment, but got ' 405 'DT flash device write-block-size {}'.format(align)) 406 407 # The partitions node, and its subnode, must provide 408 # the size of slot1_partition or slot0_partition partition via the regs property. 409 slot_key = 'slot1_partition' if 'slot1_partition' in slots else 'slot0_partition' 410 if not slots[slot_key].regs: 411 log.die(f'{slot_key} flash partition has no regs property;', 412 "can't determine size of slot") 413 414 # always use addr of slot0_partition, which is where slots are run 415 addr = slots['slot0_partition'].regs[0].addr 416 417 size = slots[slot_key].regs[0].size 418 if size == 0: 419 log.die('expected nonzero slot size for {}'.format(slot_key)) 420 421 return (align, addr, size) 422 423class RimageSigner(Signer): 424 425 def rimage_config_dir(self): 426 'Returns the rimage/config/ directory with the highest precedence' 427 args = self.command.args 428 if args.tool_data: 429 conf_dir = pathlib.Path(args.tool_data) 430 elif self.cmake_cache.get('RIMAGE_CONFIG_PATH'): 431 conf_dir = pathlib.Path(self.cmake_cache['RIMAGE_CONFIG_PATH']) 432 else: 433 conf_dir = self.sof_src_dir / 'tools' / 'rimage' / 'config' 434 self.command.dbg(f'rimage config directory={conf_dir}') 435 return conf_dir 436 437 def preprocess_toml(self, config_dir, toml_basename, subdir): 438 'Runs the C pre-processor on config_dir/toml_basename.h' 439 440 compiler_path = self.cmake_cache.get("CMAKE_C_COMPILER") 441 preproc_cmd = [compiler_path, '-E', str(config_dir / (toml_basename + '.h'))] 442 # -P removes line markers to keep the .toml output reproducible. To 443 # trace #includes, temporarily comment out '-P' (-f*-prefix-map 444 # unfortunately don't seem to make any difference here and they're 445 # gcc-specific) 446 preproc_cmd += ['-P'] 447 448 # "REM" escapes _leading_ '#' characters from cpp and allows 449 # such comments to be preserved in generated/*.toml files: 450 # 451 # REM # my comment... 452 # 453 # Note _trailing_ '#' characters and comments are ignored by cpp 454 # and don't need any REM trick. 455 preproc_cmd += ['-DREM='] 456 457 preproc_cmd += ['-I', str(self.sof_src_dir / 'src')] 458 preproc_cmd += ['-imacros', 459 str(pathlib.Path('zephyr') / 'include' / 'generated' / 'zephyr' / 'autoconf.h')] 460 preproc_cmd += ['-o', str(subdir / 'rimage_config.toml')] 461 self.command.inf(quote_sh_list(preproc_cmd)) 462 subprocess.run(preproc_cmd, check=True, cwd=self.build_dir) 463 464 def sign(self, command, build_dir, build_conf, formats): 465 self.command = command 466 args = command.args 467 468 b = pathlib.Path(build_dir) 469 self.build_dir = b 470 cache = CMakeCache.from_build_dir(build_dir) 471 self.cmake_cache = cache 472 473 # Warning: RIMAGE_TARGET in Zephyr is a duplicate of 474 # CONFIG_RIMAGE_SIGNING_SCHEMA in SOF. 475 target = cache.get('RIMAGE_TARGET') 476 477 if not target: 478 msg = 'rimage target not defined in board.cmake' 479 if args.if_tool_available: 480 log.inf(msg) 481 sys.exit(0) 482 else: 483 log.die(msg) 484 485 kernel_name = build_conf.get('CONFIG_KERNEL_BIN_NAME', 'zephyr') 486 487 # TODO: make this a new sign.py --bootloader option. 488 if target in ('imx8', 'imx8m', 'imx8ulp'): 489 bootloader = None 490 kernel = str(b / 'zephyr' / f'{kernel_name}.elf') 491 out_bin = str(b / 'zephyr' / f'{kernel_name}.ri') 492 out_xman = str(b / 'zephyr' / f'{kernel_name}.ri.xman') 493 out_tmp = str(b / 'zephyr' / f'{kernel_name}.rix') 494 else: 495 bootloader = str(b / 'zephyr' / 'boot.mod') 496 kernel = str(b / 'zephyr' / 'main.mod') 497 out_bin = str(b / 'zephyr' / f'{kernel_name}.ri') 498 out_xman = str(b / 'zephyr' / f'{kernel_name}.ri.xman') 499 out_tmp = str(b / 'zephyr' / f'{kernel_name}.rix') 500 501 # Clean any stale output. This is especially important when using --if-tool-available 502 # (but not just) 503 for o in [ out_bin, out_xman, out_tmp ]: 504 pathlib.Path(o).unlink(missing_ok=True) 505 506 tool_path = ( 507 args.tool_path if args.tool_path else 508 config_get(command.config, 'rimage.path', None) 509 ) 510 err_prefix = '--tool-path' if args.tool_path else 'west config' 511 512 if tool_path: 513 command.check_force(shutil.which(tool_path), 514 f'{err_prefix} {tool_path}: not an executable') 515 else: 516 tool_path = shutil.which('rimage') 517 if not tool_path: 518 err_msg = 'rimage not found; either install it or provide --tool-path' 519 if args.if_tool_available: 520 log.wrn(err_msg) 521 log.wrn('zephyr binary _not_ signed!') 522 return 523 else: 524 log.die(err_msg) 525 526 #### -c sof/rimage/config/signing_schema.toml #### 527 528 if not args.quiet: 529 log.inf('Signing with tool {}'.format(tool_path)) 530 531 try: 532 sof_proj = command.manifest.get_projects(['sof'], allow_paths=False) 533 sof_src_dir = pathlib.Path(sof_proj[0].abspath) 534 except ValueError: # sof is the manifest 535 sof_src_dir = pathlib.Path(manifest.manifest_path()).parent 536 537 self.sof_src_dir = sof_src_dir 538 539 540 log.inf('Signing for SOC target ' + target) 541 542 # FIXME: deprecate --no-manifest and replace it with a much 543 # simpler and more direct `-- -e` which the user can _already_ 544 # pass today! With unclear consequences right now... 545 if '--no-manifest' in args.tool_args: 546 no_manifest = True 547 args.tool_args.remove('--no-manifest') 548 else: 549 no_manifest = False 550 551 # Non-SOF build does not have extended manifest data for 552 # rimage to process, which might result in rimage error. 553 # So skip it when not doing SOF builds. 554 is_sof_build = build_conf.getboolean('CONFIG_SOF') 555 if not is_sof_build: 556 no_manifest = True 557 558 if no_manifest: 559 extra_ri_args = [ ] 560 else: 561 extra_ri_args = ['-e'] 562 563 sign_base = [tool_path] 564 565 # Align rimage verbosity. 566 # Sub-command arg 'west sign -q' takes precedence over west '-v' 567 if not args.quiet and args.verbose: 568 sign_base += ['-v'] * args.verbose 569 570 components = [ ] if bootloader is None else [ bootloader ] 571 components += [ kernel ] 572 573 sign_config_extra_args = config_get_words(command.config, 'rimage.extra-args', []) 574 575 if '-k' not in sign_config_extra_args + args.tool_args: 576 # rimage requires a key argument even when it does not sign 577 cmake_default_key = cache.get('RIMAGE_SIGN_KEY', 'key placeholder from sign.py') 578 extra_ri_args += [ '-k', str(sof_src_dir / 'keys' / cmake_default_key) ] 579 580 if args.tool_data and '-c' in args.tool_args: 581 log.wrn('--tool-data ' + args.tool_data + ' ignored! Overridden by: -- -c ... ') 582 583 if '-c' not in sign_config_extra_args + args.tool_args: 584 conf_dir = self.rimage_config_dir() 585 toml_basename = target + '.toml' 586 if ((conf_dir / toml_basename).exists() and 587 (conf_dir / (toml_basename + '.h')).exists()): 588 command.die(f"Cannot have both {toml_basename + '.h'} and {toml_basename} in {conf_dir}") 589 590 if (conf_dir / (toml_basename + '.h')).exists(): 591 generated_subdir = pathlib.Path('zephyr') / 'misc' / 'generated' 592 self.preprocess_toml(conf_dir, toml_basename, generated_subdir) 593 extra_ri_args += ['-c', str(b / generated_subdir / 'rimage_config.toml')] 594 else: 595 toml_dir = conf_dir 596 extra_ri_args += ['-c', str(toml_dir / toml_basename)] 597 598 # Warning: while not officially supported (yet?), the rimage --option that is last 599 # on the command line currently wins in case of duplicate options. So pay 600 # attention to the _args order below. 601 sign_base += (['-o', out_bin] + sign_config_extra_args + 602 extra_ri_args + args.tool_args + components) 603 604 command.inf(quote_sh_list(sign_base)) 605 subprocess.check_call(sign_base) 606 607 if no_manifest: 608 filenames = [out_bin] 609 else: 610 filenames = [out_xman, out_bin] 611 if not args.quiet: 612 log.inf('Prefixing ' + out_bin + ' with manifest ' + out_xman) 613 with open(out_tmp, 'wb') as outfile: 614 for fname in filenames: 615 with open(fname, 'rb') as infile: 616 outfile.write(infile.read()) 617 618 os.remove(out_bin) 619 os.rename(out_tmp, out_bin) 620