imgtool: Add CBOR encoded boot record to TLV area

Add new '--boot-record' option for imgtool to add a new type of TLV to
the image manifest called BOOT_RECORD. This TLV contains CBOR encoded
data with some basic information about the image (SW component) it
belongs to, these are the following:
- SW type (role of the software component)
- SW version
- Signer ID (identifies the signing authority)
- Measurement value (hash of the image)
- Measurement type (algorithm used to calculate the measurement value)

The boot_record.py file and most of the modifications in image.py are
coming from the Trusted Firmware-M project
(https://www.trustedfirmware.org/about/).
Hash of the source commit: 08d5572b4bcee306d8cf709c2200359a22d5b72c.

This patch is based on the recommendations of Arm's Platform Security
Architecture (PSA) and its purpose is to support compliance with it.

Change-Id: I379ccc57b48ad2311837cb3fd90f5f9d1c9b5bac
Signed-off-by: David Vincze <david.vincze@linaro.org>
diff --git a/scripts/imgtool.nix b/scripts/imgtool.nix
index 6072742..7d750df 100644
--- a/scripts/imgtool.nix
+++ b/scripts/imgtool.nix
@@ -19,6 +19,7 @@
       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 0000000..4112b22
--- /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 9701e21..644a028 100644
--- a/scripts/imgtool/image.py
+++ b/scripts/imgtool/image.py
@@ -19,6 +19,7 @@
 """
 
 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 @@
 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 @@
         'ENCEC256': 0x32,
         'DEPENDENCY': 0x40,
         'SEC_CNT': 0x50,
+        'BOOT_RECORD': 0x60,
 }
 
 TLV_SIZE = 4
@@ -256,9 +259,18 @@
             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 @@
             #                                   = 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 @@
                 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 @@
         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 d998c5b..fa15200 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.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 @@
               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 @@
 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 @@
     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 d3bcfbb..9481e2c 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 2789094..058d0cb 100644
--- a/scripts/setup.py
+++ b/scripts/setup.py
@@ -10,10 +10,12 @@
     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"]