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, signature = image.Image.verify(imgfile, key)
230    if ret == image.VerifyResult.OK:
231        print("Image was correctly validated")
232        print("Image version: {}.{}.{}+{}".format(*version))
233        if digest:
234            print("Image digest: {}".format(digest.hex()))
235        if signature and digest is None:
236            print("Image signature over image: {}".format(signature.hex()))
237        return
238    elif ret == image.VerifyResult.INVALID_MAGIC:
239        print("Invalid image magic; is this an MCUboot image?")
240    elif ret == image.VerifyResult.INVALID_TLV_INFO_MAGIC:
241        print("Invalid TLV info magic; is this an MCUboot image?")
242    elif ret == image.VerifyResult.INVALID_HASH:
243        print("Image has an invalid hash")
244    elif ret == image.VerifyResult.INVALID_SIGNATURE:
245        print("No signature found for the given key")
246    elif ret == image.VerifyResult.KEY_MISMATCH:
247        print("Key type does not match TLV record")
248    else:
249        print("Unknown return code: {}".format(ret))
250    sys.exit(1)
251
252
253@click.argument('imgfile')
254@click.option('-o', '--outfile', metavar='filename', required=False,
255              help='Save image information to outfile in YAML format')
256@click.option('-s', '--silent', default=False, is_flag=True,
257              help='Do not print image information to output')
258@click.command(help='Print header, TLV area and trailer information '
259                    'of a signed image')
260def dumpinfo(imgfile, outfile, silent):
261    dump_imginfo(imgfile, outfile, silent)
262    print("dumpinfo has run successfully")
263
264
265def validate_version(ctx, param, value):
266    try:
267        decode_version(value)
268        return value
269    except ValueError as e:
270        raise click.BadParameter("{}".format(e))
271
272
273def validate_security_counter(ctx, param, value):
274    if value is not None:
275        if value.lower() == 'auto':
276            return 'auto'
277        else:
278            try:
279                return int(value, 0)
280            except ValueError:
281                raise click.BadParameter(
282                    "{} is not a valid integer. Please use code literals "
283                    "prefixed with 0b/0B, 0o/0O, or 0x/0X as necessary."
284                    .format(value))
285
286
287def validate_header_size(ctx, param, value):
288    min_hdr_size = image.IMAGE_HEADER_SIZE
289    if value < min_hdr_size:
290        raise click.BadParameter(
291            "Minimum value for -H/--header-size is {}".format(min_hdr_size))
292    return value
293
294
295def get_dependencies(ctx, param, value):
296    if value is not None:
297        versions = []
298        images = re.findall(r"\((\d+)", value)
299        if len(images) == 0:
300            raise click.BadParameter(
301                "Image dependency format is invalid: {}".format(value))
302        raw_versions = re.findall(r",\s*([0-9.+]+)\)", value)
303        if len(images) != len(raw_versions):
304            raise click.BadParameter(
305                '''There's a mismatch between the number of dependency images
306                and versions in: {}'''.format(value))
307        for raw_version in raw_versions:
308            try:
309                versions.append(decode_version(raw_version))
310            except ValueError as e:
311                raise click.BadParameter("{}".format(e))
312        dependencies = dict()
313        dependencies[image.DEP_IMAGES_KEY] = images
314        dependencies[image.DEP_VERSIONS_KEY] = versions
315        return dependencies
316
317def create_lzma2_header(dictsize, pb, lc, lp):
318    header = bytearray()
319    for i in range(0, 40):
320        if dictsize <= ((2 | ((i) & 1)) << int((i) / 2 + 11)):
321            header.append(i)
322            break
323    header.append( ( pb * 5 + lp) * 9 + lc)
324    return header
325
326class BasedIntParamType(click.ParamType):
327    name = 'integer'
328
329    def convert(self, value, param, ctx):
330        try:
331            return int(value, 0)
332        except ValueError:
333            self.fail('%s is not a valid integer. Please use code literals '
334                      'prefixed with 0b/0B, 0o/0O, or 0x/0X as necessary.'
335                      % value, param, ctx)
336
337
338@click.argument('outfile')
339@click.argument('infile')
340@click.option('--non-bootable', default=False, is_flag=True,
341              help='Mark the image as non-bootable.')
342@click.option('--custom-tlv', required=False, nargs=2, default=[],
343              multiple=True, metavar='[tag] [value]',
344              help='Custom TLV that will be placed into protected area. '
345                   'Add "0x" prefix if the value should be interpreted as an '
346                   'integer, otherwise it will be interpreted as a string. '
347                   'Specify the option multiple times to add multiple TLVs.')
348@click.option('-R', '--erased-val', type=click.Choice(['0', '0xff']),
349              required=False,
350              help='The value that is read back from erased flash.')
351@click.option('-x', '--hex-addr', type=BasedIntParamType(), required=False,
352              help='Adjust address in hex output file.')
353@click.option('-L', '--load-addr', type=BasedIntParamType(), required=False,
354              help='Load address for image when it should run from RAM.')
355@click.option('-F', '--rom-fixed', type=BasedIntParamType(), required=False,
356              help='Set flash address the image is built for.')
357@click.option('--save-enctlv', default=False, is_flag=True,
358              help='When upgrading, save encrypted key TLVs instead of plain '
359                   'keys. Enable when BOOT_SWAP_SAVE_ENCTLV config option '
360                   'was set.')
361@click.option('-E', '--encrypt', metavar='filename',
362              help='Encrypt image using the provided public key. '
363                   '(Not supported in direct-xip or ram-load mode.)')
364@click.option('--encrypt-keylen', default='128',
365              type=click.Choice(['128', '256']),
366              help='When encrypting the image using AES, select a 128 bit or '
367                   '256 bit key len.')
368@click.option('--compression', default='disabled',
369              type=click.Choice(['disabled', 'lzma2', 'lzma2armthumb']),
370              help='Enable image compression using specified type. '
371                   'Will fall back without image compression automatically '
372                   'if the compression increases the image size.')
373@click.option('-c', '--clear', required=False, is_flag=True, default=False,
374              help='Output a non-encrypted image with encryption capabilities,'
375                   'so it can be installed in the primary slot, and encrypted '
376                   'when swapped to the secondary.')
377@click.option('-e', '--endian', type=click.Choice(['little', 'big']),
378              default='little', help="Select little or big endian")
379@click.option('--overwrite-only', default=False, is_flag=True,
380              help='Use overwrite-only instead of swap upgrades')
381@click.option('--boot-record', metavar='sw_type', help='Create CBOR encoded '
382              'boot record TLV. The sw_type represents the role of the '
383              'software component (e.g. CoFM for coprocessor firmware). '
384              '[max. 12 characters]')
385@click.option('-M', '--max-sectors', type=int,
386              help='When padding allow for this amount of sectors (defaults '
387                   'to 128)')
388@click.option('--confirm', default=False, is_flag=True,
389              help='When padding the image, mark it as confirmed (implies '
390                   '--pad)')
391@click.option('--pad', default=False, is_flag=True,
392              help='Pad image to --slot-size bytes, adding trailer magic')
393@click.option('-S', '--slot-size', type=BasedIntParamType(), required=True,
394              help='Size of the slot. If the slots have different sizes, use '
395              'the size of the secondary slot.')
396@click.option('--pad-header', default=False, is_flag=True,
397              help='Add --header-size zeroed bytes at the beginning of the '
398                   'image')
399@click.option('-H', '--header-size', callback=validate_header_size,
400              type=BasedIntParamType(), required=True)
401@click.option('--pad-sig', default=False, is_flag=True,
402              help='Add 0-2 bytes of padding to ECDSA signature '
403                   '(for mcuboot <1.5)')
404@click.option('-d', '--dependencies', callback=get_dependencies,
405              required=False, help='''Add dependence on another image, format:
406              "(<image_ID>,<image_version>), ... "''')
407@click.option('-s', '--security-counter', callback=validate_security_counter,
408              help='Specify the value of security counter. Use the `auto` '
409              'keyword to automatically generate it from the image version.')
410@click.option('-v', '--version', callback=validate_version,  required=True)
411@click.option('--align', type=click.Choice(['1', '2', '4', '8', '16', '32']),
412              default='1',
413              required=False,
414              help='Alignment used by swap update modes.')
415@click.option('--max-align', type=click.Choice(['8', '16', '32']),
416              required=False,
417              help='Maximum flash alignment. Set if flash alignment of the '
418              'primary and secondary slot differ and any of them is larger '
419              'than 8.')
420@click.option('--public-key-format', type=click.Choice(['hash', 'full']),
421              default='hash', help='In what format to add the public key to '
422              'the image manifest: full key or hash of the key.')
423@click.option('-k', '--key', metavar='filename')
424@click.option('--fix-sig', metavar='filename',
425              help='fixed signature for the image. It will be used instead of '
426              'the signature calculated using the public key')
427@click.option('--fix-sig-pubkey', metavar='filename',
428              help='public key relevant to fixed signature')
429@click.option('--pure', 'is_pure', is_flag=True, default=False, show_default=True,
430              help='Expected Pure variant of signature; the Pure variant is '
431              'expected to be signature done over an image rather than hash of '
432              'that image.')
433@click.option('--sig-out', metavar='filename',
434              help='Path to the file to which signature will be written. '
435              'The image signature will be encoded as base64 formatted string')
436@click.option('--sha', 'user_sha', type=click.Choice(valid_sha), default='auto',
437              help='selected sha algorithm to use; defaults to "auto" which is 256 if '
438              'no cryptographic signature is used, or default for signature type')
439@click.option('--vector-to-sign', type=click.Choice(['payload', 'digest']),
440              help='send to OUTFILE the payload or payload''s digest instead '
441              'of complied image. These data can be used for external image '
442              'signing')
443@click.command(help='''Create a signed or unsigned image\n
444               INFILE and OUTFILE are parsed as Intel HEX if the params have
445               .hex extension, otherwise binary format is used''')
446def sign(key, public_key_format, align, version, pad_sig, header_size,
447         pad_header, slot_size, pad, confirm, max_sectors, overwrite_only,
448         endian, encrypt_keylen, encrypt, compression, infile, outfile,
449         dependencies, load_addr, hex_addr, erased_val, save_enctlv,
450         security_counter, boot_record, custom_tlv, rom_fixed, max_align,
451         clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, is_pure,
452         vector_to_sign, non_bootable):
453
454    if confirm:
455        # Confirmed but non-padded images don't make much sense, because
456        # otherwise there's no trailer area for writing the confirmed status.
457        pad = True
458    img = image.Image(version=decode_version(version), header_size=header_size,
459                      pad_header=pad_header, pad=pad, confirm=confirm,
460                      align=int(align), slot_size=slot_size,
461                      max_sectors=max_sectors, overwrite_only=overwrite_only,
462                      endian=endian, load_addr=load_addr, rom_fixed=rom_fixed,
463                      erased_val=erased_val, save_enctlv=save_enctlv,
464                      security_counter=security_counter, max_align=max_align,
465                      non_bootable=non_bootable)
466    compression_tlvs = {}
467    img.load(infile)
468    key = load_key(key) if key else None
469    enckey = load_key(encrypt) if encrypt else None
470    if enckey and key:
471        if ((isinstance(key, keys.ECDSA256P1) and
472             not isinstance(enckey, keys.ECDSA256P1Public))
473           or (isinstance(key, keys.ECDSA384P1) and
474               not isinstance(enckey, keys.ECDSA384P1Public))
475                or (isinstance(key, keys.RSA) and
476                    not isinstance(enckey, keys.RSAPublic))):
477            # FIXME
478            raise click.UsageError("Signing and encryption must use the same "
479                                   "type of key")
480
481    if pad_sig and hasattr(key, 'pad_sig'):
482        key.pad_sig = True
483
484    # Get list of custom protected TLVs from the command-line
485    custom_tlvs = {}
486    for tlv in custom_tlv:
487        tag = int(tlv[0], 0)
488        if tag in custom_tlvs:
489            raise click.UsageError('Custom TLV %s already exists.' % hex(tag))
490        if tag in image.TLV_VALUES.values():
491            raise click.UsageError(
492                'Custom TLV %s conflicts with predefined TLV.' % hex(tag))
493
494        value = tlv[1]
495        if value.startswith('0x'):
496            if len(value[2:]) % 2:
497                raise click.UsageError('Custom TLV length is odd.')
498            custom_tlvs[tag] = bytes.fromhex(value[2:])
499        else:
500            custom_tlvs[tag] = value.encode('utf-8')
501
502    # Allow signature calculated externally.
503    raw_signature = load_signature(fix_sig) if fix_sig else None
504
505    baked_signature = None
506    pub_key = None
507
508    if raw_signature is not None:
509        if fix_sig_pubkey is None:
510            raise click.UsageError(
511                'public key of the fixed signature is not specified')
512
513        pub_key = load_key(fix_sig_pubkey)
514
515        baked_signature = {
516            'value': raw_signature
517        }
518
519    if is_pure and user_sha != 'auto':
520        raise click.UsageError(
521            'Pure signatures, currently, enforces preferred hash algorithm, '
522            'and forbids sha selection by user.')
523
524    if compression in ["lzma2", "lzma2armthumb"]:
525        img.create(key, public_key_format, enckey, dependencies, boot_record,
526               custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear,
527               baked_signature, pub_key, vector_to_sign, user_sha=user_sha,
528               is_pure=is_pure, keep_comp_size=False, dont_encrypt=True)
529        compressed_img = image.Image(version=decode_version(version),
530                  header_size=header_size, pad_header=pad_header,
531                  pad=pad, confirm=confirm, align=int(align),
532                  slot_size=slot_size, max_sectors=max_sectors,
533                  overwrite_only=overwrite_only, endian=endian,
534                  load_addr=load_addr, rom_fixed=rom_fixed,
535                  erased_val=erased_val, save_enctlv=save_enctlv,
536                  security_counter=security_counter, max_align=max_align)
537        compression_filters = [
538            {"id": lzma.FILTER_LZMA2, "preset": comp_default_preset,
539                "dict_size": comp_default_dictsize, "lp": comp_default_lp,
540                "lc": comp_default_lc}
541        ]
542        if compression == "lzma2armthumb":
543            compression_filters.insert(0, {"id":lzma.FILTER_ARMTHUMB})
544        compressed_data = lzma.compress(img.get_infile_data(),filters=compression_filters,
545            format=lzma.FORMAT_RAW)
546        uncompressed_size = len(img.get_infile_data())
547        compressed_size = len(compressed_data)
548        print(f"compressed image size: {compressed_size} bytes")
549        print(f"original image size: {uncompressed_size} bytes")
550        compression_tlvs["DECOMP_SIZE"] = struct.pack(
551            img.get_struct_endian() + 'L', img.image_size)
552        compression_tlvs["DECOMP_SHA"] = img.image_hash
553        compression_tlvs_size = len(compression_tlvs["DECOMP_SIZE"])
554        compression_tlvs_size += len(compression_tlvs["DECOMP_SHA"])
555        if img.get_signature():
556            compression_tlvs["DECOMP_SIGNATURE"] = img.get_signature()
557            compression_tlvs_size += len(compression_tlvs["DECOMP_SIGNATURE"])
558        if (compressed_size + compression_tlvs_size) < uncompressed_size:
559            compression_header = create_lzma2_header(
560                dictsize = comp_default_dictsize, pb = comp_default_pb,
561                lc = comp_default_lc, lp = comp_default_lp)
562            compressed_img.load_compressed(compressed_data, compression_header)
563            compressed_img.base_addr = img.base_addr
564            keep_comp_size = False;
565            if enckey:
566                keep_comp_size = True
567            compressed_img.create(key, public_key_format, enckey,
568               dependencies, boot_record, custom_tlvs, compression_tlvs,
569               compression, int(encrypt_keylen), clear, baked_signature,
570               pub_key, vector_to_sign, user_sha=user_sha,
571               is_pure=is_pure, keep_comp_size=keep_comp_size)
572            img = compressed_img
573    else:
574        img.create(key, public_key_format, enckey, dependencies, boot_record,
575               custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear,
576               baked_signature, pub_key, vector_to_sign, user_sha=user_sha,
577               is_pure=is_pure)
578    img.save(outfile, hex_addr)
579    if sig_out is not None:
580        new_signature = img.get_signature()
581        save_signature(sig_out, new_signature)
582
583
584class AliasesGroup(click.Group):
585
586    _aliases = {
587        "create": "sign",
588    }
589
590    def list_commands(self, ctx):
591        cmds = [k for k in self.commands]
592        aliases = [k for k in self._aliases]
593        return sorted(cmds + aliases)
594
595    def get_command(self, ctx, cmd_name):
596        rv = click.Group.get_command(self, ctx, cmd_name)
597        if rv is not None:
598            return rv
599        if cmd_name in self._aliases:
600            return click.Group.get_command(self, ctx, self._aliases[cmd_name])
601        return None
602
603
604@click.command(help='Print imgtool version information')
605def version():
606    print(imgtool_version)
607
608
609@click.command(cls=AliasesGroup,
610               context_settings=dict(help_option_names=['-h', '--help']))
611def imgtool():
612    pass
613
614
615imgtool.add_command(keygen)
616imgtool.add_command(getpub)
617imgtool.add_command(getpubhash)
618imgtool.add_command(getpriv)
619imgtool.add_command(verify)
620imgtool.add_command(sign)
621imgtool.add_command(version)
622imgtool.add_command(dumpinfo)
623
624
625if __name__ == '__main__':
626    imgtool()
627