| # Copyright 2023-2024 Arm Limited |
| # |
| # SPDX-License-Identifier: Apache-2.0 |
| # |
| # 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. |
| |
| """ |
| Parse and print header, TLV area and trailer information of a signed image. |
| """ |
| import os.path |
| import struct |
| import sys |
| |
| import click |
| import yaml |
| |
| from imgtool import image |
| |
| HEADER_ITEMS = ("magic", "load_addr", "hdr_size", "protected_tlv_size", |
| "img_size", "flags", "version") |
| TLV_TYPES = dict((value, key) for key, value in image.TLV_VALUES.items()) |
| BOOT_MAGIC = bytes([ |
| 0x77, 0xc2, 0x95, 0xf3, |
| 0x60, 0xd2, 0xef, 0x7f, |
| 0x35, 0x52, 0x50, 0x0f, |
| 0x2c, 0xb6, 0x79, 0x80, ]) |
| BOOT_MAGIC_2 = bytes([ |
| 0x2d, 0xe1, 0x5d, 0x29, |
| 0x41, 0x0b, 0x8d, 0x77, |
| 0x67, 0x9c, 0x11, 0x0f, |
| 0x1f, 0x8a, ]) |
| BOOT_MAGIC_SIZE = len(BOOT_MAGIC) |
| _LINE_LENGTH = 60 |
| STATUS = { |
| '0x1': 'SET', |
| '0x2': 'BAD', |
| '0x3': 'UNSET', |
| '0x4': 'ANY', |
| } |
| |
| |
| def parse_enc(key_field_len): |
| if key_field_len is not None: |
| return "(len: {}, if BOOT_SWAP_SAVE_ENCTLV is unset)".format(hex(key_field_len)) |
| else: |
| return "Image not encrypted" |
| |
| |
| def parse_size(size_hex): |
| if size_hex == '0xffffffff': |
| return "unknown" |
| return size_hex + " octal: " + str(int(size_hex, 0)) |
| |
| |
| def parse_status(status_hex): |
| return f"{STATUS[status_hex]} ({status_hex})" if status_hex in STATUS else f"INVALID ({status_hex})" |
| |
| |
| def parse_boot_magic(trailer_magic): |
| magic = "" |
| for i in range(BOOT_MAGIC_SIZE): |
| magic += "{0:#04x} ".format(trailer_magic[i]) |
| if i == (BOOT_MAGIC_SIZE / 2 - 1): |
| magic += ("\n" + " ") |
| return magic |
| |
| |
| def print_in_frame(header_text, content): |
| sepc = " " |
| header = "#### " + header_text + sepc |
| post_header = "#" * (_LINE_LENGTH - len(header)) |
| print(header + post_header) |
| |
| print("|", sepc * (_LINE_LENGTH - 2), "|", sep="") |
| offset = (_LINE_LENGTH - len(content)) // 2 |
| pre = "|" + (sepc * (offset - 1)) |
| post = sepc * (_LINE_LENGTH - len(pre) - len(content) - 1) + "|" |
| print(pre, content, post, sep="") |
| print("|", sepc * (_LINE_LENGTH - 2), "|", sep="") |
| print("#" * _LINE_LENGTH) |
| |
| |
| def print_in_row(row_text): |
| row_text = "#### " + row_text + " " |
| fill = "#" * (_LINE_LENGTH - len(row_text)) |
| print(row_text + fill) |
| |
| |
| def print_tlv_records(tlv_list): |
| indent = _LINE_LENGTH // 8 |
| for tlv in tlv_list: |
| print(" " * indent, "-" * 45) |
| tlv_type, tlv_length, tlv_data = tlv.keys() |
| |
| if tlv[tlv_type] in TLV_TYPES: |
| print(" " * indent, "{}: {} ({})".format( |
| tlv_type, TLV_TYPES[tlv[tlv_type]], hex(tlv[tlv_type]))) |
| else: |
| print(" " * indent, "{}: {} ({})".format( |
| tlv_type, "UNKNOWN", hex(tlv[tlv_type]))) |
| print(" " * indent, "{}: ".format(tlv_length), hex(tlv[tlv_length])) |
| print(" " * indent, "{}: ".format(tlv_data), end="") |
| |
| for j, data in enumerate(tlv[tlv_data]): |
| print("{0:#04x}".format(data), end=" ") |
| if ((j + 1) % 8 == 0) and ((j + 1) != len(tlv[tlv_data])): |
| print("\n", end=" " * (indent + 7)) |
| print() |
| |
| |
| def dump_imginfo(imgfile, outfile=None, silent=False): |
| """Parse a signed image binary and print/save the available information.""" |
| trailer_magic = None |
| # set to INVALID by default |
| swap_size = 0x99 |
| swap_info = 0x99 |
| copy_done = 0x99 |
| image_ok = 0x99 |
| trailer = {} |
| key_field_len = None |
| |
| try: |
| with open(imgfile, "rb") as f: |
| b = f.read() |
| except FileNotFoundError: |
| raise click.UsageError("Image file not found ({})".format(imgfile)) |
| |
| # Parsing the image header |
| _header = struct.unpack('IIHHIIBBHI', b[:28]) |
| # Image version consists of the last 4 item ('BBHI') |
| _version = _header[-4:] |
| header = {} |
| for i, key in enumerate(HEADER_ITEMS): |
| if key == "version": |
| header[key] = "{}.{}.{}+{}".format(*_version) |
| else: |
| header[key] = _header[i] |
| |
| # Parsing the TLV area |
| tlv_area = {"tlv_hdr_prot": {}, |
| "tlvs_prot": [], |
| "tlv_hdr": {}, |
| "tlvs": []} |
| tlv_off = header["hdr_size"] + header["img_size"] |
| protected_tlv_size = header["protected_tlv_size"] |
| |
| if protected_tlv_size != 0: |
| _tlv_prot_head = struct.unpack( |
| 'HH', |
| b[tlv_off:(tlv_off + image.TLV_INFO_SIZE)]) |
| tlv_area["tlv_hdr_prot"]["magic"] = _tlv_prot_head[0] |
| tlv_area["tlv_hdr_prot"]["tlv_tot"] = _tlv_prot_head[1] |
| tlv_end = tlv_off + tlv_area["tlv_hdr_prot"]["tlv_tot"] |
| tlv_off += image.TLV_INFO_SIZE |
| |
| # Iterating through the protected TLV area |
| while tlv_off < tlv_end: |
| tlv_type, tlv_len = struct.unpack( |
| 'HH', |
| b[tlv_off:(tlv_off + image.TLV_INFO_SIZE)]) |
| tlv_off += image.TLV_INFO_SIZE |
| tlv_data = b[tlv_off:(tlv_off + tlv_len)] |
| tlv_area["tlvs_prot"].append( |
| {"type": tlv_type, "len": tlv_len, "data": tlv_data}) |
| tlv_off += tlv_len |
| |
| _tlv_head = struct.unpack('HH', b[tlv_off:(tlv_off + image.TLV_INFO_SIZE)]) |
| tlv_area["tlv_hdr"]["magic"] = _tlv_head[0] |
| tlv_area["tlv_hdr"]["tlv_tot"] = _tlv_head[1] |
| |
| tlv_end = tlv_off + tlv_area["tlv_hdr"]["tlv_tot"] |
| tlv_off += image.TLV_INFO_SIZE |
| |
| # Iterating through the TLV area |
| while tlv_off < tlv_end: |
| tlv_type, tlv_len = struct.unpack( |
| 'HH', |
| b[tlv_off:(tlv_off + image.TLV_INFO_SIZE)]) |
| tlv_off += image.TLV_INFO_SIZE |
| tlv_data = b[tlv_off:(tlv_off + tlv_len)] |
| tlv_area["tlvs"].append( |
| {"type": tlv_type, "len": tlv_len, "data": tlv_data}) |
| tlv_off += tlv_len |
| |
| _img_pad_size = len(b) - tlv_end |
| |
| if _img_pad_size: |
| # Parsing the image trailer |
| trailer_off = -BOOT_MAGIC_SIZE |
| trailer_magic = b[trailer_off:] |
| trailer["magic"] = trailer_magic |
| max_align = None |
| if trailer_magic == BOOT_MAGIC: |
| # The maximum supported write alignment is the default 8 Bytes |
| max_align = 8 |
| elif trailer_magic[-len(BOOT_MAGIC_2):] == BOOT_MAGIC_2: |
| # The alignment value is encoded in the magic field |
| max_align = int.from_bytes(trailer_magic[:2], "little") |
| else: |
| # Invalid magic: the rest of the image trailer cannot be processed. |
| print("Warning: the trailer magic value is invalid!") |
| |
| if max_align is not None: |
| if max_align > BOOT_MAGIC_SIZE: |
| trailer_off -= max_align - BOOT_MAGIC_SIZE |
| # Parsing rest of the trailer fields |
| trailer_off -= max_align |
| image_ok = b[trailer_off] |
| trailer["image_ok"] = image_ok |
| |
| trailer_off -= max_align |
| copy_done = b[trailer_off] |
| trailer["copy_done"] = copy_done |
| |
| trailer_off -= max_align |
| swap_info = b[trailer_off] |
| trailer["swap_info"] = swap_info |
| |
| trailer_off -= max_align |
| swap_size = int.from_bytes(b[trailer_off:(trailer_off + 4)], |
| "little") |
| trailer["swap_size"] = swap_size |
| |
| # Encryption key 0/1 |
| if ((header["flags"] & image.IMAGE_F["ENCRYPTED_AES128"]) or |
| (header["flags"] & image.IMAGE_F["ENCRYPTED_AES256"])): |
| # The image is encrypted |
| # Estimated value of key_field_len is correct if |
| # BOOT_SWAP_SAVE_ENCTLV is unset |
| key_field_len = image.align_up(16, max_align) * 2 |
| |
| # Generating output yaml file |
| if outfile is not None: |
| imgdata = {"header": header, |
| "tlv_area": tlv_area, |
| "trailer": trailer} |
| with open(outfile, "w") as outf: |
| # sort_keys - from pyyaml 5.1 |
| yaml.dump(imgdata, outf, sort_keys=False) |
| |
| ############################################################################### |
| |
| if silent: |
| sys.exit(0) |
| |
| print("Printing content of signed image:", os.path.basename(imgfile), "\n") |
| |
| # Image header |
| section_name = "Image header (offset: 0x0)" |
| print_in_row(section_name) |
| for key, value in header.items(): |
| if key == "flags": |
| if not value: |
| flag_string = hex(value) |
| else: |
| flag_string = "" |
| for flag in image.IMAGE_F.keys(): |
| if value & image.IMAGE_F[flag]: |
| if flag_string: |
| flag_string += ("\n" + (" " * 20)) |
| flag_string += "{} ({})".format( |
| flag, hex(image.IMAGE_F[flag])) |
| value = flag_string |
| |
| if not isinstance(value, str): |
| value = hex(value) |
| print(key, ":", " " * (19 - len(key)), value, sep="") |
| print("#" * _LINE_LENGTH) |
| |
| # Image payload |
| _sectionoff = header["hdr_size"] |
| frame_header_text = "Payload (offset: {})".format(hex(_sectionoff)) |
| frame_content = "FW image (size: {} Bytes)".format(hex(header["img_size"])) |
| print_in_frame(frame_header_text, frame_content) |
| |
| # TLV area |
| _sectionoff += header["img_size"] |
| if protected_tlv_size != 0: |
| # Protected TLV area |
| section_name = "Protected TLV area (offset: {})".format(hex(_sectionoff)) |
| print_in_row(section_name) |
| print("magic: ", hex(tlv_area["tlv_hdr_prot"]["magic"])) |
| print("area size:", hex(tlv_area["tlv_hdr_prot"]["tlv_tot"])) |
| print_tlv_records(tlv_area["tlvs_prot"]) |
| print("#" * _LINE_LENGTH) |
| |
| _sectionoff += protected_tlv_size |
| section_name = "TLV area (offset: {})".format(hex(_sectionoff)) |
| print_in_row(section_name) |
| print("magic: ", hex(tlv_area["tlv_hdr"]["magic"])) |
| print("area size:", hex(tlv_area["tlv_hdr"]["tlv_tot"])) |
| print_tlv_records(tlv_area["tlvs"]) |
| print("#" * _LINE_LENGTH) |
| |
| if _img_pad_size: |
| _sectionoff += tlv_area["tlv_hdr"]["tlv_tot"] |
| _erased_val = b[_sectionoff] |
| frame_header_text = "Image padding (offset: {})".format(hex(_sectionoff)) |
| frame_content = "padding ({})".format(hex(_erased_val)) |
| print_in_frame(frame_header_text, frame_content) |
| |
| # Image trailer |
| section_name = "Image trailer (offset: unknown)" |
| print_in_row(section_name) |
| notice = "(Note: some fields may not be used, depending on the update strategy)\n" |
| notice = '\n'.join(notice[i:i + _LINE_LENGTH] for i in range(0, len(notice), _LINE_LENGTH)) |
| print(notice) |
| print("swap status: (len: unknown)") |
| print("enc. keys: ", parse_enc(key_field_len)) |
| print("swap size: ", parse_size(hex(swap_size))) |
| print("swap_info: ", parse_status(hex(swap_info))) |
| print("copy_done: ", parse_status(hex(copy_done))) |
| print("image_ok: ", parse_status(hex(image_ok))) |
| print("boot magic: ", parse_boot_magic(trailer_magic)) |
| print() |
| |
| footer = "End of Image " |
| print_in_row(footer) |