imgtool: Add pure signature support

Adds PureEdDSA signature support.

The change includes implementation of SIG_PURE TLV that, when present,
indicates the signature that is present is Pure type.

Signed-off-by: Dominik Ermel <dominik.ermel@nordicsemi.no>
Signed-off-by: Mateusz Michalek <mateusz.michalek@nordicsemi.no>
diff --git a/scripts/imgtool/image.py b/scripts/imgtool/image.py
index 5fec3c1..88ef7b9 100644
--- a/scripts/imgtool/image.py
+++ b/scripts/imgtool/image.py
@@ -190,7 +190,15 @@
     keys.X25519             : ['256', '512']
 }
 
-def key_and_user_sha_to_alg_and_tlv(key, user_sha):
+ALLOWED_PURE_KEY_SHA = {
+    keys.Ed25519            : ['512']
+}
+
+ALLOWED_PURE_SIG_TLVS = [
+    TLV_VALUES['ED25519']
+]
+
+def key_and_user_sha_to_alg_and_tlv(key, user_sha, is_pure = False):
     """Matches key and user requested sha to sha alogrithm and TLV name.
 
        The returned tuple will contain hash functions and TVL name.
@@ -204,12 +212,16 @@
 
     # If key is not None, then we have to filter hash to only allowed
     allowed = None
+    allowed_key_ssh = ALLOWED_PURE_KEY_SHA if is_pure else ALLOWED_KEY_SHA
     try:
-        allowed = ALLOWED_KEY_SHA[type(key)]
+        allowed = allowed_key_ssh[type(key)]
+
     except KeyError:
         raise click.UsageError("Colud not find allowed hash algorithms for {}"
                                .format(type(key)))
-    if user_sha == 'auto':
+
+    # Pure enforces auto, and user selection is ignored
+    if user_sha == 'auto' or is_pure:
         return USER_SHA_TO_ALG_AND_TLV[allowed[0]]
 
     if user_sha in allowed:
@@ -447,12 +459,13 @@
     def create(self, key, public_key_format, enckey, dependencies=None,
                sw_type=None, custom_tlvs=None, compression_tlvs=None,
                compression_type=None, encrypt_keylen=128, clear=False,
-               fixed_sig=None, pub_key=None, vector_to_sign=None, user_sha='auto'):
+               fixed_sig=None, pub_key=None, vector_to_sign=None,
+               user_sha='auto', is_pure=False):
         self.enckey = enckey
 
         # key decides on sha, then pub_key; of both are none default is used
         check_key = key if key is not None else pub_key
-        hash_algorithm, hash_tlv = key_and_user_sha_to_alg_and_tlv(check_key, user_sha)
+        hash_algorithm, hash_tlv = key_and_user_sha_to_alg_and_tlv(check_key, user_sha, is_pure)
 
         # Calculate the hash of the public key
         if key is not None:
@@ -592,9 +605,17 @@
         sha = hash_algorithm()
         sha.update(self.payload)
         digest = sha.digest()
-        message = digest;
         tlv.add(hash_tlv, digest)
         self.image_hash = digest
+        # Unless pure, we are signing digest.
+        message = digest
+
+        if is_pure:
+            # Note that when Pure signature is used, hash TLV is not present.
+            message = bytes(self.payload)
+            e = STRUCT_ENDIAN_DICT[self.endian]
+            sig_pure = struct.pack(e + '?', True)
+            tlv.add('SIG_PURE', sig_pure)
 
         if vector_to_sign == 'payload':
             # Stop amending data to the image
@@ -786,7 +807,7 @@
         version = struct.unpack('BBHI', b[20:28])
 
         if magic != IMAGE_MAGIC:
-            return VerifyResult.INVALID_MAGIC, None, None
+            return VerifyResult.INVALID_MAGIC, None, None, None
 
         tlv_off = header_size + img_size
         tlv_info = b[tlv_off:tlv_off + TLV_INFO_SIZE]
@@ -797,11 +818,27 @@
             magic, tlv_tot = struct.unpack('HH', tlv_info)
 
         if magic != TLV_INFO_MAGIC:
-            return VerifyResult.INVALID_TLV_INFO_MAGIC, None, None
+            return VerifyResult.INVALID_TLV_INFO_MAGIC, None, None, None
+
+        # This is set by existence of TLV SIG_PURE
+        is_pure = False
 
         prot_tlv_size = tlv_off
         hash_region = b[:prot_tlv_size]
+        tlv_end = tlv_off + tlv_tot
+        tlv_off += TLV_INFO_SIZE  # skip tlv info
+
+        # First scan all TLVs in search of SIG_PURE
+        while tlv_off < tlv_end:
+            tlv = b[tlv_off:tlv_off + TLV_SIZE]
+            tlv_type, _, tlv_len = struct.unpack('BBH', tlv)
+            if tlv_type == TLV_VALUES['SIG_PURE']:
+                is_pure = True
+                break
+            tlv_off += TLV_SIZE + tlv_len
+
         digest = None
+        tlv_off = header_size + img_size
         tlv_end = tlv_off + tlv_tot
         tlv_off += TLV_INFO_SIZE  # skip tlv info
         while tlv_off < tlv_end:
@@ -809,15 +846,15 @@
             tlv_type, _, tlv_len = struct.unpack('BBH', tlv)
             if is_sha_tlv(tlv_type):
                 if not tlv_matches_key_type(tlv_type, key):
-                    return VerifyResult.KEY_MISMATCH, None, None
+                    return VerifyResult.KEY_MISMATCH, None, None, None
                 off = tlv_off + TLV_SIZE
                 digest = get_digest(tlv_type, hash_region)
                 if digest == b[off:off + tlv_len]:
                     if key is None:
-                        return VerifyResult.OK, version, digest
+                        return VerifyResult.OK, version, digest, None
                 else:
-                    return VerifyResult.INVALID_HASH, None, None
-            elif key is not None and tlv_type == TLV_VALUES[key.sig_tlv()]:
+                    return VerifyResult.INVALID_HASH, None, None, None
+            elif not is_pure and key is not None and tlv_type == TLV_VALUES[key.sig_tlv()]:
                 off = tlv_off + TLV_SIZE
                 tlv_sig = b[off:off + tlv_len]
                 payload = b[:prot_tlv_size]
@@ -826,9 +863,18 @@
                         key.verify(tlv_sig, payload)
                     else:
                         key.verify_digest(tlv_sig, digest)
-                    return VerifyResult.OK, version, digest
+                    return VerifyResult.OK, version, digest, None
+                except InvalidSignature:
+                    # continue to next TLV
+                    pass
+            elif is_pure and key is not None and tlv_type in ALLOWED_PURE_SIG_TLVS:
+                off = tlv_off + TLV_SIZE
+                tlv_sig = b[off:off + tlv_len]
+                try:
+                    key.verify_digest(tlv_sig, hash_region)
+                    return VerifyResult.OK, version, None, tlv_sig
                 except InvalidSignature:
                     # continue to next TLV
                     pass
             tlv_off += TLV_SIZE + tlv_len
-        return VerifyResult.INVALID_SIGNATURE, None, None
+        return VerifyResult.INVALID_SIGNATURE, None, None, None
diff --git a/scripts/imgtool/main.py b/scripts/imgtool/main.py
index e0f7094..434530c 100755
--- a/scripts/imgtool/main.py
+++ b/scripts/imgtool/main.py
@@ -226,11 +226,14 @@
 @click.command(help="Check that signed image can be verified by given key")
 def verify(key, imgfile):
     key = load_key(key) if key else None
-    ret, version, digest = image.Image.verify(imgfile, key)
+    ret, version, digest, signature = image.Image.verify(imgfile, key)
     if ret == image.VerifyResult.OK:
         print("Image was correctly validated")
         print("Image version: {}.{}.{}+{}".format(*version))
-        print("Image digest: {}".format(digest.hex()))
+        if digest:
+            print("Image digest: {}".format(digest.hex()))
+        if signature and digest is None:
+            print("Image signature over image: {}".format(signature.hex()))
         return
     elif ret == image.VerifyResult.INVALID_MAGIC:
         print("Invalid image magic; is this an MCUboot image?")
@@ -423,6 +426,10 @@
               'the signature calculated using the public key')
 @click.option('--fix-sig-pubkey', metavar='filename',
               help='public key relevant to fixed signature')
+@click.option('--pure', 'is_pure', is_flag=True, default=False, show_default=True,
+              help='Expected Pure variant of signature; the Pure variant is '
+              'expected to be signature done over an image rather than hash of '
+              'that image.')
 @click.option('--sig-out', metavar='filename',
               help='Path to the file to which signature will be written. '
               'The image signature will be encoded as base64 formatted string')
@@ -441,8 +448,8 @@
          endian, encrypt_keylen, encrypt, compression, infile, outfile,
          dependencies, load_addr, hex_addr, erased_val, save_enctlv,
          security_counter, boot_record, custom_tlv, rom_fixed, max_align,
-         clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, vector_to_sign,
-         non_bootable):
+         clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, is_pure,
+         vector_to_sign, non_bootable):
 
     if confirm:
         # Confirmed but non-padded images don't make much sense, because
@@ -509,9 +516,15 @@
             'value': raw_signature
         }
 
+    if is_pure and user_sha != 'auto':
+        raise click.UsageError(
+            'Pure signatures, currently, enforces preferred hash algorithm, '
+            'and forbids sha selection by user.')
+
     img.create(key, public_key_format, enckey, dependencies, boot_record,
                custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear,
-               baked_signature, pub_key, vector_to_sign, user_sha)
+               baked_signature, pub_key, vector_to_sign, user_sha=user_sha,
+               is_pure=is_pure)
 
     if compression in ["lzma2", "lzma2armthumb"]:
         compressed_img = image.Image(version=decode_version(version),
@@ -552,7 +565,8 @@
             compressed_img.create(key, public_key_format, enckey,
                dependencies, boot_record, custom_tlvs, compression_tlvs,
                compression, int(encrypt_keylen), clear, baked_signature,
-               pub_key, vector_to_sign, user_sha=user_sha)
+               pub_key, vector_to_sign, user_sha=user_sha,
+               is_pure=is_pure)
             img = compressed_img
     img.save(outfile, hex_addr)
     if sig_out is not None: