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