diff --git a/scripts/imgtool.nix b/scripts/imgtool.nix index 60727428..7d750df4 100644 --- a/scripts/imgtool.nix +++ b/scripts/imgtool.nix @@ -19,6 +19,7 @@ let python37.pkgs.cryptography python37.pkgs.intelhex python37.pkgs.setuptools + python37.pkgs.cbor ] ); in diff --git a/scripts/imgtool/boot_record.py b/scripts/imgtool/boot_record.py new file mode 100644 index 00000000..4112b225 --- /dev/null +++ b/scripts/imgtool/boot_record.py @@ -0,0 +1,47 @@ +# Copyright (c) 2019, Arm Limited. +# Copyright (c) 2020, Linaro Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum +import cbor + + +class SwComponent(int, Enum): + """ + Software component property IDs specified by + Arm's PSA Attestation API 1.0 document. + """ + TYPE = 1 + MEASUREMENT_VALUE = 2 + VERSION = 4 + SIGNER_ID = 5 + MEASUREMENT_DESCRIPTION = 6 + + +def create_sw_component_data(sw_type, sw_version, sw_measurement_description, + sw_measurement_value, sw_signer_id): + + # List of software component properties (Key ID + value) + properties = { + SwComponent.TYPE: sw_type, + SwComponent.VERSION: sw_version, + SwComponent.SIGNER_ID: sw_signer_id, + SwComponent.MEASUREMENT_DESCRIPTION: sw_measurement_description, + } + + # Note: The measurement value must be the last item of the property + # list because later it will be modified by the bootloader. + properties[SwComponent.MEASUREMENT_VALUE] = sw_measurement_value + + return cbor.dumps(properties) diff --git a/scripts/imgtool/image.py b/scripts/imgtool/image.py index 9701e211..644a0286 100644 --- a/scripts/imgtool/image.py +++ b/scripts/imgtool/image.py @@ -19,6 +19,7 @@ Image signing and management. """ from . import version as versmod +from .boot_record import create_sw_component_data import click from enum import Enum from intelhex import IntelHex @@ -42,6 +43,7 @@ DEFAULT_MAX_SECTORS = 128 MAX_ALIGN = 8 DEP_IMAGES_KEY = "images" DEP_VERSIONS_KEY = "versions" +MAX_SW_TYPE_LENGTH = 12 # Bytes # Image header flags. IMAGE_F = { @@ -63,6 +65,7 @@ TLV_VALUES = { 'ENCEC256': 0x32, 'DEPENDENCY': 0x40, 'SEC_CNT': 0x50, + 'BOOT_RECORD': 0x60, } TLV_SIZE = 4 @@ -256,9 +259,18 @@ class Image(): format=PublicFormat.UncompressedPoint) return cipherkey, ciphermac, pubk - def create(self, key, enckey, dependencies=None): + def create(self, key, enckey, dependencies=None, sw_type=None): self.enckey = enckey + # Calculate the hash of the public key + if key is not None: + pub = key.get_public_bytes() + sha = hashlib.sha256() + sha.update(pub) + pubbytes = sha.digest() + else: + pubbytes = bytes(hashlib.sha256().digest_size) + protected_tlv_size = 0 if self.security_counter is not None: @@ -266,6 +278,32 @@ class Image(): # = 4 + 4 = 8 Bytes protected_tlv_size += TLV_SIZE + 4 + if sw_type is not None: + if len(sw_type) > MAX_SW_TYPE_LENGTH: + msg = "'{}' is too long ({} characters) for sw_type. Its " \ + "maximum allowed length is 12 characters.".format( + sw_type, len(sw_type)) + raise click.UsageError(msg) + + image_version = (str(self.version.major) + '.' + + str(self.version.minor) + '.' + + str(self.version.revision)) + + # The image hash is computed over the image header, the image + # itself and the protected TLV area. However, the boot record TLV + # (which is part of the protected area) should contain this hash + # before it is even calculated. For this reason the script fills + # this field with zeros and the bootloader will insert the right + # value later. + digest = bytes(hashlib.sha256().digest_size) + + # Create CBOR encoded boot record + boot_record = create_sw_component_data(sw_type, image_version, + "SHA256", digest, + pubbytes) + + protected_tlv_size += TLV_SIZE + len(boot_record) + if dependencies is not None: # Size of a Dependency TLV = Header ('HH') + Payload('IBBHI') # = 4 + 12 = 16 Bytes @@ -293,6 +331,9 @@ class Image(): payload = struct.pack(e + 'I', self.security_counter) prot_tlv.add('SEC_CNT', payload) + if sw_type is not None: + prot_tlv.add('BOOT_RECORD', boot_record) + if dependencies is not None: for i in range(dependencies_num): payload = struct.pack( @@ -319,10 +360,6 @@ class Image(): tlv.add('SHA256', digest) if key is not None: - pub = key.get_public_bytes() - sha = hashlib.sha256() - sha.update(pub) - pubbytes = sha.digest() tlv.add('KEYHASH', pubbytes) # `sign` expects the full image payload (sha256 done internally), diff --git a/scripts/imgtool/main.py b/scripts/imgtool/main.py index d998c5b2..fa152007 100755 --- a/scripts/imgtool/main.py +++ b/scripts/imgtool/main.py @@ -1,6 +1,6 @@ #! /usr/bin/env python3 # -# Copyright 2017 Linaro Limited +# Copyright 2017-2020 Linaro Limited # Copyright 2019-2020 Arm Limited # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,11 @@ from imgtool import image, imgtool_version from imgtool.version import decode_version from .keys import RSAUsageError, ECDSAUsageError, Ed25519UsageError +MIN_PYTHON_VERSION = (3, 6) +if sys.version_info < MIN_PYTHON_VERSION: + sys.exit("Python %s.%s or newer is required by imgtool." + % MIN_PYTHON_VERSION) + def gen_rsa2048(keyfile, passwd): keys.RSA.generate().export_private(path=keyfile, passwd=passwd) @@ -230,6 +235,10 @@ class BasedIntParamType(click.ParamType): default='little', help="Select little or big endian") @click.option('--overwrite-only', default=False, is_flag=True, help='Use overwrite-only instead of swap upgrades') +@click.option('--boot-record', metavar='sw_type', help='Create CBOR encoded ' + 'boot record TLV. The sw_type represents the role of the ' + 'software component (e.g. CoFM for coprocessor firmware). ' + '[max. 12 characters]') @click.option('-M', '--max-sectors', type=int, help='When padding allow for this amount of sectors (defaults ' 'to 128)') @@ -263,7 +272,7 @@ class BasedIntParamType(click.ParamType): def sign(key, align, version, pad_sig, header_size, pad_header, slot_size, pad, confirm, max_sectors, overwrite_only, endian, encrypt, infile, outfile, dependencies, load_addr, hex_addr, erased_val, save_enctlv, - security_counter): + security_counter, boot_record): img = image.Image(version=decode_version(version), header_size=header_size, pad_header=pad_header, pad=pad, confirm=confirm, align=int(align), slot_size=slot_size, @@ -286,7 +295,7 @@ def sign(key, align, version, pad_sig, header_size, pad_header, slot_size, pad, if pad_sig and hasattr(key, 'pad_sig'): key.pad_sig = True - img.create(key, enckey, dependencies) + img.create(key, enckey, dependencies, boot_record) img.save(outfile, hex_addr) diff --git a/scripts/requirements.txt b/scripts/requirements.txt index d3bcfbb3..9481e2c1 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,3 +1,4 @@ cryptography>=2.6 intelhex click +cbor>=1.0.0 diff --git a/scripts/setup.py b/scripts/setup.py index 2789094e..058d0cb4 100644 --- a/scripts/setup.py +++ b/scripts/setup.py @@ -10,10 +10,12 @@ setuptools.setup( license="Apache Software License", url="http://github.com/JuulLabs-OSS/mcuboot", packages=setuptools.find_packages(), + python_requires='>=3.6', install_requires=[ 'cryptography>=2.4.2', 'intelhex>=2.2.1', 'click', + 'cbor>=1.0.0', ], entry_points={ "console_scripts": ["imgtool=imgtool.main:imgtool"]