blob: 92eaab6ac997ade7269731f5309a1011134da0a6 [file] [log] [blame]
Fabio Utzige89841d2018-12-21 11:19:06 -02001#! /usr/bin/env python3
2#
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.
16
17import click
18import getpass
19import imgtool.keys as keys
20from imgtool import image
David Brownc8751d72019-01-29 11:04:35 -070021from imgtool import suit as suitpkg
Fabio Utzige89841d2018-12-21 11:19:06 -020022from imgtool.version import decode_version
23
24
25def gen_rsa2048(keyfile, passwd):
26 keys.RSA2048.generate().export_private(path=keyfile, passwd=passwd)
27
28
29def gen_ecdsa_p256(keyfile, passwd):
30 keys.ECDSA256P1.generate().export_private(keyfile, passwd=passwd)
31
32
33def gen_ecdsa_p224(keyfile, passwd):
34 print("TODO: p-224 not yet implemented")
35
36
37valid_langs = ['c', 'rust']
38keygens = {
39 'rsa-2048': gen_rsa2048,
40 'ecdsa-p256': gen_ecdsa_p256,
41 'ecdsa-p224': gen_ecdsa_p224,
42}
43
44
45def load_key(keyfile):
46 # TODO: better handling of invalid pass-phrase
47 key = keys.load(keyfile)
48 if key is not None:
49 return key
50 passwd = getpass.getpass("Enter key passphrase: ").encode('utf-8')
51 return keys.load(keyfile, passwd)
52
53
54def get_password():
55 while True:
56 passwd = getpass.getpass("Enter key passphrase: ")
57 passwd2 = getpass.getpass("Reenter passphrase: ")
58 if passwd == passwd2:
59 break
60 print("Passwords do not match, try again")
61
62 # Password must be bytes, always use UTF-8 for consistent
63 # encoding.
64 return passwd.encode('utf-8')
65
66
67@click.option('-p', '--password', is_flag=True,
68 help='Prompt for password to protect key')
69@click.option('-t', '--type', metavar='type', required=True,
70 type=click.Choice(keygens.keys()))
71@click.option('-k', '--key', metavar='filename', required=True)
72@click.command(help='Generate pub/private keypair')
73def keygen(type, key, password):
74 password = get_password() if password else None
75 keygens[type](key, password)
76
77
78@click.option('-l', '--lang', metavar='lang', default=valid_langs[0],
79 type=click.Choice(valid_langs))
80@click.option('-k', '--key', metavar='filename', required=True)
81@click.command(help='Get public key from keypair')
82def getpub(key, lang):
83 key = load_key(key)
84 if key is None:
85 print("Invalid passphrase")
86 elif lang == 'c':
87 key.emit_c()
88 elif lang == 'rust':
89 key.emit_rust()
90 else:
91 raise ValueError("BUG: should never get here!")
92
93
94def validate_version(ctx, param, value):
95 try:
96 decode_version(value)
97 return value
98 except ValueError as e:
99 raise click.BadParameter("{}".format(e))
100
101
102def validate_header_size(ctx, param, value):
103 min_hdr_size = image.IMAGE_HEADER_SIZE
104 if value < min_hdr_size:
105 raise click.BadParameter(
106 "Minimum value for -H/--header-size is {}".format(min_hdr_size))
107 return value
108
109
110class BasedIntParamType(click.ParamType):
111 name = 'integer'
112
113 def convert(self, value, param, ctx):
114 try:
115 if value[:2].lower() == '0x':
116 return int(value[2:], 16)
117 elif value[:1] == '0':
118 return int(value, 8)
119 return int(value, 10)
120 except ValueError:
121 self.fail('%s is not a valid integer' % value, param, ctx)
122
123
124@click.argument('outfile')
125@click.argument('infile')
David Brownc8751d72019-01-29 11:04:35 -0700126@click.option('--suit', default=False, is_flag=True,
127 help="Use a SUIT signature instead of TLV")
Fabio Utzige89841d2018-12-21 11:19:06 -0200128@click.option('-E', '--encrypt', metavar='filename',
129 help='Encrypt image using the provided public key')
130@click.option('-e', '--endian', type=click.Choice(['little', 'big']),
131 default='little', help="Select little or big endian")
132@click.option('--overwrite-only', default=False, is_flag=True,
133 help='Use overwrite-only instead of swap upgrades')
134@click.option('-M', '--max-sectors', type=int,
135 help='When padding allow for this amount of sectors (defaults to 128)')
136@click.option('--pad', default=False, is_flag=True,
137 help='Pad image to --slot-size bytes, adding trailer magic')
138@click.option('-S', '--slot-size', type=BasedIntParamType(), required=True,
139 help='Size of the slot where the image will be written')
140@click.option('--pad-header', default=False, is_flag=True,
141 help='Add --header-size zeroed bytes at the beginning of the image')
142@click.option('-H', '--header-size', callback=validate_header_size,
143 type=BasedIntParamType(), required=True)
144@click.option('-v', '--version', callback=validate_version, required=True)
145@click.option('--align', type=click.Choice(['1', '2', '4', '8']),
146 required=True)
147@click.option('-k', '--key', metavar='filename')
Fabio Utzig7c00acd2019-01-07 09:54:20 -0200148@click.command(help='''Create a signed or unsigned image\n
149 INFILE and OUTFILE are parsed as Intel HEX if the params have
150 .hex extension, othewise binary format is used''')
Fabio Utzige89841d2018-12-21 11:19:06 -0200151def sign(key, align, version, header_size, pad_header, slot_size, pad,
David Brownc8751d72019-01-29 11:04:35 -0700152 max_sectors, overwrite_only, endian, encrypt, suit, infile, outfile):
153 if suit:
154 gen = suitpkg.Image
155 else:
156 gen = image.Image
157 img = gen(version=decode_version(version), header_size=header_size,
158 pad_header=pad_header, pad=pad, align=int(align),
159 slot_size=slot_size, max_sectors=max_sectors,
160 overwrite_only=overwrite_only, endian=endian)
Fabio Utzig7c00acd2019-01-07 09:54:20 -0200161 img.load(infile)
Fabio Utzige89841d2018-12-21 11:19:06 -0200162 key = load_key(key) if key else None
163 enckey = load_key(encrypt) if encrypt else None
164 if enckey:
165 if not isinstance(enckey, (keys.RSA2048, keys.RSA2048Public)):
166 raise Exception("Encryption only available with RSA")
167 if key and not isinstance(key, (keys.RSA2048, keys.RSA2048Public)):
168 raise Exception("Encryption with sign only available with RSA")
169 img.create(key, enckey)
Fabio Utzige89841d2018-12-21 11:19:06 -0200170 img.save(outfile)
171
172
173class AliasesGroup(click.Group):
174
175 _aliases = {
176 "create": "sign",
177 }
178
179 def list_commands(self, ctx):
180 cmds = [k for k in self.commands]
181 aliases = [k for k in self._aliases]
182 return sorted(cmds + aliases)
183
184 def get_command(self, ctx, cmd_name):
185 rv = click.Group.get_command(self, ctx, cmd_name)
186 if rv is not None:
187 return rv
188 if cmd_name in self._aliases:
189 return click.Group.get_command(self, ctx, self._aliases[cmd_name])
190 return None
191
192
193@click.command(cls=AliasesGroup,
194 context_settings=dict(help_option_names=['-h', '--help']))
195def imgtool():
196 pass
197
198
199imgtool.add_command(keygen)
200imgtool.add_command(getpub)
201imgtool.add_command(sign)
202
203
204if __name__ == '__main__':
205 imgtool()