1# Copyright 2018 Nordic Semiconductor ASA
2# Copyright 2017-2020 Linaro Limited
3# Copyright 2019-2024 Arm Limited
4#
5# SPDX-License-Identifier: Apache-2.0
6#
7# Licensed under the Apache License, Version 2.0 (the "License");
8# you may not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11#     http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS,
15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18
19"""
20Image signing and management.
21"""
22
23import hashlib
24import os.path
25import struct
26from enum import Enum
27
28import click
29from cryptography.exceptions import InvalidSignature
30from cryptography.hazmat.backends import default_backend
31from cryptography.hazmat.primitives import hashes, hmac
32from cryptography.hazmat.primitives.asymmetric import ec, padding
33from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
34from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
35from cryptography.hazmat.primitives.kdf.hkdf import HKDF
36from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
37from intelhex import IntelHex
38
39from . import version as versmod, keys
40from .boot_record import create_sw_component_data
41from .keys import rsa, ecdsa, x25519
42
43IMAGE_MAGIC = 0x96f3b83d
44IMAGE_HEADER_SIZE = 32
45BIN_EXT = "bin"
46INTEL_HEX_EXT = "hex"
47DEFAULT_MAX_SECTORS = 128
48DEFAULT_MAX_ALIGN = 8
49DEP_IMAGES_KEY = "images"
50DEP_VERSIONS_KEY = "versions"
51MAX_SW_TYPE_LENGTH = 12  # Bytes
52
53# Image header flags.
54IMAGE_F = {
55        'PIC':                   0x0000001,
56        'ENCRYPTED_AES128':      0x0000004,
57        'ENCRYPTED_AES256':      0x0000008,
58        'NON_BOOTABLE':          0x0000010,
59        'RAM_LOAD':              0x0000020,
60        'ROM_FIXED':             0x0000100,
61}
62
63TLV_VALUES = {
64        'KEYHASH': 0x01,
65        'PUBKEY': 0x02,
66        'SHA256': 0x10,
67        'SHA384': 0x11,
68        'RSA2048': 0x20,
69        'ECDSASIG': 0x22,
70        'RSA3072': 0x23,
71        'ED25519': 0x24,
72        'ENCRSA2048': 0x30,
73        'ENCKW': 0x31,
74        'ENCEC256': 0x32,
75        'ENCX25519': 0x33,
76        'DEPENDENCY': 0x40,
77        'SEC_CNT': 0x50,
78        'BOOT_RECORD': 0x60,
79}
80
81TLV_SIZE = 4
82TLV_INFO_SIZE = 4
83TLV_INFO_MAGIC = 0x6907
84TLV_PROT_INFO_MAGIC = 0x6908
85
86TLV_VENDOR_RES_MIN = 0x00a0
87TLV_VENDOR_RES_MAX = 0xfffe
88
89STRUCT_ENDIAN_DICT = {
90        'little': '<',
91        'big':    '>'
92}
93
94VerifyResult = Enum('VerifyResult',
95                    ['OK', 'INVALID_MAGIC', 'INVALID_TLV_INFO_MAGIC', 'INVALID_HASH', 'INVALID_SIGNATURE',
96                     'KEY_MISMATCH'])
97
98
99def align_up(num, align):
100    assert (align & (align - 1) == 0) and align != 0
101    return (num + (align - 1)) & ~(align - 1)
102
103
104class TLV():
105    def __init__(self, endian, magic=TLV_INFO_MAGIC):
106        self.magic = magic
107        self.buf = bytearray()
108        self.endian = endian
109
110    def __len__(self):
111        return TLV_INFO_SIZE + len(self.buf)
112
113    def add(self, kind, payload):
114        """
115        Add a TLV record.  Kind should be a string found in TLV_VALUES above.
116        """
117        e = STRUCT_ENDIAN_DICT[self.endian]
118        if isinstance(kind, int):
119            if not TLV_VENDOR_RES_MIN <= kind <= TLV_VENDOR_RES_MAX:
120                msg = "Invalid custom TLV type value '0x{:04x}', allowed " \
121                      "value should be between 0x{:04x} and 0x{:04x}".format(
122                        kind, TLV_VENDOR_RES_MIN, TLV_VENDOR_RES_MAX)
123                raise click.UsageError(msg)
124            buf = struct.pack(e + 'HH', kind, len(payload))
125        else:
126            buf = struct.pack(e + 'BBH', TLV_VALUES[kind], 0, len(payload))
127        self.buf += buf
128        self.buf += payload
129
130    def get(self):
131        if len(self.buf) == 0:
132            return bytes()
133        e = STRUCT_ENDIAN_DICT[self.endian]
134        header = struct.pack(e + 'HH', self.magic, len(self))
135        return header + bytes(self.buf)
136
137
138def get_digest(tlv_type, hash_region):
139    if tlv_type == TLV_VALUES["SHA384"]:
140        sha = hashlib.sha384()
141    elif tlv_type == TLV_VALUES["SHA256"]:
142        sha = hashlib.sha256()
143
144    sha.update(hash_region)
145    return sha.digest()
146
147
148def tlv_matches_key_type(tlv_type, key):
149    """Check if provided key matches to TLV record in the image"""
150    return (key is None or
151            type(key) == keys.ECDSA384P1 and tlv_type == TLV_VALUES["SHA384"] or
152            type(key) != keys.ECDSA384P1 and tlv_type == TLV_VALUES["SHA256"])
153
154
155class Image:
156
157    def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE,
158                 pad_header=False, pad=False, confirm=False, align=1,
159                 slot_size=0, max_sectors=DEFAULT_MAX_SECTORS,
160                 overwrite_only=False, endian="little", load_addr=0,
161                 rom_fixed=None, erased_val=None, save_enctlv=False,
162                 security_counter=None, max_align=None):
163
164        if load_addr and rom_fixed:
165            raise click.UsageError("Can not set rom_fixed and load_addr at the same time")
166
167        self.version = version or versmod.decode_version("0")
168        self.header_size = header_size
169        self.pad_header = pad_header
170        self.pad = pad
171        self.confirm = confirm
172        self.align = align
173        self.slot_size = slot_size
174        self.max_sectors = max_sectors
175        self.overwrite_only = overwrite_only
176        self.endian = endian
177        self.base_addr = None
178        self.load_addr = 0 if load_addr is None else load_addr
179        self.rom_fixed = rom_fixed
180        self.erased_val = 0xff if erased_val is None else int(erased_val, 0)
181        self.payload = []
182        self.enckey = None
183        self.save_enctlv = save_enctlv
184        self.enctlv_len = 0
185        self.max_align = max(DEFAULT_MAX_ALIGN, align) if max_align is None else int(max_align)
186
187        if self.max_align == DEFAULT_MAX_ALIGN:
188            self.boot_magic = bytes([
189                0x77, 0xc2, 0x95, 0xf3,
190                0x60, 0xd2, 0xef, 0x7f,
191                0x35, 0x52, 0x50, 0x0f,
192                0x2c, 0xb6, 0x79, 0x80, ])
193        else:
194            lsb = self.max_align & 0x00ff
195            msb = (self.max_align & 0xff00) >> 8
196            align = bytes([msb, lsb]) if self.endian == "big" else bytes([lsb, msb])
197            self.boot_magic = align + bytes([0x2d, 0xe1,
198                                             0x5d, 0x29, 0x41, 0x0b,
199                                             0x8d, 0x77, 0x67, 0x9c,
200                                             0x11, 0x0f, 0x1f, 0x8a, ])
201
202        if security_counter == 'auto':
203            # Security counter has not been explicitly provided,
204            # generate it from the version number
205            self.security_counter = ((self.version.major << 24)
206                                     + (self.version.minor << 16)
207                                     + self.version.revision)
208        else:
209            self.security_counter = security_counter
210
211    def __repr__(self):
212        return "<Image version={}, header_size={}, security_counter={}, \
213                base_addr={}, load_addr={}, align={}, slot_size={}, \
214                max_sectors={}, overwrite_only={}, endian={} format={}, \
215                payloadlen=0x{:x}>".format(
216                    self.version,
217                    self.header_size,
218                    self.security_counter,
219                    self.base_addr if self.base_addr is not None else "N/A",
220                    self.load_addr,
221                    self.align,
222                    self.slot_size,
223                    self.max_sectors,
224                    self.overwrite_only,
225                    self.endian,
226                    self.__class__.__name__,
227                    len(self.payload))
228
229    def load(self, path):
230        """Load an image from a given file"""
231        ext = os.path.splitext(path)[1][1:].lower()
232        try:
233            if ext == INTEL_HEX_EXT:
234                ih = IntelHex(path)
235                self.payload = ih.tobinarray()
236                self.base_addr = ih.minaddr()
237            else:
238                with open(path, 'rb') as f:
239                    self.payload = f.read()
240        except FileNotFoundError:
241            raise click.UsageError("Input file not found")
242
243        # Add the image header if needed.
244        if self.pad_header and self.header_size > 0:
245            if self.base_addr:
246                # Adjust base_addr for new header
247                self.base_addr -= self.header_size
248            self.payload = bytes([self.erased_val] * self.header_size) + \
249                self.payload
250
251        self.check_header()
252
253    def save(self, path, hex_addr=None):
254        """Save an image from a given file"""
255        ext = os.path.splitext(path)[1][1:].lower()
256        if ext == INTEL_HEX_EXT:
257            # input was in binary format, but HEX needs to know the base addr
258            if self.base_addr is None and hex_addr is None:
259                raise click.UsageError("No address exists in input file "
260                                       "neither was it provided by user")
261            h = IntelHex()
262            if hex_addr is not None:
263                self.base_addr = hex_addr
264            h.frombytes(bytes=self.payload, offset=self.base_addr)
265            if self.pad:
266                trailer_size = self._trailer_size(self.align, self.max_sectors,
267                                                  self.overwrite_only,
268                                                  self.enckey,
269                                                  self.save_enctlv,
270                                                  self.enctlv_len)
271                trailer_addr = (self.base_addr + self.slot_size) - trailer_size
272                if self.confirm and not self.overwrite_only:
273                    magic_align_size = align_up(len(self.boot_magic),
274                                                self.max_align)
275                    image_ok_idx = -(magic_align_size + self.max_align)
276                    flag = bytearray([self.erased_val] * self.max_align)
277                    flag[0] = 0x01  # image_ok = 0x01
278                    h.puts(trailer_addr + trailer_size + image_ok_idx,
279                           bytes(flag))
280                h.puts(trailer_addr + (trailer_size - len(self.boot_magic)),
281                       bytes(self.boot_magic))
282            h.tofile(path, 'hex')
283        else:
284            if self.pad:
285                self.pad_to(self.slot_size)
286            with open(path, 'wb') as f:
287                f.write(self.payload)
288
289    def check_header(self):
290        if self.header_size > 0 and not self.pad_header:
291            if any(v != 0 for v in self.payload[0:self.header_size]):
292                raise click.UsageError("Header padding was not requested and "
293                                       "image does not start with zeros")
294
295    def check_trailer(self):
296        if self.slot_size > 0:
297            tsize = self._trailer_size(self.align, self.max_sectors,
298                                       self.overwrite_only, self.enckey,
299                                       self.save_enctlv, self.enctlv_len)
300            padding = self.slot_size - (len(self.payload) + tsize)
301            if padding < 0:
302                msg = "Image size (0x{:x}) + trailer (0x{:x}) exceeds " \
303                      "requested size 0x{:x}".format(
304                          len(self.payload), tsize, self.slot_size)
305                raise click.UsageError(msg)
306
307    def ecies_hkdf(self, enckey, plainkey):
308        if isinstance(enckey, ecdsa.ECDSA256P1Public):
309            newpk = ec.generate_private_key(ec.SECP256R1(), default_backend())
310            shared = newpk.exchange(ec.ECDH(), enckey._get_public())
311        else:
312            newpk = X25519PrivateKey.generate()
313            shared = newpk.exchange(enckey._get_public())
314        derived_key = HKDF(
315            algorithm=hashes.SHA256(), length=48, salt=None,
316            info=b'MCUBoot_ECIES_v1', backend=default_backend()).derive(shared)
317        encryptor = Cipher(algorithms.AES(derived_key[:16]),
318                           modes.CTR(bytes([0] * 16)),
319                           backend=default_backend()).encryptor()
320        cipherkey = encryptor.update(plainkey) + encryptor.finalize()
321        mac = hmac.HMAC(derived_key[16:], hashes.SHA256(),
322                        backend=default_backend())
323        mac.update(cipherkey)
324        ciphermac = mac.finalize()
325        if isinstance(enckey, ecdsa.ECDSA256P1Public):
326            pubk = newpk.public_key().public_bytes(
327                encoding=Encoding.X962,
328                format=PublicFormat.UncompressedPoint)
329        else:
330            pubk = newpk.public_key().public_bytes(
331                encoding=Encoding.Raw,
332                format=PublicFormat.Raw)
333        return cipherkey, ciphermac, pubk
334
335    def create(self, key, public_key_format, enckey, dependencies=None,
336               sw_type=None, custom_tlvs=None, encrypt_keylen=128, clear=False,
337               fixed_sig=None, pub_key=None, vector_to_sign=None):
338        self.enckey = enckey
339
340        # Check what hashing algorithm should be used
341        if (key and isinstance(key, ecdsa.ECDSA384P1)
342                or pub_key and isinstance(pub_key, ecdsa.ECDSA384P1Public)):
343            hash_algorithm = hashlib.sha384
344            hash_tlv = "SHA384"
345        else:
346            hash_algorithm = hashlib.sha256
347            hash_tlv = "SHA256"
348        # Calculate the hash of the public key
349        if key is not None:
350            pub = key.get_public_bytes()
351            sha = hash_algorithm()
352            sha.update(pub)
353            pubbytes = sha.digest()
354        elif pub_key is not None:
355            if hasattr(pub_key, 'sign'):
356                print(os.path.basename(__file__) + ": sign the payload")
357            pub = pub_key.get_public_bytes()
358            sha = hash_algorithm()
359            sha.update(pub)
360            pubbytes = sha.digest()
361        else:
362            pubbytes = bytes(hashlib.sha256().digest_size)
363
364        protected_tlv_size = 0
365
366        if self.security_counter is not None:
367            # Size of the security counter TLV: header ('HH') + payload ('I')
368            #                                   = 4 + 4 = 8 Bytes
369            protected_tlv_size += TLV_SIZE + 4
370
371        if sw_type is not None:
372            if len(sw_type) > MAX_SW_TYPE_LENGTH:
373                msg = "'{}' is too long ({} characters) for sw_type. Its " \
374                      "maximum allowed length is 12 characters.".format(
375                       sw_type, len(sw_type))
376                raise click.UsageError(msg)
377
378            image_version = (str(self.version.major) + '.'
379                             + str(self.version.minor) + '.'
380                             + str(self.version.revision))
381
382            # The image hash is computed over the image header, the image
383            # itself and the protected TLV area. However, the boot record TLV
384            # (which is part of the protected area) should contain this hash
385            # before it is even calculated. For this reason the script fills
386            # this field with zeros and the bootloader will insert the right
387            # value later.
388            digest = bytes(hash_algorithm().digest_size)
389
390            # Create CBOR encoded boot record
391            boot_record = create_sw_component_data(sw_type, image_version,
392                                                   hash_tlv, digest,
393                                                   pubbytes)
394
395            protected_tlv_size += TLV_SIZE + len(boot_record)
396
397        if dependencies is not None:
398            # Size of a Dependency TLV = Header ('HH') + Payload('IBBHI')
399            # = 4 + 12 = 16 Bytes
400            dependencies_num = len(dependencies[DEP_IMAGES_KEY])
401            protected_tlv_size += (dependencies_num * 16)
402
403        if custom_tlvs is not None:
404            for value in custom_tlvs.values():
405                protected_tlv_size += TLV_SIZE + len(value)
406
407        if protected_tlv_size != 0:
408            # Add the size of the TLV info header
409            protected_tlv_size += TLV_INFO_SIZE
410
411        # At this point the image is already on the payload
412        #
413        # This adds the padding if image is not aligned to the 16 Bytes
414        # in encrypted mode
415        if self.enckey is not None:
416            pad_len = len(self.payload) % 16
417            if pad_len > 0:
418                pad = bytes(16 - pad_len)
419                if isinstance(self.payload, bytes):
420                    self.payload += pad
421                else:
422                    self.payload.extend(pad)
423
424        # This adds the header to the payload as well
425        if encrypt_keylen == 256:
426            self.add_header(enckey, protected_tlv_size, 256)
427        else:
428            self.add_header(enckey, protected_tlv_size)
429
430        prot_tlv = TLV(self.endian, TLV_PROT_INFO_MAGIC)
431
432        # Protected TLVs must be added first, because they are also included
433        # in the hash calculation
434        protected_tlv_off = None
435        if protected_tlv_size != 0:
436
437            e = STRUCT_ENDIAN_DICT[self.endian]
438
439            if self.security_counter is not None:
440                payload = struct.pack(e + 'I', self.security_counter)
441                prot_tlv.add('SEC_CNT', payload)
442
443            if sw_type is not None:
444                prot_tlv.add('BOOT_RECORD', boot_record)
445
446            if dependencies is not None:
447                for i in range(dependencies_num):
448                    payload = struct.pack(
449                        e + 'B3x' + 'BBHI',
450                        int(dependencies[DEP_IMAGES_KEY][i]),
451                        dependencies[DEP_VERSIONS_KEY][i].major,
452                        dependencies[DEP_VERSIONS_KEY][i].minor,
453                        dependencies[DEP_VERSIONS_KEY][i].revision,
454                        dependencies[DEP_VERSIONS_KEY][i].build
455                    )
456                    prot_tlv.add('DEPENDENCY', payload)
457
458            if custom_tlvs is not None:
459                for tag, value in custom_tlvs.items():
460                    prot_tlv.add(tag, value)
461
462            protected_tlv_off = len(self.payload)
463            self.payload += prot_tlv.get()
464
465        tlv = TLV(self.endian)
466
467        # Note that ecdsa wants to do the hashing itself, which means
468        # we get to hash it twice.
469        sha = hash_algorithm()
470        sha.update(self.payload)
471        digest = sha.digest()
472        tlv.add(hash_tlv, digest)
473
474        if vector_to_sign == 'payload':
475            # Stop amending data to the image
476            # Just keep data vector which is expected to be signed
477            print(os.path.basename(__file__) + ': export payload')
478            return
479        elif vector_to_sign == 'digest':
480            self.payload = digest
481            print(os.path.basename(__file__) + ': export digest')
482            return
483
484        if key is not None or fixed_sig is not None:
485            if public_key_format == 'hash':
486                tlv.add('KEYHASH', pubbytes)
487            else:
488                tlv.add('PUBKEY', pub)
489
490            if key is not None and fixed_sig is None:
491                # `sign` expects the full image payload (hashing done
492                # internally), while `sign_digest` expects only the digest
493                # of the payload
494
495                if hasattr(key, 'sign'):
496                    print(os.path.basename(__file__) + ": sign the payload")
497                    sig = key.sign(bytes(self.payload))
498                else:
499                    print(os.path.basename(__file__) + ": sign the digest")
500                    sig = key.sign_digest(digest)
501                tlv.add(key.sig_tlv(), sig)
502                self.signature = sig
503            elif fixed_sig is not None and key is None:
504                tlv.add(pub_key.sig_tlv(), fixed_sig['value'])
505                self.signature = fixed_sig['value']
506            else:
507                raise click.UsageError("Can not sign using key and provide fixed-signature at the same time")
508
509        # At this point the image was hashed + signed, we can remove the
510        # protected TLVs from the payload (will be re-added later)
511        if protected_tlv_off is not None:
512            self.payload = self.payload[:protected_tlv_off]
513
514        if enckey is not None:
515            if encrypt_keylen == 256:
516                plainkey = os.urandom(32)
517            else:
518                plainkey = os.urandom(16)
519
520            if isinstance(enckey, rsa.RSAPublic):
521                cipherkey = enckey._get_public().encrypt(
522                    plainkey, padding.OAEP(
523                        mgf=padding.MGF1(algorithm=hashes.SHA256()),
524                        algorithm=hashes.SHA256(),
525                        label=None))
526                self.enctlv_len = len(cipherkey)
527                tlv.add('ENCRSA2048', cipherkey)
528            elif isinstance(enckey, (ecdsa.ECDSA256P1Public,
529                                     x25519.X25519Public)):
530                cipherkey, mac, pubk = self.ecies_hkdf(enckey, plainkey)
531                enctlv = pubk + mac + cipherkey
532                self.enctlv_len = len(enctlv)
533                if isinstance(enckey, ecdsa.ECDSA256P1Public):
534                    tlv.add('ENCEC256', enctlv)
535                else:
536                    tlv.add('ENCX25519', enctlv)
537
538            if not clear:
539                nonce = bytes([0] * 16)
540                cipher = Cipher(algorithms.AES(plainkey), modes.CTR(nonce),
541                                backend=default_backend())
542                encryptor = cipher.encryptor()
543                img = bytes(self.payload[self.header_size:])
544                self.payload[self.header_size:] = \
545                    encryptor.update(img) + encryptor.finalize()
546
547        self.payload += prot_tlv.get()
548        self.payload += tlv.get()
549
550        self.check_trailer()
551
552    def get_signature(self):
553        return self.signature
554
555    def add_header(self, enckey, protected_tlv_size, aes_length=128):
556        """Install the image header."""
557
558        flags = 0
559        if enckey is not None:
560            if aes_length == 128:
561                flags |= IMAGE_F['ENCRYPTED_AES128']
562            else:
563                flags |= IMAGE_F['ENCRYPTED_AES256']
564        if self.load_addr != 0:
565            # Indicates that this image should be loaded into RAM
566            # instead of run directly from flash.
567            flags |= IMAGE_F['RAM_LOAD']
568        if self.rom_fixed:
569            flags |= IMAGE_F['ROM_FIXED']
570
571        e = STRUCT_ENDIAN_DICT[self.endian]
572        fmt = (e +
573               # type ImageHdr struct {
574               'I' +     # Magic    uint32
575               'I' +     # LoadAddr uint32
576               'H' +     # HdrSz    uint16
577               'H' +     # PTLVSz   uint16
578               'I' +     # ImgSz    uint32
579               'I' +     # Flags    uint32
580               'BBHI' +  # Vers     ImageVersion
581               'I'       # Pad1     uint32
582               )  # }
583        assert struct.calcsize(fmt) == IMAGE_HEADER_SIZE
584        header = struct.pack(fmt,
585                             IMAGE_MAGIC,
586                             self.rom_fixed or self.load_addr,
587                             self.header_size,
588                             protected_tlv_size,  # TLV Info header +
589                                                  # Protected TLVs
590                             len(self.payload) - self.header_size,  # ImageSz
591                             flags,
592                             self.version.major,
593                             self.version.minor or 0,
594                             self.version.revision or 0,
595                             self.version.build or 0,
596                             0)  # Pad1
597        self.payload = bytearray(self.payload)
598        self.payload[:len(header)] = header
599
600    def _trailer_size(self, write_size, max_sectors, overwrite_only, enckey,
601                      save_enctlv, enctlv_len):
602        # NOTE: should already be checked by the argument parser
603        magic_size = 16
604        magic_align_size = align_up(magic_size, self.max_align)
605        if overwrite_only:
606            return self.max_align * 2 + magic_align_size
607        else:
608            if write_size not in set([1, 2, 4, 8, 16, 32]):
609                raise click.BadParameter("Invalid alignment: {}".format(
610                    write_size))
611            m = DEFAULT_MAX_SECTORS if max_sectors is None else max_sectors
612            trailer = m * 3 * write_size  # status area
613            if enckey is not None:
614                if save_enctlv:
615                    # TLV saved by the bootloader is aligned
616                    keylen = align_up(enctlv_len, self.max_align)
617                else:
618                    keylen = align_up(16, self.max_align)
619                trailer += keylen * 2  # encryption keys
620            trailer += self.max_align * 4  # image_ok/copy_done/swap_info/swap_size
621            trailer += magic_align_size
622            return trailer
623
624    def pad_to(self, size):
625        """Pad the image to the given size, with the given flash alignment."""
626        tsize = self._trailer_size(self.align, self.max_sectors,
627                                   self.overwrite_only, self.enckey,
628                                   self.save_enctlv, self.enctlv_len)
629        padding = size - (len(self.payload) + tsize)
630        pbytes = bytearray([self.erased_val] * padding)
631        pbytes += bytearray([self.erased_val] * (tsize - len(self.boot_magic)))
632        pbytes += self.boot_magic
633        if self.confirm and not self.overwrite_only:
634            magic_size = 16
635            magic_align_size = align_up(magic_size, self.max_align)
636            image_ok_idx = -(magic_align_size + self.max_align)
637            pbytes[image_ok_idx] = 0x01  # image_ok = 0x01
638        self.payload += pbytes
639
640    @staticmethod
641    def verify(imgfile, key):
642        ext = os.path.splitext(imgfile)[1][1:].lower()
643        try:
644            if ext == INTEL_HEX_EXT:
645                b = IntelHex(imgfile).tobinstr()
646            else:
647                with open(imgfile, 'rb') as f:
648                    b = f.read()
649        except FileNotFoundError:
650            raise click.UsageError(f"Image file {imgfile} not found")
651
652        magic, _, header_size, _, img_size = struct.unpack('IIHHI', b[:16])
653        version = struct.unpack('BBHI', b[20:28])
654
655        if magic != IMAGE_MAGIC:
656            return VerifyResult.INVALID_MAGIC, None, None
657
658        tlv_off = header_size + img_size
659        tlv_info = b[tlv_off:tlv_off + TLV_INFO_SIZE]
660        magic, tlv_tot = struct.unpack('HH', tlv_info)
661        if magic == TLV_PROT_INFO_MAGIC:
662            tlv_off += tlv_tot
663            tlv_info = b[tlv_off:tlv_off + TLV_INFO_SIZE]
664            magic, tlv_tot = struct.unpack('HH', tlv_info)
665
666        if magic != TLV_INFO_MAGIC:
667            return VerifyResult.INVALID_TLV_INFO_MAGIC, None, None
668
669        prot_tlv_size = tlv_off
670        hash_region = b[:prot_tlv_size]
671        digest = None
672        tlv_end = tlv_off + tlv_tot
673        tlv_off += TLV_INFO_SIZE  # skip tlv info
674        while tlv_off < tlv_end:
675            tlv = b[tlv_off:tlv_off + TLV_SIZE]
676            tlv_type, _, tlv_len = struct.unpack('BBH', tlv)
677            if tlv_type == TLV_VALUES["SHA256"] or tlv_type == TLV_VALUES["SHA384"]:
678                if not tlv_matches_key_type(tlv_type, key):
679                    return VerifyResult.KEY_MISMATCH, None, None
680                off = tlv_off + TLV_SIZE
681                digest = get_digest(tlv_type, hash_region)
682                if digest == b[off:off + tlv_len]:
683                    if key is None:
684                        return VerifyResult.OK, version, digest
685                else:
686                    return VerifyResult.INVALID_HASH, None, None
687            elif key is not None and tlv_type == TLV_VALUES[key.sig_tlv()]:
688                off = tlv_off + TLV_SIZE
689                tlv_sig = b[off:off + tlv_len]
690                payload = b[:prot_tlv_size]
691                try:
692                    if hasattr(key, 'verify'):
693                        key.verify(tlv_sig, payload)
694                    else:
695                        key.verify_digest(tlv_sig, digest)
696                    return VerifyResult.OK, version, digest
697                except InvalidSignature:
698                    # continue to next TLV
699                    pass
700            tlv_off += TLV_SIZE + tlv_len
701        return VerifyResult.INVALID_SIGNATURE, None, None
702