1#! /usr/bin/env python3 2# 3# Copyright 2017-2020 Linaro Limited 4# Copyright 2019-2023 Arm Limited 5# 6# SPDX-License-Identifier: Apache-2.0 7# 8# Licensed under the Apache License, Version 2.0 (the "License"); 9# you may not use this file except in compliance with the License. 10# You may obtain a copy of the License at 11# 12# http://www.apache.org/licenses/LICENSE-2.0 13# 14# Unless required by applicable law or agreed to in writing, software 15# distributed under the License is distributed on an "AS IS" BASIS, 16# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17# See the License for the specific language governing permissions and 18# limitations under the License. 19 20import re 21import click 22import getpass 23import imgtool.keys as keys 24import sys 25import struct 26import os 27import lzma 28import hashlib 29import base64 30from imgtool import image, imgtool_version 31from imgtool.version import decode_version 32from imgtool.dumpinfo import dump_imginfo 33from .keys import ( 34 RSAUsageError, ECDSAUsageError, Ed25519UsageError, X25519UsageError) 35 36comp_default_dictsize=131072 37comp_default_pb=2 38comp_default_lc=3 39comp_default_lp=1 40comp_default_preset=9 41 42 43MIN_PYTHON_VERSION = (3, 6) 44if sys.version_info < MIN_PYTHON_VERSION: 45 sys.exit("Python %s.%s or newer is required by imgtool." 46 % MIN_PYTHON_VERSION) 47 48 49def gen_rsa2048(keyfile, passwd): 50 keys.RSA.generate().export_private(path=keyfile, passwd=passwd) 51 52 53def gen_rsa3072(keyfile, passwd): 54 keys.RSA.generate(key_size=3072).export_private(path=keyfile, 55 passwd=passwd) 56 57 58def gen_ecdsa_p256(keyfile, passwd): 59 keys.ECDSA256P1.generate().export_private(keyfile, passwd=passwd) 60 61 62def gen_ecdsa_p384(keyfile, passwd): 63 keys.ECDSA384P1.generate().export_private(keyfile, passwd=passwd) 64 65 66def gen_ed25519(keyfile, passwd): 67 keys.Ed25519.generate().export_private(path=keyfile, passwd=passwd) 68 69 70def gen_x25519(keyfile, passwd): 71 keys.X25519.generate().export_private(path=keyfile, passwd=passwd) 72 73 74valid_langs = ['c', 'rust'] 75valid_hash_encodings = ['lang-c', 'raw'] 76valid_encodings = ['lang-c', 'lang-rust', 'pem', 'raw'] 77keygens = { 78 'rsa-2048': gen_rsa2048, 79 'rsa-3072': gen_rsa3072, 80 'ecdsa-p256': gen_ecdsa_p256, 81 'ecdsa-p384': gen_ecdsa_p384, 82 'ed25519': gen_ed25519, 83 'x25519': gen_x25519, 84} 85valid_formats = ['openssl', 'pkcs8'] 86valid_sha = [ 'auto', '256', '384', '512' ] 87 88 89def load_signature(sigfile): 90 with open(sigfile, 'rb') as f: 91 signature = base64.b64decode(f.read()) 92 return signature 93 94 95def save_signature(sigfile, sig): 96 with open(sigfile, 'wb') as f: 97 signature = base64.b64encode(sig) 98 f.write(signature) 99 100 101def load_key(keyfile): 102 # TODO: better handling of invalid pass-phrase 103 key = keys.load(keyfile) 104 if key is not None: 105 return key 106 passwd = getpass.getpass("Enter key passphrase: ").encode('utf-8') 107 return keys.load(keyfile, passwd) 108 109 110def get_password(): 111 while True: 112 passwd = getpass.getpass("Enter key passphrase: ") 113 passwd2 = getpass.getpass("Reenter passphrase: ") 114 if passwd == passwd2: 115 break 116 print("Passwords do not match, try again") 117 118 # Password must be bytes, always use UTF-8 for consistent 119 # encoding. 120 return passwd.encode('utf-8') 121 122 123@click.option('-p', '--password', is_flag=True, 124 help='Prompt for password to protect key') 125@click.option('-t', '--type', metavar='type', required=True, 126 type=click.Choice(keygens.keys()), prompt=True, 127 help='{}'.format('One of: {}'.format(', '.join(keygens.keys())))) 128@click.option('-k', '--key', metavar='filename', required=True) 129@click.command(help='Generate pub/private keypair') 130def keygen(type, key, password): 131 password = get_password() if password else None 132 keygens[type](key, password) 133 134 135@click.option('-l', '--lang', metavar='lang', 136 type=click.Choice(valid_langs), 137 help='This option is deprecated. Please use the ' 138 '`--encoding` option. ' 139 'Valid langs: {}'.format(', '.join(valid_langs))) 140@click.option('-e', '--encoding', metavar='encoding', 141 type=click.Choice(valid_encodings), 142 help='Valid encodings: {}'.format(', '.join(valid_encodings))) 143@click.option('-k', '--key', metavar='filename', required=True) 144@click.option('-o', '--output', metavar='output', required=False, 145 help='Specify the output file\'s name. \ 146 The stdout is used if it is not provided.') 147@click.command(help='Dump public key from keypair') 148def getpub(key, encoding, lang, output): 149 if encoding and lang: 150 raise click.UsageError('Please use only one of `--encoding/-e` ' 151 'or `--lang/-l`') 152 elif not encoding and not lang: 153 # Preserve old behavior defaulting to `c`. If `lang` is removed, 154 # `default=valid_encodings[0]` should be added to `-e` param. 155 lang = valid_langs[0] 156 key = load_key(key) 157 158 if not output: 159 output = sys.stdout 160 if key is None: 161 print("Invalid passphrase") 162 elif lang == 'c' or encoding == 'lang-c': 163 key.emit_c_public(file=output) 164 elif lang == 'rust' or encoding == 'lang-rust': 165 key.emit_rust_public(file=output) 166 elif encoding == 'pem': 167 key.emit_public_pem(file=output) 168 elif encoding == 'raw': 169 key.emit_raw_public(file=output) 170 else: 171 raise click.UsageError() 172 173 174@click.option('-e', '--encoding', metavar='encoding', 175 type=click.Choice(valid_hash_encodings), 176 help='Valid encodings: {}. ' 177 'Default value is {}.' 178 .format(', '.join(valid_hash_encodings), 179 valid_hash_encodings[0])) 180@click.option('-k', '--key', metavar='filename', required=True) 181@click.option('-o', '--output', metavar='output', required=False, 182 help='Specify the output file\'s name. \ 183 The stdout is used if it is not provided.') 184@click.command(help='Dump the SHA256 hash of the public key') 185def getpubhash(key, output, encoding): 186 if not encoding: 187 encoding = valid_hash_encodings[0] 188 key = load_key(key) 189 190 if not output: 191 output = sys.stdout 192 if key is None: 193 print("Invalid passphrase") 194 elif encoding == 'lang-c': 195 key.emit_c_public_hash(file=output) 196 elif encoding == 'raw': 197 key.emit_raw_public_hash(file=output) 198 else: 199 raise click.UsageError() 200 201 202@click.option('--minimal', default=False, is_flag=True, 203 help='Reduce the size of the dumped private key to include only ' 204 'the minimum amount of data required to decrypt. This ' 205 'might require changes to the build config. Check the docs!' 206 ) 207@click.option('-k', '--key', metavar='filename', required=True) 208@click.option('-f', '--format', 209 type=click.Choice(valid_formats), 210 help='Valid formats: {}'.format(', '.join(valid_formats)) 211 ) 212@click.command(help='Dump private key from keypair') 213def getpriv(key, minimal, format): 214 key = load_key(key) 215 if key is None: 216 print("Invalid passphrase") 217 try: 218 key.emit_private(minimal, format) 219 except (RSAUsageError, ECDSAUsageError, Ed25519UsageError, 220 X25519UsageError) as e: 221 raise click.UsageError(e) 222 223 224@click.argument('imgfile') 225@click.option('-k', '--key', metavar='filename') 226@click.command(help="Check that signed image can be verified by given key") 227def verify(key, imgfile): 228 key = load_key(key) if key else None 229 ret, version, digest = image.Image.verify(imgfile, key) 230 if ret == image.VerifyResult.OK: 231 print("Image was correctly validated") 232 print("Image version: {}.{}.{}+{}".format(*version)) 233 print("Image digest: {}".format(digest.hex())) 234 return 235 elif ret == image.VerifyResult.INVALID_MAGIC: 236 print("Invalid image magic; is this an MCUboot image?") 237 elif ret == image.VerifyResult.INVALID_TLV_INFO_MAGIC: 238 print("Invalid TLV info magic; is this an MCUboot image?") 239 elif ret == image.VerifyResult.INVALID_HASH: 240 print("Image has an invalid hash") 241 elif ret == image.VerifyResult.INVALID_SIGNATURE: 242 print("No signature found for the given key") 243 elif ret == image.VerifyResult.KEY_MISMATCH: 244 print("Key type does not match TLV record") 245 else: 246 print("Unknown return code: {}".format(ret)) 247 sys.exit(1) 248 249 250@click.argument('imgfile') 251@click.option('-o', '--outfile', metavar='filename', required=False, 252 help='Save image information to outfile in YAML format') 253@click.option('-s', '--silent', default=False, is_flag=True, 254 help='Do not print image information to output') 255@click.command(help='Print header, TLV area and trailer information ' 256 'of a signed image') 257def dumpinfo(imgfile, outfile, silent): 258 dump_imginfo(imgfile, outfile, silent) 259 print("dumpinfo has run successfully") 260 261 262def validate_version(ctx, param, value): 263 try: 264 decode_version(value) 265 return value 266 except ValueError as e: 267 raise click.BadParameter("{}".format(e)) 268 269 270def validate_security_counter(ctx, param, value): 271 if value is not None: 272 if value.lower() == 'auto': 273 return 'auto' 274 else: 275 try: 276 return int(value, 0) 277 except ValueError: 278 raise click.BadParameter( 279 "{} is not a valid integer. Please use code literals " 280 "prefixed with 0b/0B, 0o/0O, or 0x/0X as necessary." 281 .format(value)) 282 283 284def validate_header_size(ctx, param, value): 285 min_hdr_size = image.IMAGE_HEADER_SIZE 286 if value < min_hdr_size: 287 raise click.BadParameter( 288 "Minimum value for -H/--header-size is {}".format(min_hdr_size)) 289 return value 290 291 292def get_dependencies(ctx, param, value): 293 if value is not None: 294 versions = [] 295 images = re.findall(r"\((\d+)", value) 296 if len(images) == 0: 297 raise click.BadParameter( 298 "Image dependency format is invalid: {}".format(value)) 299 raw_versions = re.findall(r",\s*([0-9.+]+)\)", value) 300 if len(images) != len(raw_versions): 301 raise click.BadParameter( 302 '''There's a mismatch between the number of dependency images 303 and versions in: {}'''.format(value)) 304 for raw_version in raw_versions: 305 try: 306 versions.append(decode_version(raw_version)) 307 except ValueError as e: 308 raise click.BadParameter("{}".format(e)) 309 dependencies = dict() 310 dependencies[image.DEP_IMAGES_KEY] = images 311 dependencies[image.DEP_VERSIONS_KEY] = versions 312 return dependencies 313 314def create_lzma2_header(dictsize, pb, lc, lp): 315 header = bytearray() 316 for i in range(0, 40): 317 if dictsize <= ((2 | ((i) & 1)) << int((i) / 2 + 11)): 318 header.append(i) 319 break 320 header.append( ( pb * 5 + lp) * 9 + lc) 321 return header 322 323class BasedIntParamType(click.ParamType): 324 name = 'integer' 325 326 def convert(self, value, param, ctx): 327 try: 328 return int(value, 0) 329 except ValueError: 330 self.fail('%s is not a valid integer. Please use code literals ' 331 'prefixed with 0b/0B, 0o/0O, or 0x/0X as necessary.' 332 % value, param, ctx) 333 334 335@click.argument('outfile') 336@click.argument('infile') 337@click.option('--non-bootable', default=False, is_flag=True, 338 help='Mark the image as non-bootable.') 339@click.option('--custom-tlv', required=False, nargs=2, default=[], 340 multiple=True, metavar='[tag] [value]', 341 help='Custom TLV that will be placed into protected area. ' 342 'Add "0x" prefix if the value should be interpreted as an ' 343 'integer, otherwise it will be interpreted as a string. ' 344 'Specify the option multiple times to add multiple TLVs.') 345@click.option('-R', '--erased-val', type=click.Choice(['0', '0xff']), 346 required=False, 347 help='The value that is read back from erased flash.') 348@click.option('-x', '--hex-addr', type=BasedIntParamType(), required=False, 349 help='Adjust address in hex output file.') 350@click.option('-L', '--load-addr', type=BasedIntParamType(), required=False, 351 help='Load address for image when it should run from RAM.') 352@click.option('-F', '--rom-fixed', type=BasedIntParamType(), required=False, 353 help='Set flash address the image is built for.') 354@click.option('--save-enctlv', default=False, is_flag=True, 355 help='When upgrading, save encrypted key TLVs instead of plain ' 356 'keys. Enable when BOOT_SWAP_SAVE_ENCTLV config option ' 357 'was set.') 358@click.option('-E', '--encrypt', metavar='filename', 359 help='Encrypt image using the provided public key. ' 360 '(Not supported in direct-xip or ram-load mode.)') 361@click.option('--encrypt-keylen', default='128', 362 type=click.Choice(['128', '256']), 363 help='When encrypting the image using AES, select a 128 bit or ' 364 '256 bit key len.') 365@click.option('--compression', default='disabled', 366 type=click.Choice(['disabled', 'lzma2', 'lzma2armthumb']), 367 help='Enable image compression using specified type. ' 368 'Will fall back without image compression automatically ' 369 'if the compression increases the image size.') 370@click.option('-c', '--clear', required=False, is_flag=True, default=False, 371 help='Output a non-encrypted image with encryption capabilities,' 372 'so it can be installed in the primary slot, and encrypted ' 373 'when swapped to the secondary.') 374@click.option('-e', '--endian', type=click.Choice(['little', 'big']), 375 default='little', help="Select little or big endian") 376@click.option('--overwrite-only', default=False, is_flag=True, 377 help='Use overwrite-only instead of swap upgrades') 378@click.option('--boot-record', metavar='sw_type', help='Create CBOR encoded ' 379 'boot record TLV. The sw_type represents the role of the ' 380 'software component (e.g. CoFM for coprocessor firmware). ' 381 '[max. 12 characters]') 382@click.option('-M', '--max-sectors', type=int, 383 help='When padding allow for this amount of sectors (defaults ' 384 'to 128)') 385@click.option('--confirm', default=False, is_flag=True, 386 help='When padding the image, mark it as confirmed (implies ' 387 '--pad)') 388@click.option('--pad', default=False, is_flag=True, 389 help='Pad image to --slot-size bytes, adding trailer magic') 390@click.option('-S', '--slot-size', type=BasedIntParamType(), required=True, 391 help='Size of the slot. If the slots have different sizes, use ' 392 'the size of the secondary slot.') 393@click.option('--pad-header', default=False, is_flag=True, 394 help='Add --header-size zeroed bytes at the beginning of the ' 395 'image') 396@click.option('-H', '--header-size', callback=validate_header_size, 397 type=BasedIntParamType(), required=True) 398@click.option('--pad-sig', default=False, is_flag=True, 399 help='Add 0-2 bytes of padding to ECDSA signature ' 400 '(for mcuboot <1.5)') 401@click.option('-d', '--dependencies', callback=get_dependencies, 402 required=False, help='''Add dependence on another image, format: 403 "(<image_ID>,<image_version>), ... "''') 404@click.option('-s', '--security-counter', callback=validate_security_counter, 405 help='Specify the value of security counter. Use the `auto` ' 406 'keyword to automatically generate it from the image version.') 407@click.option('-v', '--version', callback=validate_version, required=True) 408@click.option('--align', type=click.Choice(['1', '2', '4', '8', '16', '32']), 409 default='1', 410 required=False, 411 help='Alignment used by swap update modes.') 412@click.option('--max-align', type=click.Choice(['8', '16', '32']), 413 required=False, 414 help='Maximum flash alignment. Set if flash alignment of the ' 415 'primary and secondary slot differ and any of them is larger ' 416 'than 8.') 417@click.option('--public-key-format', type=click.Choice(['hash', 'full']), 418 default='hash', help='In what format to add the public key to ' 419 'the image manifest: full key or hash of the key.') 420@click.option('-k', '--key', metavar='filename') 421@click.option('--fix-sig', metavar='filename', 422 help='fixed signature for the image. It will be used instead of ' 423 'the signature calculated using the public key') 424@click.option('--fix-sig-pubkey', metavar='filename', 425 help='public key relevant to fixed signature') 426@click.option('--sig-out', metavar='filename', 427 help='Path to the file to which signature will be written. ' 428 'The image signature will be encoded as base64 formatted string') 429@click.option('--sha', 'user_sha', type=click.Choice(valid_sha), default='auto', 430 help='selected sha algorithm to use; defaults to "auto" which is 256 if ' 431 'no cryptographic signature is used, or default for signature type') 432@click.option('--vector-to-sign', type=click.Choice(['payload', 'digest']), 433 help='send to OUTFILE the payload or payload''s digest instead ' 434 'of complied image. These data can be used for external image ' 435 'signing') 436@click.command(help='''Create a signed or unsigned image\n 437 INFILE and OUTFILE are parsed as Intel HEX if the params have 438 .hex extension, otherwise binary format is used''') 439def sign(key, public_key_format, align, version, pad_sig, header_size, 440 pad_header, slot_size, pad, confirm, max_sectors, overwrite_only, 441 endian, encrypt_keylen, encrypt, compression, infile, outfile, 442 dependencies, load_addr, hex_addr, erased_val, save_enctlv, 443 security_counter, boot_record, custom_tlv, rom_fixed, max_align, 444 clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, vector_to_sign, 445 non_bootable): 446 447 if confirm: 448 # Confirmed but non-padded images don't make much sense, because 449 # otherwise there's no trailer area for writing the confirmed status. 450 pad = True 451 img = image.Image(version=decode_version(version), header_size=header_size, 452 pad_header=pad_header, pad=pad, confirm=confirm, 453 align=int(align), slot_size=slot_size, 454 max_sectors=max_sectors, overwrite_only=overwrite_only, 455 endian=endian, load_addr=load_addr, rom_fixed=rom_fixed, 456 erased_val=erased_val, save_enctlv=save_enctlv, 457 security_counter=security_counter, max_align=max_align, 458 non_bootable=non_bootable) 459 compression_tlvs = {} 460 img.load(infile) 461 key = load_key(key) if key else None 462 enckey = load_key(encrypt) if encrypt else None 463 if enckey and key: 464 if ((isinstance(key, keys.ECDSA256P1) and 465 not isinstance(enckey, keys.ECDSA256P1Public)) 466 or (isinstance(key, keys.ECDSA384P1) and 467 not isinstance(enckey, keys.ECDSA384P1Public)) 468 or (isinstance(key, keys.RSA) and 469 not isinstance(enckey, keys.RSAPublic))): 470 # FIXME 471 raise click.UsageError("Signing and encryption must use the same " 472 "type of key") 473 474 if pad_sig and hasattr(key, 'pad_sig'): 475 key.pad_sig = True 476 477 # Get list of custom protected TLVs from the command-line 478 custom_tlvs = {} 479 for tlv in custom_tlv: 480 tag = int(tlv[0], 0) 481 if tag in custom_tlvs: 482 raise click.UsageError('Custom TLV %s already exists.' % hex(tag)) 483 if tag in image.TLV_VALUES.values(): 484 raise click.UsageError( 485 'Custom TLV %s conflicts with predefined TLV.' % hex(tag)) 486 487 value = tlv[1] 488 if value.startswith('0x'): 489 if len(value[2:]) % 2: 490 raise click.UsageError('Custom TLV length is odd.') 491 custom_tlvs[tag] = bytes.fromhex(value[2:]) 492 else: 493 custom_tlvs[tag] = value.encode('utf-8') 494 495 # Allow signature calculated externally. 496 raw_signature = load_signature(fix_sig) if fix_sig else None 497 498 baked_signature = None 499 pub_key = None 500 501 if raw_signature is not None: 502 if fix_sig_pubkey is None: 503 raise click.UsageError( 504 'public key of the fixed signature is not specified') 505 506 pub_key = load_key(fix_sig_pubkey) 507 508 baked_signature = { 509 'value': raw_signature 510 } 511 512 img.create(key, public_key_format, enckey, dependencies, boot_record, 513 custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear, 514 baked_signature, pub_key, vector_to_sign, user_sha) 515 516 if compression in ["lzma2", "lzma2armthumb"]: 517 compressed_img = image.Image(version=decode_version(version), 518 header_size=header_size, pad_header=pad_header, 519 pad=pad, confirm=confirm, align=int(align), 520 slot_size=slot_size, max_sectors=max_sectors, 521 overwrite_only=overwrite_only, endian=endian, 522 load_addr=load_addr, rom_fixed=rom_fixed, 523 erased_val=erased_val, save_enctlv=save_enctlv, 524 security_counter=security_counter, max_align=max_align) 525 compression_filters = [ 526 {"id": lzma.FILTER_LZMA2, "preset": comp_default_preset, 527 "dict_size": comp_default_dictsize, "lp": comp_default_lp, 528 "lc": comp_default_lc} 529 ] 530 if compression == "lzma2armthumb": 531 compression_filters.insert(0, {"id":lzma.FILTER_ARMTHUMB}) 532 compressed_data = lzma.compress(img.get_infile_data(),filters=compression_filters, 533 format=lzma.FORMAT_RAW) 534 uncompressed_size = len(img.get_infile_data()) 535 compressed_size = len(compressed_data) 536 print(f"compressed image size: {compressed_size} bytes") 537 print(f"original image size: {uncompressed_size} bytes") 538 compression_tlvs["DECOMP_SIZE"] = struct.pack( 539 img.get_struct_endian() + 'L', img.image_size) 540 compression_tlvs["DECOMP_SHA"] = img.image_hash 541 compression_tlvs_size = len(compression_tlvs["DECOMP_SIZE"]) 542 compression_tlvs_size += len(compression_tlvs["DECOMP_SHA"]) 543 if img.get_signature(): 544 compression_tlvs["DECOMP_SIGNATURE"] = img.get_signature() 545 compression_tlvs_size += len(compression_tlvs["DECOMP_SIGNATURE"]) 546 if (compressed_size + compression_tlvs_size) < uncompressed_size: 547 compression_header = create_lzma2_header( 548 dictsize = comp_default_dictsize, pb = comp_default_pb, 549 lc = comp_default_lc, lp = comp_default_lp) 550 compressed_img.load_compressed(compressed_data, compression_header) 551 compressed_img.base_addr = img.base_addr 552 compressed_img.create(key, public_key_format, enckey, 553 dependencies, boot_record, custom_tlvs, compression_tlvs, 554 compression, int(encrypt_keylen), clear, baked_signature, 555 pub_key, vector_to_sign) 556 img = compressed_img 557 img.save(outfile, hex_addr) 558 if sig_out is not None: 559 new_signature = img.get_signature() 560 save_signature(sig_out, new_signature) 561 562 563class AliasesGroup(click.Group): 564 565 _aliases = { 566 "create": "sign", 567 } 568 569 def list_commands(self, ctx): 570 cmds = [k for k in self.commands] 571 aliases = [k for k in self._aliases] 572 return sorted(cmds + aliases) 573 574 def get_command(self, ctx, cmd_name): 575 rv = click.Group.get_command(self, ctx, cmd_name) 576 if rv is not None: 577 return rv 578 if cmd_name in self._aliases: 579 return click.Group.get_command(self, ctx, self._aliases[cmd_name]) 580 return None 581 582 583@click.command(help='Print imgtool version information') 584def version(): 585 print(imgtool_version) 586 587 588@click.command(cls=AliasesGroup, 589 context_settings=dict(help_option_names=['-h', '--help'])) 590def imgtool(): 591 pass 592 593 594imgtool.add_command(keygen) 595imgtool.add_command(getpub) 596imgtool.add_command(getpubhash) 597imgtool.add_command(getpriv) 598imgtool.add_command(verify) 599imgtool.add_command(sign) 600imgtool.add_command(version) 601imgtool.add_command(dumpinfo) 602 603 604if __name__ == '__main__': 605 imgtool() 606