Boot: Add measured boot record TLV to image manifest

Generate CBOR encoded boot record structure at build time and add as a
new type of TLV to the image manifest for device attestation. This new
TLV can represent the image to which it belongs as one item of the
SW_COMPONENTS array in an IAT token.

Change-Id: I84adf0faa57c40428c3e48cce4398e346d9192b7
Signed-off-by: David Vincze <david.vincze@arm.com>
diff --git a/bl2/ext/mcuboot/scripts/imgtool.py b/bl2/ext/mcuboot/scripts/imgtool.py
index 1b539c2..2d1d97b 100644
--- a/bl2/ext/mcuboot/scripts/imgtool.py
+++ b/bl2/ext/mcuboot/scripts/imgtool.py
@@ -103,6 +103,13 @@
                                  + (version_num.minor << 16)
                                  + version_num.revision)
 
+    if "_s.c" in args.layout:
+        sw_type = "SPE"
+    elif "_ns.c" in args.layout:
+        sw_type = "NSPE"
+    else:
+        sw_type = "NSPE_SPE"
+
     pad_size = macro_parser.evaluate_macro(args.layout, sign_bin_size_re, 0, 1)
     img = image.Image.load(args.infile,
                            version=version_num,
@@ -112,7 +119,7 @@
                            pad=pad_size)
     key = keys.load(args.key, args.public_key_format) if args.key else None
     ram_load_address = macro_parser.evaluate_macro(args.layout, image_load_address_re, 0, 1)
-    img.sign(key, ram_load_address, args.dependencies)
+    img.sign(sw_type, key, ram_load_address, args.dependencies)
 
     if pad_size:
         img.pad_to(pad_size, args.align)
diff --git a/bl2/ext/mcuboot/scripts/imgtool_lib/boot_record.py b/bl2/ext/mcuboot/scripts/imgtool_lib/boot_record.py
new file mode 100644
index 0000000..41887bb
--- /dev/null
+++ b/bl2/ext/mcuboot/scripts/imgtool_lib/boot_record.py
@@ -0,0 +1,77 @@
+
+# Copyright (c) 2019, Arm 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.
+
+import os
+import sys
+import cbor
+
+
+# SW component IDs
+SW_COMPONENT_RANGE = 0
+SW_COMPONENT_TYPE = SW_COMPONENT_RANGE + 1
+MEASUREMENT_VALUE = SW_COMPONENT_RANGE + 2
+SW_COMPONENT_VERSION = SW_COMPONENT_RANGE + 4
+SIGNER_ID = SW_COMPONENT_RANGE + 5
+MEASUREMENT_DESCRIPTION = SW_COMPONENT_RANGE + 6
+
+
+def create_sw_component_data(sw_type, sw_version, sw_measurement_type,
+                             sw_measurement_value, sw_signer_id):
+
+    # List of SW component claims (key ID + value)
+    key_value_list = [
+        SW_COMPONENT_TYPE, sw_type,
+        SW_COMPONENT_VERSION, sw_version,
+        SIGNER_ID, sw_signer_id,
+        MEASUREMENT_DESCRIPTION, sw_measurement_type,
+        MEASUREMENT_VALUE, sw_measurement_value
+    ]
+    # The measurement value should be the last item (key + value) in the list
+    # to make it easier to modify its value later in the bootloader.
+    # A dictionary would be the best suited data structure to store these
+    # key-value pairs (claims), however dictionaries are not sorted, but for
+    # example the lists do keep to order of items which we care about now.
+    # An ordered dictionary could be used instead, but it would be converted
+    # to a dict before the encoding and this conversion may not keep the order
+    # of the items.
+
+    if (len(key_value_list) % 2) != 0:
+        print('Error: The length of the sw component claim list must '
+              'be even (key + value).', file=sys.stderr)
+        sys.exit(1)
+    else:
+        claim_number = (int)(len(key_value_list) / 2)
+
+    # The output of this function must be a CBOR encoded map (dictionary) of
+    # the SW component claims. The CBOR representation of an array and a map
+    # (dictionary) is quite similar. To convert the encoded list to a map, it
+    # is enough to modify the first byte (CBOR data item header) of the
+    # data. This applies up to 23 items (11 claims in this case) - until the 5
+    # lower bits of the item header are used as an item count specifier.
+
+    if claim_number > 11:
+        print('Error: There are more than 11 claims in the '
+              'list of sw component claims.', file=sys.stderr)
+        sys.exit(1)
+
+    record_array = bytearray(cbor.dumps(key_value_list))
+    # Modify the CBOR data item header (from array to map)
+    # 7..5 bits : Major type
+    #             Array - 0x80
+    #             Map   - 0xA0
+    # 4..0 bits : Number of items
+    record_array[0] = 0xA0 + claim_number
+
+    return bytes(record_array)
diff --git a/bl2/ext/mcuboot/scripts/imgtool_lib/image.py b/bl2/ext/mcuboot/scripts/imgtool_lib/image.py
index cb97a35..d89ec99 100644
--- a/bl2/ext/mcuboot/scripts/imgtool_lib/image.py
+++ b/bl2/ext/mcuboot/scripts/imgtool_lib/image.py
@@ -18,6 +18,7 @@
 """
 
 from . import version as versmod
+from . import boot_record as br
 import hashlib
 import struct
 
@@ -41,7 +42,8 @@
         'RSA2048': 0x20,
         'RSA3072': 0x23,
         'DEPENDENCY': 0x40,
-        'SEC_CNT': 0x50, }
+        'SEC_CNT': 0x50,
+        'BOOT_RECORD': 0x60, }
 
 TLV_INFO_SIZE = 4
 TLV_INFO_MAGIC = 0x6907
@@ -116,10 +118,39 @@
             if any(v != 0 and v != b'\000' for v in self.payload[0:self.header_size]):
                 raise Exception("Padding requested, but image does not start with zeros")
 
-    def sign(self, key, ramLoadAddress, dependencies=None):
-        # Size of the security counter TLV:
-        # header ('BBH') + payload ('I') = 8 Bytes
-        protected_tlv_size = TLV_INFO_SIZE + 8
+    def sign(self, sw_type, key, ramLoadAddress, dependencies=None):
+        image_version = (str(self.version.major) + '.'
+                      + str(self.version.minor) + '.'
+                      + str(self.version.revision))
+
+        # 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(KEYHASH_SIZE)
+
+        # 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.
+        image_hash = bytes(PAYLOAD_DIGEST_SIZE)
+
+        # Create CBOR encoded boot record
+        boot_record = br.create_sw_component_data(sw_type, image_version,
+                                                  "SHA256", image_hash,
+                                                  pubbytes)
+
+        # Mandatory protected TLV area: TLV info header
+        #                               + security counter TLV
+        #                               + boot record TLV
+        # Size of the security counter TLV: header ('BBH') + payload ('I')
+        #                                   = 8 Bytes
+        protected_tlv_size = TLV_INFO_SIZE + 8 + TLV_HEADER_SIZE \
+                           + len(boot_record)
 
         if dependencies is None:
             dependencies_num = 0
@@ -135,6 +166,7 @@
 
         payload = struct.pack('I', self.security_cnt)
         tlv.add('SEC_CNT', payload)
+        tlv.add('BOOT_RECORD', boot_record)
 
         if dependencies_num != 0:
             for i in range(dependencies_num):
@@ -153,7 +185,6 @@
         full_size = (TLV_INFO_SIZE + len(tlv.buf) + TLV_HEADER_SIZE
                      + PAYLOAD_DIGEST_SIZE)
         if key is not None:
-            pub = key.get_public_bytes()
             if key.get_public_key_format() == 'hash':
                 tlv_key_data_size = KEYHASH_SIZE
             else:
@@ -166,15 +197,12 @@
 
         sha = hashlib.sha256()
         sha.update(self.payload)
-        digest = sha.digest()
+        image_hash = sha.digest()
 
-        tlv.add('SHA256', digest)
+        tlv.add('SHA256', image_hash)
 
         if key is not None:
             if key.get_public_key_format() == 'hash':
-                sha = hashlib.sha256()
-                sha.update(pub)
-                pubbytes = sha.digest()
                 tlv.add('KEYHASH', pubbytes)
             else:
                 tlv.add('KEY', pub)