Add new verify command

imgtool verify -k <some-key.(pub|sec)> <img-file>

Allow imgtool to validate that an image has a valid sha256sum and that
it was signed by the supplied key.

NOTE: this does not yet support verifying encrypted images

Signed-off-by: Fabio Utzig <utzig@apache.org>
diff --git a/scripts/imgtool/image.py b/scripts/imgtool/image.py
index ad156a1..20f8e75 100644
--- a/scripts/imgtool/image.py
+++ b/scripts/imgtool/image.py
@@ -19,6 +19,7 @@
 """
 
 from . import version as versmod
+from enum import Enum
 from intelhex import IntelHex
 import hashlib
 import struct
@@ -27,6 +28,7 @@
 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import hashes
+from cryptography.exceptions import InvalidSignature
 
 IMAGE_MAGIC = 0x96f3b83d
 IMAGE_HEADER_SIZE = 32
@@ -55,6 +57,7 @@
         'DEPENDENCY': 0x40
 }
 
+TLV_SIZE = 4
 TLV_INFO_SIZE = 4
 TLV_INFO_MAGIC = 0x6907
 
@@ -69,6 +72,13 @@
         'big':    '>'
 }
 
+VerifyResult = Enum('VerifyResult',
+                    """
+                    OK INVALID_MAGIC INVALID_TLV_INFO_MAGIC INVALID_HASH
+                    INVALID_SIGNATURE
+                    """)
+
+
 class TLV():
     def __init__(self, endian):
         self.buf = bytearray()
@@ -307,3 +317,47 @@
         pbytes += b'\xff' * (tsize - len(boot_magic))
         pbytes += boot_magic
         self.payload += pbytes
+
+    @staticmethod
+    def verify(imgfile, key):
+        with open(imgfile, "rb") as f:
+            b = f.read()
+
+        magic, _, header_size, _, img_size = struct.unpack('IIHHI', b[:16])
+        if magic != IMAGE_MAGIC:
+            return VerifyResult.INVALID_MAGIC
+
+        tlv_info = b[header_size+img_size:header_size+img_size+TLV_INFO_SIZE]
+        magic, tlv_tot = struct.unpack('HH', tlv_info)
+        if magic != TLV_INFO_MAGIC:
+            return VerifyResult.INVALID_TLV_INFO_MAGIC
+
+        sha = hashlib.sha256()
+        sha.update(b[:header_size+img_size])
+        digest = sha.digest()
+
+        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:
+            tlv = b[tlv_off:tlv_off+TLV_SIZE]
+            tlv_type, _, tlv_len = struct.unpack('BBH', tlv)
+            if tlv_type == TLV_VALUES["SHA256"]:
+                off = tlv_off + TLV_SIZE
+                if digest == b[off:off+tlv_len]:
+                    if key is None:
+                        return VerifyResult.OK
+                else:
+                    return VerifyResult.INVALID_HASH
+            elif 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[:header_size+img_size]
+                try:
+                    key.verify(tlv_sig, payload)
+                    return VerifyResult.OK
+                except InvalidSignature:
+                    # continue to next TLV
+                    pass
+            tlv_off += TLV_SIZE + tlv_len
+        return VerifyResult.INVALID_SIGNATURE
diff --git a/scripts/imgtool/keys/ecdsa.py b/scripts/imgtool/keys/ecdsa.py
index f541d16..f93783d 100644
--- a/scripts/imgtool/keys/ecdsa.py
+++ b/scripts/imgtool/keys/ecdsa.py
@@ -57,6 +57,14 @@
         # signature.
         return 72
 
+    def verify(self, signature, payload):
+        k = self.key
+        if isinstance(self.key, ec.EllipticCurvePrivateKey):
+            k = self.key.public_key()
+        return k.verify(signature=signature, data=payload,
+                        signature_algorithm=ec.ECDSA(SHA256()))
+
+
 class ECDSA256P1(ECDSA256P1Public):
     """
     Wrapper around an ECDSA private key.
diff --git a/scripts/imgtool/keys/rsa.py b/scripts/imgtool/keys/rsa.py
index 94af064..0f9a905 100644
--- a/scripts/imgtool/keys/rsa.py
+++ b/scripts/imgtool/keys/rsa.py
@@ -62,6 +62,14 @@
     def sig_len(self):
         return self.key_size() / 8
 
+    def verify(self, signature, payload):
+        k = self.key
+        if isinstance(self.key, rsa.RSAPrivateKey):
+            k = self.key.public_key()
+        return k.verify(signature=signature, data=payload,
+                        padding=PSS(mgf=MGF1(SHA256()), salt_length=32),
+                        algorithm=SHA256())
+
 
 class RSA(RSAPublic):
     """
diff --git a/scripts/imgtool/main.py b/scripts/imgtool/main.py
index cb204b0..476884c 100755
--- a/scripts/imgtool/main.py
+++ b/scripts/imgtool/main.py
@@ -19,6 +19,7 @@
 import click
 import getpass
 import imgtool.keys as keys
+import sys
 from imgtool import image
 from imgtool.version import decode_version
 
@@ -98,6 +99,26 @@
         raise ValueError("BUG: should never get here!")
 
 
+@click.argument('imgfile')
+@click.option('-k', '--key', metavar='filename')
+@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 = image.Image.verify(imgfile, key)
+    if ret == image.VerifyResult.OK:
+        print("Image was correctly validated")
+        return
+    elif ret == image.VerifyResult.INVALID_MAGIC:
+        print("Invalid image magic; is this an MCUboot image?")
+    elif ret == image.VerifyResult.INVALID_MAGIC:
+        print("Invalid TLV info magic; is this an MCUboot image?")
+    elif ret == image.VerifyResult.INVALID_HASH:
+        print("Image has an invalid sha256 digest")
+    elif ret == image.VerifyResult.INVALID_SIGNATURE:
+        print("No signature found for the given key")
+    sys.exit(1)
+
+
 def validate_version(ctx, param, value):
     try:
         decode_version(value)
@@ -226,6 +247,7 @@
 
 imgtool.add_command(keygen)
 imgtool.add_command(getpub)
+imgtool.add_command(verify)
 imgtool.add_command(sign)