1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00

dist/tools: add imgtool

This commit is contained in:
kYc0o 2017-06-09 17:53:06 +02:00
parent 39c6051090
commit f464cf830a
6 changed files with 531 additions and 0 deletions

89
dist/tools/mcuboot/imgtool.md vendored Normal file
View File

@ -0,0 +1,89 @@
## Image tool
The Python program `imgtool.py` can be used to perform the
operations that are necessary to manage keys and sign images.
This program is written for Python3, and has several dependencies on
Python libraries. These can be installed using 'pip3' manually:
pip3 install --user pycrypto
pip3 install --user pyasn1
pip3 install --user ecdsa
or, on Ubuntu, using the package manager:
sudo apt-get install python3-crypto python3-pyasn1 python3-ecdsa
## Managing keys
This tool currently supports rsa-2048 and ecdsa-p256 keys. You can
generate a keypair for one of these types using the 'keygen' command:
./imgtool.py keygen -k filename.pem -t rsa-2048
or use ecdsa-p256 for the type. The key type used should match what
mcuboot is configured to verify.
This key file is what is used to sign images, this file should be
protected, and not widely distributed.
## Incorporating the public key into the code
There is a development key distributed with mcuboot that can be used
for testing. Since this private key is widely distributed, it should
never be used for production. Once you have generated a production
key, as described above, you should replace the public key in the
bootloader with the generated one.
For Zephyr, the keys live in the file `boot/zephyr/keys.c`. For
mynewt, follow the instructions in `doc/signed_images.md` to generate
the key file.
./imgtool.py getpub -k filename.pem
will extract the public key from the given private key file, and
output it as a C data structure. You can replace or insert this code
into the key file.
## Signing images
Image signing takes a binary image intended for Slot 0 and adds a
header and trailer that the bootloader is expecting:
usage: imgtool.py sign [-h] -k filename --align ALIGN -v VERSION -H
HEADER_SIZE [--pad PAD] [--rsa-pkcs1-15]
infile outfile
positional arguments:
infile
outfile
optional arguments:
-h, --help show this help message and exit
-k filename, --key filename
--align ALIGN
-v VERSION, --version VERSION
-H HEADER_SIZE, --header-size HEADER_SIZE
--included-header Image has gap for header
--pad PAD Pad image to this many bytes, adding trailer magic
--rsa-pkcs1-15 Use old PKCS#1 v1.5 signature algorithm
The main arguments given are the key file generated above, a version
field to place in the header (1.2.3 for example), the alignment of the
flash device in question, and the header size.
The header size depends on the operating system and the particular
flash device. For Zephyr, it will be configured as part of the build,
and will be a small power of two. By default, the header will be
prepended to the image. If `--included-header` is given, the image
must start with header-size bytes of zeros, and the header will be
overwritten over these bytes.
The optional --pad argument will place a trailer on the image that
indicates that the image should be considered an upgrade. Writing
this image in slot 1 will then cause the bootloader to upgrade to it.
Lastly, the --rsa-pkcs1-15 will cause the tool to use the older,
deprecated pkcs#1 v1.5 signing algorithm when using RSA. This can be
enabled in the bootloader as well, and may be needed if you are using
an older version of the bootloader.

100
dist/tools/mcuboot/imgtool.py vendored Executable file
View File

@ -0,0 +1,100 @@
#! /usr/bin/env python3
import argparse
from imgtool import keys
from imgtool import image
from imgtool import version
import sys
def gen_rsa2048(args):
keys.RSA2048.generate().export_private(args.key)
def gen_ecdsa_p256(args):
keys.ecdsa256p1.generate().export_private(args.key)
def gen_ecdsa_p224(args):
print("TODO: p-224 not yet implemented")
keygens = {
'rsa-2048': gen_rsa2048,
'ecdsa-p256': gen_ecdsa_p256,
'ecdsa-p224': gen_ecdsa_p224, }
def do_keygen(args):
if args.type not in keygens:
msg = "Unexpected key type: {}".format(args.type)
raise argparse.ArgumentTypeError(msg)
keygens[args.type](args)
def do_getpub(args):
key = keys.load(args.key)
key.emit_c()
def do_sign(args):
align = args.align
if args.rsa_pkcs1_15:
keys.sign_rsa_pss = False
img = image.Image.load(args.infile, version=args.version,
header_size=args.header_size,
included_header=args.included_header,
pad=args.pad)
key = keys.load(args.key) if args.key else None
img.sign(key)
if args.pad:
img.pad_to(args.pad, args.align)
img.save(args.outfile)
subcmds = {
'keygen': do_keygen,
'getpub': do_getpub,
'sign': do_sign, }
def alignment_value(text):
value = int(text)
if value not in [1, 2, 4, 8]:
msg = "{} must be one of 1, 2, 4 or 8".format(value)
raise argparse.ArgumentTypeError(msg)
return value
def intparse(text):
"""Parse a command line argument as an integer.
Accepts 0x and other prefixes to allow other bases to be used."""
return int(text, 0)
def args():
parser = argparse.ArgumentParser()
subs = parser.add_subparsers(help='subcommand help', dest='subcmd')
keygenp = subs.add_parser('keygen', help='Generate pub/private keypair')
keygenp.add_argument('-k', '--key', metavar='filename', required=True)
keygenp.add_argument('-t', '--type', metavar='type',
choices=['rsa-2048', 'ecdsa-p224', 'ecdsa-p256'],
required=True)
getpub = subs.add_parser('getpub', help='Get public key from keypair')
getpub.add_argument('-k', '--key', metavar='filename', required=True)
sign = subs.add_parser('sign', help='Sign an image with a private key')
sign.add_argument('-k', '--key', metavar='filename')
sign.add_argument("--align", type=alignment_value, required=True)
sign.add_argument("-v", "--version", type=version.decode_version, required=True)
sign.add_argument("-H", "--header-size", type=intparse, required=True)
sign.add_argument("--included-header", default=False, action='store_true',
help='Image has gap for header')
sign.add_argument("--pad", type=intparse,
help='Pad image to this many bytes, adding trailer magic')
sign.add_argument("--rsa-pkcs1-15", help='Use old PKCS#1 v1.5 signature algorithm',
default=False, action='store_true')
sign.add_argument("infile")
sign.add_argument("outfile")
args = parser.parse_args()
if args.subcmd is None:
print('Must specify a subcommand', file=sys.stderr)
sys.exit(1)
subcmds[args.subcmd](args)
if __name__ == '__main__':
args()

View File

172
dist/tools/mcuboot/imgtool/image.py vendored Normal file
View File

@ -0,0 +1,172 @@
"""
Image signing and management.
"""
from . import version as versmod
import hashlib
import struct
IMAGE_MAGIC = 0x96f3b83c
IMAGE_HEADER_SIZE = 32
# Image header flags.
IMAGE_F = {
'PIC': 0x0000001,
'SHA256': 0x0000002,
'PKCS15_RSA2048_SHA256': 0x0000004,
'ECDSA224_SHA256': 0x0000008,
'NON_BOOTABLE': 0x0000010,
'ECDSA256_SHA256': 0x0000020,
'PKCS1_PSS_RSA2048_SHA256': 0x0000040, }
TLV_VALUES = {
'SHA256': 1,
'RSA2048': 2,
'ECDSA224': 3,
'ECDSA256': 4, }
TLV_HEADER_SIZE = 4
# Sizes of the image trailer, depending on image alignment.
trailer_sizes = {
1: 402,
2: 788,
4: 1560,
8: 3104, }
boot_magic = bytes([
0x77, 0xc2, 0x95, 0xf3,
0x60, 0xd2, 0xef, 0x7f,
0x35, 0x52, 0x50, 0x0f,
0x2c, 0xb6, 0x79, 0x80, ])
class TLV():
def __init__(self):
self.buf = bytearray()
def add(self, kind, payload):
"""Add a TLV record. Kind should be a string found in TLV_VALUES above."""
buf = struct.pack('<BBH', TLV_VALUES[kind], 0, len(payload))
self.buf += buf
self.buf += payload
def get(self):
return bytes(self.buf)
class Image():
@classmethod
def load(cls, path, included_header=False, **kwargs):
"""Load an image from a given file"""
with open(path, 'rb') as f:
payload = f.read()
obj = cls(**kwargs)
obj.payload = payload
# Add the image header if needed.
if not included_header and obj.header_size > 0:
obj.payload = (b'\000' * obj.header_size) + obj.payload
obj.check()
return obj
def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE, pad=0):
self.version = version or versmod.decode_version("0")
self.header_size = header_size or IMAGE_HEADER_SIZE
self.pad = pad
def __repr__(self):
return "<Image version={}, header_size={}, pad={}, payloadlen=0x{:x}>".format(
self.version,
self.header_size,
self.pad,
len(self.payload))
def save(self, path):
with open(path, 'wb') as f:
f.write(self.payload)
def check(self):
"""Perform some sanity checking of the image."""
# If there is a header requested, make sure that the image
# starts with all zeros.
if self.header_size > 0:
if any(v != 0 for v in self.payload[0:self.header_size]):
raise Exception("Padding requested, but image does not start with zeros")
def sign(self, key):
self.add_header(key)
tlv = TLV()
# Note that ecdsa wants to do the hashing itself, which means
# we get to hash it twice.
sha = hashlib.sha256()
sha.update(self.payload)
digest = sha.digest()
tlv.add('SHA256', digest)
if key is not None:
sig = key.sign(self.payload)
tlv.add(key.sig_tlv(), sig)
self.payload += tlv.get()
def add_header(self, key):
"""Install the image header.
The key is needed to know the type of signature, and
approximate the size of the signature."""
flags = 0
tlvsz = 0
if key is not None:
flags |= IMAGE_F[key.sig_type()]
tlvsz += TLV_HEADER_SIZE + key.sig_len()
flags |= IMAGE_F['SHA256']
tlvsz += 4 + hashlib.sha256().digest_size
fmt = ('<' +
# type ImageHdr struct {
'I' + # Magic uint32
'H' + # TlvSz uint16
'B' + # KeyId uint8
'B' + # Pad1 uint8
'H' + # HdrSz uint16
'H' + # Pad2 uint16
'I' + # ImgSz uint32
'I' + # Flags uint32
'BBHI' + # Vers ImageVersion
'I' # Pad3 uint32
) # }
assert struct.calcsize(fmt) == IMAGE_HEADER_SIZE
header = struct.pack(fmt,
IMAGE_MAGIC,
tlvsz, # TlvSz
0, # KeyId (TODO: allow other ids)
0, # Pad1
self.header_size,
0, # Pad2
len(self.payload) - self.header_size, # ImageSz
flags, # Flags
self.version.major,
self.version.minor or 0,
self.version.revision or 0,
self.version.build or 0,
0) # Pad3
self.payload = bytearray(self.payload)
self.payload[:len(header)] = header
def pad_to(self, size, align):
"""Pad the image to the given size, with the given flash alignment."""
tsize = trailer_sizes[align]
padding = size - (len(self.payload) + tsize)
if padding < 0:
msg = "Image size (0x{:x}) + trailer (0x{:x}) exceeds requested size 0x{:x}".format(
len(self.payload), tsize, size)
raise Exception(msg)
pbytes = b'\xff' * padding
pbytes += boot_magic
pbytes += b'\xff' * (tsize - len(boot_magic))
self.payload += pbytes

137
dist/tools/mcuboot/imgtool/keys.py vendored Normal file
View File

@ -0,0 +1,137 @@
"""
Cryptographic key management for imgtool.
"""
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5, PKCS1_PSS
from ecdsa import SigningKey, NIST256p, util
import hashlib
from pyasn1.type import namedtype, univ
from pyasn1.codec.der.encoder import encode
# By default, we use RSA-PSS (PKCS 2.1). That can be overridden on
# the command line to support the older (less secure) PKCS1.5
sign_rsa_pss = True
AUTOGEN_MESSAGE = "/* Autogenerated by imgtool.py, do not edit. */"
class RSAPublicKey(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('modulus', univ.Integer()),
namedtype.NamedType('publicExponent', univ.Integer()))
class RSA2048():
def __init__(self, key):
"""Construct an RSA2048 key with the given key data"""
self.key = key
@staticmethod
def generate():
return RSA2048(RSA.generate(2048))
def export_private(self, path):
with open(path, 'wb') as f:
f.write(self.key.exportKey('PEM'))
def emit_c(self):
node = RSAPublicKey()
node['modulus'] = self.key.n
node['publicExponent'] = self.key.e
print(AUTOGEN_MESSAGE)
print("const unsigned char rsa_pub_key[] = {", end='')
encoded = bytearray(encode(node))
for count, b in enumerate(encoded):
if count % 8 == 0:
print("\n\t", end='')
else:
print(" ", end='')
print("0x{:02x},".format(b), end='')
print("\n};")
print("const unsigned int rsa_pub_key_len = {};".format(len(encoded)))
def sig_type(self):
"""Return the type of this signature (as a string)"""
if sign_rsa_pss:
return "PKCS1_PSS_RSA2048_SHA256"
else:
return "PKCS15_RSA2048_SHA256"
def sig_len(self):
return 256
def sig_tlv(self):
return "RSA2048"
def sign(self, payload):
sha = SHA256.new(payload)
if sign_rsa_pss:
signer = PKCS1_PSS.new(self.key)
else:
signer = PKCS1_v1_5.new(self.key)
signature = signer.sign(sha)
assert len(signature) == self.sig_len()
return signature
class ECDSA256P1():
def __init__(self, key):
"""Construct an ECDSA P-256 private key"""
self.key = key
@staticmethod
def generate():
return ECDSA256P1(SigningKey.generate(curve=NIST256p))
def export_private(self, path):
with open(path, 'wb') as f:
f.write(key.to_pem())
def emit_c(self):
vk = self.key.get_verifying_key()
print(AUTOGEN_MESSAGE)
print("const unsigned char ecdsa_pub_key[] = {", end='')
encoded = bytes(vk.to_der())
for count, b in enumerate(encoded):
if count % 8 == 0:
print("\n\t", end='')
else:
print(" ", end='')
print("0x{:02x},".format(b), end='')
print("\n};")
print("const unsigned int ecdsa_pub_key_len = {};".format(len(encoded)))
def sign(self, payload):
# To make this fixed length, possibly pad with zeros.
sig = self.key.sign(payload, hashfunc=hashlib.sha256, sigencode=util.sigencode_der)
sig += b'\000' * (self.sig_len() - len(sig))
return sig
def sig_len(self):
# The DER encoding depends on the high bit, and can be
# anywhere from 70 to 72 bytes. Because we have to fill in
# the length field before computing the signature, however,
# we'll give the largest, and the sig checking code will allow
# for it to be up to two bytes larger than the actual
# signature.
return 72
def sig_type(self):
"""Return the type of this signature (as a string)"""
return "ECDSA256_SHA256"
def sig_tlv(self):
return "ECDSA256"
def load(path):
with open(path, 'rb') as f:
pem = f.read()
try:
key = RSA.importKey(pem)
if key.n.bit_length() != 2048:
raise Exception("Unsupported RSA bit length, only 2048 supported")
return RSA2048(key)
except ValueError:
key = SigningKey.from_pem(pem)
if key.curve.name != 'NIST256p':
raise Exception("Unsupported ECDSA curve")
return ECDSA256P1(key)

33
dist/tools/mcuboot/imgtool/version.py vendored Normal file
View File

@ -0,0 +1,33 @@
"""
Semi Semantic Versioning
Implements a subset of semantic versioning that is supportable by the image header.
"""
import argparse
from collections import namedtuple
import re
SemiSemVersion = namedtuple('SemiSemVersion', ['major', 'minor', 'revision', 'build'])
version_re = re.compile(r"""^([1-9]\d*|0)(\.([1-9]\d*|0)(\.([1-9]\d*|0)(\+([1-9]\d*|0))?)?)?$""")
def decode_version(text):
"""Decode the version string, which should be of the form maj.min.rev+build"""
m = version_re.match(text)
# print("decode:", text, m.groups())
if m:
result = SemiSemVersion(
int(m.group(1)) if m.group(1) else 0,
int(m.group(3)) if m.group(3) else 0,
int(m.group(5)) if m.group(5) else 0,
int(m.group(7)) if m.group(7) else 0)
return result
else:
msg = "Invalid version number, should be maj.min.rev+build with later parts optional"
raise argparse.ArgumentTypeError(msg)
if __name__ == '__main__':
print(decode_version("1.2"))
print(decode_version("1.0"))
print(decode_version("0.0.2+75"))
print(decode_version("0.0.0+00"))