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