blob: ccac193870909358023c62c9c07cde9ae550dbe3 [file] [log] [blame]
David Brown23f91ad2017-05-16 11:38:17 -06001#! /usr/bin/env python3
David Brown1314bf32017-12-20 11:10:55 -07002#
3# Copyright 2017 Linaro Limited
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
David Brown23f91ad2017-05-16 11:38:17 -060016
Fabio Utzig51c112a2018-03-27 07:25:07 -030017import click
David Brown1d5bea12017-11-16 15:11:10 -070018import getpass
David Brown23f91ad2017-05-16 11:38:17 -060019from imgtool import keys
20from imgtool import image
Fabio Utzig51c112a2018-03-27 07:25:07 -030021from imgtool.version import decode_version
David Brown23f91ad2017-05-16 11:38:17 -060022
David Brown1d5bea12017-11-16 15:11:10 -070023
Fabio Utzig51c112a2018-03-27 07:25:07 -030024def gen_rsa2048(keyfile, passwd):
25 keys.RSA2048.generate().export_private(path=keyfile, passwd=passwd)
David Brownf88d9f92017-12-06 10:53:35 -070026
David Brown1d5bea12017-11-16 15:11:10 -070027
Fabio Utzig51c112a2018-03-27 07:25:07 -030028def gen_ecdsa_p256(keyfile, passwd):
29 keys.ECDSA256P1.generate().export_private(keyfile, passwd=passwd)
David Brownf88d9f92017-12-06 10:53:35 -070030
Fabio Utzig51c112a2018-03-27 07:25:07 -030031
32def gen_ecdsa_p224(keyfile, passwd):
David Brown23f91ad2017-05-16 11:38:17 -060033 print("TODO: p-224 not yet implemented")
34
Fabio Utzig51c112a2018-03-27 07:25:07 -030035
36valid_langs = ['c', 'rust']
David Brown23f91ad2017-05-16 11:38:17 -060037keygens = {
Fabio Utzig51c112a2018-03-27 07:25:07 -030038 'rsa-2048': gen_rsa2048,
39 'ecdsa-p256': gen_ecdsa_p256,
40 'ecdsa-p224': gen_ecdsa_p224,
Fabio Utzigaa70dae2018-05-10 07:34:50 -030041}
David Brown23f91ad2017-05-16 11:38:17 -060042
David Brown23f91ad2017-05-16 11:38:17 -060043
Fabio Utzig51c112a2018-03-27 07:25:07 -030044def load_key(keyfile):
45 # TODO: better handling of invalid pass-phrase
46 key = keys.load(keyfile)
47 if key is not None:
48 return key
49 passwd = getpass.getpass("Enter key passphrase: ").encode('utf-8')
50 return keys.load(keyfile, passwd)
David Brown23f91ad2017-05-16 11:38:17 -060051
David Brown23f91ad2017-05-16 11:38:17 -060052
Fabio Utzig51c112a2018-03-27 07:25:07 -030053def get_password():
54 while True:
55 passwd = getpass.getpass("Enter key passphrase: ")
56 passwd2 = getpass.getpass("Reenter passphrase: ")
57 if passwd == passwd2:
58 break
59 print("Passwords do not match, try again")
David Brown23f91ad2017-05-16 11:38:17 -060060
Fabio Utzig51c112a2018-03-27 07:25:07 -030061 # Password must be bytes, always use UTF-8 for consistent
62 # encoding.
63 return passwd.encode('utf-8')
David Brown23f91ad2017-05-16 11:38:17 -060064
David Brown23f91ad2017-05-16 11:38:17 -060065
Fabio Utzig51c112a2018-03-27 07:25:07 -030066@click.option('-p', '--password', is_flag=True,
67 help='Prompt for password to protect key')
68@click.option('-t', '--type', metavar='type', required=True,
69 type=click.Choice(keygens.keys()))
70@click.option('-k', '--key', metavar='filename', required=True)
71@click.command(help='Generate pub/private keypair')
72def keygen(type, key, password):
73 password = get_password() if password else None
74 keygens[type](key, password)
David Brown23f91ad2017-05-16 11:38:17 -060075
David Brown23f91ad2017-05-16 11:38:17 -060076
Fabio Utzig51c112a2018-03-27 07:25:07 -030077@click.option('-l', '--lang', metavar='lang', default=valid_langs[0],
78 type=click.Choice(valid_langs))
79@click.option('-k', '--key', metavar='filename', required=True)
80@click.command(help='Get public key from keypair')
81def getpub(key, lang):
82 key = load_key(key)
83 if key is None:
84 print("Invalid passphrase")
85 elif lang == 'c':
86 key.emit_c()
87 elif lang == 'rust':
88 key.emit_rust()
89 else:
90 raise ValueError("BUG: should never get here!")
91
92
93def validate_version(ctx, param, value):
94 try:
95 decode_version(value)
96 return value
97 except ValueError as e:
98 raise click.BadParameter("{}".format(e))
99
100
101class BasedIntParamType(click.ParamType):
102 name = 'integer'
103
104 def convert(self, value, param, ctx):
105 try:
106 if value[:2].lower() == '0x':
107 return int(value[2:], 16)
108 elif value[:1] == '0':
109 return int(value, 8)
110 return int(value, 10)
111 except ValueError:
112 self.fail('%s is not a valid integer' % value, param, ctx)
113
114
115@click.argument('outfile')
116@click.argument('infile')
Fabio Utzigdcf0c9b2018-06-11 12:27:49 -0700117@click.option('--overwrite-only', default=False, is_flag=True,
118 help='Use overwrite-only instead of swap upgrades')
Fabio Utzig519285f2018-06-04 11:11:53 -0300119@click.option('-M', '--max-sectors', type=int,
120 help='When padding allow for this amount of sectors (defaults to 128)')
Fabio Utzig263d4392018-06-05 10:37:35 -0300121@click.option('--pad', default=False, is_flag=True,
122 help='Pad image to --slot-size bytes, adding trailer magic')
123@click.option('-S', '--slot-size', type=BasedIntParamType(), required=True,
124 help='Size of the slot where the image will be written')
Fabio Utzig51c112a2018-03-27 07:25:07 -0300125@click.option('--included-header', default=False, is_flag=True,
126 help='Image has gap for header')
127@click.option('-H', '--header-size', type=BasedIntParamType(), required=True)
128@click.option('-v', '--version', callback=validate_version, required=True)
129@click.option('--align', type=click.Choice(['1', '2', '4', '8']),
130 required=True)
131@click.option('-k', '--key', metavar='filename')
132@click.command(help='Create a signed or unsigned image')
Fabio Utzig263d4392018-06-05 10:37:35 -0300133def sign(key, align, version, header_size, included_header, slot_size, pad,
Fabio Utzigdcf0c9b2018-06-11 12:27:49 -0700134 max_sectors, overwrite_only, infile, outfile):
Fabio Utzig51c112a2018-03-27 07:25:07 -0300135 img = image.Image.load(infile, version=decode_version(version),
136 header_size=header_size,
Fabio Utzig263d4392018-06-05 10:37:35 -0300137 included_header=included_header, pad=pad,
138 align=int(align), slot_size=slot_size,
Fabio Utzigdcf0c9b2018-06-11 12:27:49 -0700139 max_sectors=max_sectors,
140 overwrite_only=overwrite_only)
Fabio Utzig51c112a2018-03-27 07:25:07 -0300141 key = load_key(key) if key else None
142 img.sign(key)
143
Fabio Utzig263d4392018-06-05 10:37:35 -0300144 if pad:
145 img.pad_to(slot_size)
Fabio Utzig51c112a2018-03-27 07:25:07 -0300146
147 img.save(outfile)
148
149
150class AliasesGroup(click.Group):
151
152 _aliases = {
153 "create": "sign",
154 }
155
156 def list_commands(self, ctx):
157 cmds = [k for k in self.commands]
158 aliases = [k for k in self._aliases]
159 return sorted(cmds + aliases)
160
161 def get_command(self, ctx, cmd_name):
162 rv = click.Group.get_command(self, ctx, cmd_name)
163 if rv is not None:
164 return rv
165 if cmd_name in self._aliases:
166 return click.Group.get_command(self, ctx, self._aliases[cmd_name])
167 return None
168
169
170@click.command(cls=AliasesGroup,
171 context_settings=dict(help_option_names=['-h', '--help']))
172def imgtool():
173 pass
174
175
176imgtool.add_command(keygen)
177imgtool.add_command(getpub)
178imgtool.add_command(sign)
179
David Brown23f91ad2017-05-16 11:38:17 -0600180
181if __name__ == '__main__':
Fabio Utzig51c112a2018-03-27 07:25:07 -0300182 imgtool()