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 base64 26from imgtool import image, imgtool_version 27from imgtool.version import decode_version 28from imgtool.dumpinfo import dump_imginfo 29from .keys import ( 30 RSAUsageError, ECDSAUsageError, Ed25519UsageError, X25519UsageError) 31 32MIN_PYTHON_VERSION = (3, 6) 33if sys.version_info < MIN_PYTHON_VERSION: 34 sys.exit("Python %s.%s or newer is required by imgtool." 35 % MIN_PYTHON_VERSION) 36 37 38def gen_rsa2048(keyfile, passwd): 39 keys.RSA.generate().export_private(path=keyfile, passwd=passwd) 40 41 42def gen_rsa3072(keyfile, passwd): 43 keys.RSA.generate(key_size=3072).export_private(path=keyfile, 44 passwd=passwd) 45 46 47def gen_ecdsa_p256(keyfile, passwd): 48 keys.ECDSA256P1.generate().export_private(keyfile, passwd=passwd) 49 50 51def gen_ecdsa_p384(keyfile, passwd): 52 keys.ECDSA384P1.generate().export_private(keyfile, passwd=passwd) 53 54 55def gen_ed25519(keyfile, passwd): 56 keys.Ed25519.generate().export_private(path=keyfile, passwd=passwd) 57 58 59def gen_x25519(keyfile, passwd): 60 keys.X25519.generate().export_private(path=keyfile, passwd=passwd) 61 62 63valid_langs = ['c', 'rust'] 64valid_encodings = ['lang-c', 'lang-rust', 'pem'] 65keygens = { 66 'rsa-2048': gen_rsa2048, 67 'rsa-3072': gen_rsa3072, 68 'ecdsa-p256': gen_ecdsa_p256, 69 'ecdsa-p384': gen_ecdsa_p384, 70 'ed25519': gen_ed25519, 71 'x25519': gen_x25519, 72} 73valid_formats = ['openssl', 'pkcs8'] 74 75 76def load_signature(sigfile): 77 with open(sigfile, 'rb') as f: 78 signature = base64.b64decode(f.read()) 79 return signature 80 81 82def save_signature(sigfile, sig): 83 with open(sigfile, 'wb') as f: 84 signature = base64.b64encode(sig) 85 f.write(signature) 86 87 88def load_key(keyfile): 89 # TODO: better handling of invalid pass-phrase 90 key = keys.load(keyfile) 91 if key is not None: 92 return key 93 passwd = getpass.getpass("Enter key passphrase: ").encode('utf-8') 94 return keys.load(keyfile, passwd) 95 96 97def get_password(): 98 while True: 99 passwd = getpass.getpass("Enter key passphrase: ") 100 passwd2 = getpass.getpass("Reenter passphrase: ") 101 if passwd == passwd2: 102 break 103 print("Passwords do not match, try again") 104 105 # Password must be bytes, always use UTF-8 for consistent 106 # encoding. 107 return passwd.encode('utf-8') 108 109 110@click.option('-p', '--password', is_flag=True, 111 help='Prompt for password to protect key') 112@click.option('-t', '--type', metavar='type', required=True, 113 type=click.Choice(keygens.keys()), prompt=True, 114 help='{}'.format('One of: {}'.format(', '.join(keygens.keys())))) 115@click.option('-k', '--key', metavar='filename', required=True) 116@click.command(help='Generate pub/private keypair') 117def keygen(type, key, password): 118 password = get_password() if password else None 119 keygens[type](key, password) 120 121 122@click.option('-l', '--lang', metavar='lang', 123 type=click.Choice(valid_langs), 124 help='This option is deprecated. Please use the ' 125 '`--encoding` option. ' 126 'Valid langs: {}'.format(', '.join(valid_langs))) 127@click.option('-e', '--encoding', metavar='encoding', 128 type=click.Choice(valid_encodings), 129 help='Valid encodings: {}'.format(', '.join(valid_encodings))) 130@click.option('-k', '--key', metavar='filename', required=True) 131@click.command(help='Dump public key from keypair') 132def getpub(key, encoding, lang): 133 if encoding and lang: 134 raise click.UsageError('Please use only one of `--encoding/-e` ' 135 'or `--lang/-l`') 136 elif not encoding and not lang: 137 # Preserve old behavior defaulting to `c`. If `lang` is removed, 138 # `default=valid_encodings[0]` should be added to `-e` param. 139 lang = valid_langs[0] 140 key = load_key(key) 141 if key is None: 142 print("Invalid passphrase") 143 elif lang == 'c' or encoding == 'lang-c': 144 key.emit_c_public() 145 elif lang == 'rust' or encoding == 'lang-rust': 146 key.emit_rust_public() 147 elif encoding == 'pem': 148 key.emit_public_pem() 149 else: 150 raise click.UsageError() 151 152 153@click.option('--minimal', default=False, is_flag=True, 154 help='Reduce the size of the dumped private key to include only ' 155 'the minimum amount of data required to decrypt. This ' 156 'might require changes to the build config. Check the docs!' 157 ) 158@click.option('-k', '--key', metavar='filename', required=True) 159@click.option('-f', '--format', 160 type=click.Choice(valid_formats), 161 help='Valid formats: {}'.format(', '.join(valid_formats)) 162 ) 163@click.command(help='Dump private key from keypair') 164def getpriv(key, minimal, format): 165 key = load_key(key) 166 if key is None: 167 print("Invalid passphrase") 168 try: 169 key.emit_private(minimal, format) 170 except (RSAUsageError, ECDSAUsageError, Ed25519UsageError, 171 X25519UsageError) as e: 172 raise click.UsageError(e) 173 174 175@click.argument('imgfile') 176@click.option('-k', '--key', metavar='filename') 177@click.command(help="Check that signed image can be verified by given key") 178def verify(key, imgfile): 179 key = load_key(key) if key else None 180 ret, version, digest = image.Image.verify(imgfile, key) 181 if ret == image.VerifyResult.OK: 182 print("Image was correctly validated") 183 print("Image version: {}.{}.{}+{}".format(*version)) 184 print("Image digest: {}".format(digest.hex())) 185 return 186 elif ret == image.VerifyResult.INVALID_MAGIC: 187 print("Invalid image magic; is this an MCUboot image?") 188 elif ret == image.VerifyResult.INVALID_TLV_INFO_MAGIC: 189 print("Invalid TLV info magic; is this an MCUboot image?") 190 elif ret == image.VerifyResult.INVALID_HASH: 191 print("Image has an invalid hash") 192 elif ret == image.VerifyResult.INVALID_SIGNATURE: 193 print("No signature found for the given key") 194 else: 195 print("Unknown return code: {}".format(ret)) 196 sys.exit(1) 197 198 199@click.argument('imgfile') 200@click.option('-o', '--outfile', metavar='filename', required=False, 201 help='Save image information to outfile in YAML format') 202@click.option('-s', '--silent', default=False, is_flag=True, 203 help='Do not print image information to output') 204@click.command(help='Print header, TLV area and trailer information ' 205 'of a signed image') 206def dumpinfo(imgfile, outfile, silent): 207 dump_imginfo(imgfile, outfile, silent) 208 print("dumpinfo has run successfully") 209 210 211def validate_version(ctx, param, value): 212 try: 213 decode_version(value) 214 return value 215 except ValueError as e: 216 raise click.BadParameter("{}".format(e)) 217 218 219def validate_security_counter(ctx, param, value): 220 if value is not None: 221 if value.lower() == 'auto': 222 return 'auto' 223 else: 224 try: 225 return int(value, 0) 226 except ValueError: 227 raise click.BadParameter( 228 "{} is not a valid integer. Please use code literals " 229 "prefixed with 0b/0B, 0o/0O, or 0x/0X as necessary." 230 .format(value)) 231 232 233def validate_header_size(ctx, param, value): 234 min_hdr_size = image.IMAGE_HEADER_SIZE 235 if value < min_hdr_size: 236 raise click.BadParameter( 237 "Minimum value for -H/--header-size is {}".format(min_hdr_size)) 238 return value 239 240 241def get_dependencies(ctx, param, value): 242 if value is not None: 243 versions = [] 244 images = re.findall(r"\((\d+)", value) 245 if len(images) == 0: 246 raise click.BadParameter( 247 "Image dependency format is invalid: {}".format(value)) 248 raw_versions = re.findall(r",\s*([0-9.+]+)\)", value) 249 if len(images) != len(raw_versions): 250 raise click.BadParameter( 251 '''There's a mismatch between the number of dependency images 252 and versions in: {}'''.format(value)) 253 for raw_version in raw_versions: 254 try: 255 versions.append(decode_version(raw_version)) 256 except ValueError as e: 257 raise click.BadParameter("{}".format(e)) 258 dependencies = dict() 259 dependencies[image.DEP_IMAGES_KEY] = images 260 dependencies[image.DEP_VERSIONS_KEY] = versions 261 return dependencies 262 263 264class BasedIntParamType(click.ParamType): 265 name = 'integer' 266 267 def convert(self, value, param, ctx): 268 try: 269 return int(value, 0) 270 except ValueError: 271 self.fail('%s is not a valid integer. Please use code literals ' 272 'prefixed with 0b/0B, 0o/0O, or 0x/0X as necessary.' 273 % value, param, ctx) 274 275 276@click.argument('outfile') 277@click.argument('infile') 278@click.option('--custom-tlv', required=False, nargs=2, default=[], 279 multiple=True, metavar='[tag] [value]', 280 help='Custom TLV that will be placed into protected area. ' 281 'Add "0x" prefix if the value should be interpreted as an ' 282 'integer, otherwise it will be interpreted as a string. ' 283 'Specify the option multiple times to add multiple TLVs.') 284@click.option('-R', '--erased-val', type=click.Choice(['0', '0xff']), 285 required=False, 286 help='The value that is read back from erased flash.') 287@click.option('-x', '--hex-addr', type=BasedIntParamType(), required=False, 288 help='Adjust address in hex output file.') 289@click.option('-L', '--load-addr', type=BasedIntParamType(), required=False, 290 help='Load address for image when it should run from RAM.') 291@click.option('-F', '--rom-fixed', type=BasedIntParamType(), required=False, 292 help='Set flash address the image is built for.') 293@click.option('--save-enctlv', default=False, is_flag=True, 294 help='When upgrading, save encrypted key TLVs instead of plain ' 295 'keys. Enable when BOOT_SWAP_SAVE_ENCTLV config option ' 296 'was set.') 297@click.option('-E', '--encrypt', metavar='filename', 298 help='Encrypt image using the provided public key. ' 299 '(Not supported in direct-xip or ram-load mode.)') 300@click.option('--encrypt-keylen', default='128', 301 type=click.Choice(['128', '256']), 302 help='When encrypting the image using AES, select a 128 bit or ' 303 '256 bit key len.') 304@click.option('-c', '--clear', required=False, is_flag=True, default=False, 305 help='Output a non-encrypted image with encryption capabilities,' 306 'so it can be installed in the primary slot, and encrypted ' 307 'when swapped to the secondary.') 308@click.option('-e', '--endian', type=click.Choice(['little', 'big']), 309 default='little', help="Select little or big endian") 310@click.option('--overwrite-only', default=False, is_flag=True, 311 help='Use overwrite-only instead of swap upgrades') 312@click.option('--boot-record', metavar='sw_type', help='Create CBOR encoded ' 313 'boot record TLV. The sw_type represents the role of the ' 314 'software component (e.g. CoFM for coprocessor firmware). ' 315 '[max. 12 characters]') 316@click.option('-M', '--max-sectors', type=int, 317 help='When padding allow for this amount of sectors (defaults ' 318 'to 128)') 319@click.option('--confirm', default=False, is_flag=True, 320 help='When padding the image, mark it as confirmed (implies ' 321 '--pad)') 322@click.option('--pad', default=False, is_flag=True, 323 help='Pad image to --slot-size bytes, adding trailer magic') 324@click.option('-S', '--slot-size', type=BasedIntParamType(), required=True, 325 help='Size of the slot. If the slots have different sizes, use ' 326 'the size of the secondary slot.') 327@click.option('--pad-header', default=False, is_flag=True, 328 help='Add --header-size zeroed bytes at the beginning of the ' 329 'image') 330@click.option('-H', '--header-size', callback=validate_header_size, 331 type=BasedIntParamType(), required=True) 332@click.option('--pad-sig', default=False, is_flag=True, 333 help='Add 0-2 bytes of padding to ECDSA signature ' 334 '(for mcuboot <1.5)') 335@click.option('-d', '--dependencies', callback=get_dependencies, 336 required=False, help='''Add dependence on another image, format: 337 "(<image_ID>,<image_version>), ... "''') 338@click.option('-s', '--security-counter', callback=validate_security_counter, 339 help='Specify the value of security counter. Use the `auto` ' 340 'keyword to automatically generate it from the image version.') 341@click.option('-v', '--version', callback=validate_version, required=True) 342@click.option('--align', type=click.Choice(['1', '2', '4', '8', '16', '32']), 343 required=True) 344@click.option('--max-align', type=click.Choice(['8', '16', '32']), 345 required=False, 346 help='Maximum flash alignment. Set if flash alignment of the ' 347 'primary and secondary slot differ and any of them is larger ' 348 'than 8.') 349@click.option('--public-key-format', type=click.Choice(['hash', 'full']), 350 default='hash', help='In what format to add the public key to ' 351 'the image manifest: full key or hash of the key.') 352@click.option('-k', '--key', metavar='filename') 353@click.option('--fix-sig', metavar='filename', 354 help='fixed signature for the image. It will be used instead of ' 355 'the signature calculated using the public key') 356@click.option('--fix-sig-pubkey', metavar='filename', 357 help='public key relevant to fixed signature') 358@click.option('--sig-out', metavar='filename', 359 help='Path to the file to which signature will be written. ' 360 'The image signature will be encoded as base64 formatted string') 361@click.option('--vector-to-sign', type=click.Choice(['payload', 'digest']), 362 help='send to OUTFILE the payload or payload''s digest instead ' 363 'of complied image. These data can be used for external image ' 364 'signing') 365@click.command(help='''Create a signed or unsigned image\n 366 INFILE and OUTFILE are parsed as Intel HEX if the params have 367 .hex extension, otherwise binary format is used''') 368def sign(key, public_key_format, align, version, pad_sig, header_size, 369 pad_header, slot_size, pad, confirm, max_sectors, overwrite_only, 370 endian, encrypt_keylen, encrypt, infile, outfile, dependencies, 371 load_addr, hex_addr, erased_val, save_enctlv, security_counter, 372 boot_record, custom_tlv, rom_fixed, max_align, clear, fix_sig, 373 fix_sig_pubkey, sig_out, vector_to_sign): 374 375 if confirm: 376 # Confirmed but non-padded images don't make much sense, because 377 # otherwise there's no trailer area for writing the confirmed status. 378 pad = True 379 img = image.Image(version=decode_version(version), header_size=header_size, 380 pad_header=pad_header, pad=pad, confirm=confirm, 381 align=int(align), slot_size=slot_size, 382 max_sectors=max_sectors, overwrite_only=overwrite_only, 383 endian=endian, load_addr=load_addr, rom_fixed=rom_fixed, 384 erased_val=erased_val, save_enctlv=save_enctlv, 385 security_counter=security_counter, max_align=max_align) 386 img.load(infile) 387 key = load_key(key) if key else None 388 enckey = load_key(encrypt) if encrypt else None 389 if enckey and key: 390 if ((isinstance(key, keys.ECDSA256P1) and 391 not isinstance(enckey, keys.ECDSA256P1Public)) 392 or (isinstance(key, keys.ECDSA384P1) and 393 not isinstance(enckey, keys.ECDSA384P1Public)) 394 or (isinstance(key, keys.RSA) and 395 not isinstance(enckey, keys.RSAPublic))): 396 # FIXME 397 raise click.UsageError("Signing and encryption must use the same " 398 "type of key") 399 400 if pad_sig and hasattr(key, 'pad_sig'): 401 key.pad_sig = True 402 403 # Get list of custom protected TLVs from the command-line 404 custom_tlvs = {} 405 for tlv in custom_tlv: 406 tag = int(tlv[0], 0) 407 if tag in custom_tlvs: 408 raise click.UsageError('Custom TLV %s already exists.' % hex(tag)) 409 if tag in image.TLV_VALUES.values(): 410 raise click.UsageError( 411 'Custom TLV %s conflicts with predefined TLV.' % hex(tag)) 412 413 value = tlv[1] 414 if value.startswith('0x'): 415 if len(value[2:]) % 2: 416 raise click.UsageError('Custom TLV length is odd.') 417 custom_tlvs[tag] = bytes.fromhex(value[2:]) 418 else: 419 custom_tlvs[tag] = value.encode('utf-8') 420 421 # Allow signature calculated externally. 422 raw_signature = load_signature(fix_sig) if fix_sig else None 423 424 baked_signature = None 425 pub_key = None 426 427 if raw_signature is not None: 428 if fix_sig_pubkey is None: 429 raise click.UsageError( 430 'public key of the fixed signature is not specified') 431 432 pub_key = load_key(fix_sig_pubkey) 433 434 baked_signature = { 435 'value': raw_signature 436 } 437 438 img.create(key, public_key_format, enckey, dependencies, boot_record, 439 custom_tlvs, int(encrypt_keylen), clear, baked_signature, 440 pub_key, vector_to_sign) 441 img.save(outfile, hex_addr) 442 443 if sig_out is not None: 444 new_signature = img.get_signature() 445 save_signature(sig_out, new_signature) 446 447 448class AliasesGroup(click.Group): 449 450 _aliases = { 451 "create": "sign", 452 } 453 454 def list_commands(self, ctx): 455 cmds = [k for k in self.commands] 456 aliases = [k for k in self._aliases] 457 return sorted(cmds + aliases) 458 459 def get_command(self, ctx, cmd_name): 460 rv = click.Group.get_command(self, ctx, cmd_name) 461 if rv is not None: 462 return rv 463 if cmd_name in self._aliases: 464 return click.Group.get_command(self, ctx, self._aliases[cmd_name]) 465 return None 466 467 468@click.command(help='Print imgtool version information') 469def version(): 470 print(imgtool_version) 471 472 473@click.command(cls=AliasesGroup, 474 context_settings=dict(help_option_names=['-h', '--help'])) 475def imgtool(): 476 pass 477 478 479imgtool.add_command(keygen) 480imgtool.add_command(getpub) 481imgtool.add_command(getpriv) 482imgtool.add_command(verify) 483imgtool.add_command(sign) 484imgtool.add_command(version) 485imgtool.add_command(dumpinfo) 486 487 488if __name__ == '__main__': 489 imgtool() 490