diff --git a/scripts/imgtool/dumpinfo.py b/scripts/imgtool/dumpinfo.py index 3866c9a104..2049f723a2 100644 --- a/scripts/imgtool/dumpinfo.py +++ b/scripts/imgtool/dumpinfo.py @@ -17,11 +17,14 @@ """ Parse and print header, TLV area and trailer information of a signed image. """ +import json import os.path import struct +import sys import click import yaml +from intelhex import IntelHex from imgtool import image @@ -74,50 +77,51 @@ def parse_boot_magic(trailer_magic): return magic -def print_in_frame(header_text, content): +def _human_format_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="") + lines = [] + lines.append(header + post_header) + lines.append("|" + sepc * (_LINE_LENGTH - 2) + "|") 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) + lines.append(pre + content + post) + lines.append("|" + sepc * (_LINE_LENGTH - 2) + "|") + lines.append("#" * _LINE_LENGTH) + return "\n".join(lines) -def print_in_row(row_text): +def _human_format_row(row_text): row_text = "#### " + row_text + " " fill = "#" * (_LINE_LENGTH - len(row_text)) - print(row_text + fill) + return row_text + fill -def print_tlv_records(tlv_list): +def _human_format_tlv_records(tlv_list): indent = _LINE_LENGTH // 8 + lines = [] for tlv in tlv_list: - print(" " * indent, "-" * 45) - tlv_type, tlv_length, tlv_data = tlv.keys() + lines.append(" " * indent + " " + "-" * 45) - if tlv[tlv_type] in TLV_TYPES: - print(" " * indent, f"{tlv_type}: {TLV_TYPES[tlv[tlv_type]]} ({hex(tlv[tlv_type])})") - else: - print(" " * indent, "{}: {} ({})".format( - tlv_type, "UNKNOWN", hex(tlv[tlv_type]))) - print(" " * indent, f"{tlv_length}: ", hex(tlv[tlv_length])) - print(" " * indent, f"{tlv_data}: ", end="") + type_name = tlv["type_name"] + type_hex = hex(tlv["type"]) + lines.append(" " * indent + f" type: {type_name} ({type_hex})") + lines.append(" " * indent + f" len: {hex(tlv['len'])}") - for j, data in enumerate(tlv[tlv_data]): - print(f"{data:#04x}", end=" ") - if ((j + 1) % 8 == 0) and ((j + 1) != len(tlv[tlv_data])): - print("\n", end=" " * (indent + 7)) - print() + data_line = " " * indent + " data: " + for j, data in enumerate(tlv["data"]): + data_line += f"{data:#04x} " + if ((j + 1) % 8 == 0) and ((j + 1) != len(tlv["data"])): + lines.append(data_line) + data_line = " " * (indent + 7) + lines.append(data_line) + return "\n".join(lines) -def dump_imginfo(imgfile, outfile=None, silent=False): - """Parse a signed image binary and print/save the available information.""" +def _read_imginfo(imgfile): + """Parse a signed image binary and return the image data structure.""" trailer_magic = None # set to INVALID by default swap_size = 0x99 @@ -125,11 +129,14 @@ def dump_imginfo(imgfile, outfile=None, silent=False): copy_done = 0x99 image_ok = 0x99 trailer = {} - key_field_len = None + ext = os.path.splitext(imgfile)[1][1:].lower() try: - with open(imgfile, "rb") as f: - b = f.read() + if ext == image.INTEL_HEX_EXT: + b = IntelHex(imgfile).tobinstr() + else: + with open(imgfile, "rb") as f: + b = f.read() except FileNotFoundError: raise click.UsageError(f"Image file not found ({imgfile})") @@ -169,7 +176,10 @@ def dump_imginfo(imgfile, outfile=None, silent=False): 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}) + {"type": tlv_type, + "type_name": TLV_TYPES.get(tlv_type, "UNKNOWN"), + "len": tlv_len, + "data": tlv_data}) tlv_off += tlv_len _tlv_head = struct.unpack('HH', b[tlv_off:(tlv_off + image.TLV_INFO_SIZE)]) @@ -187,7 +197,8 @@ def dump_imginfo(imgfile, outfile=None, silent=False): 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}) + {"type": tlv_type, "type_name": TLV_TYPES.get(tlv_type, "UNKNOWN"), + "len": tlv_len, "data": tlv_data}) tlv_off += tlv_len _img_pad_size = len(b) - tlv_end @@ -236,26 +247,28 @@ def dump_imginfo(imgfile, outfile=None, silent=False): # 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 + trailer["key_field_len"] = key_field_len + + imginfo = { + "filename": os.path.basename(imgfile), + "header": header, + "tlv_area": tlv_area, + "trailer": trailer} - # 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) + return imginfo - ############################################################################### - if silent: - return +def _write_format_human(imginfo, outfile): + filename = imginfo["filename"] + header = imginfo["header"] + tlv_area = imginfo["tlv_area"] + trailer = imginfo["trailer"] - print("Printing content of signed image:", os.path.basename(imgfile), "\n") + print("Printing content of signed image:", filename, "\n", file=outfile) # Image header section_name = "Image header (offset: 0x0)" - print_in_row(section_name) + print(_human_format_row(section_name), file=outfile) for key, value in header.items(): if key == "flags": if not value: @@ -271,55 +284,106 @@ def dump_imginfo(imgfile, outfile=None, silent=False): if not isinstance(value, str): value = hex(value) - print(key, ":", " " * (19 - len(key)), value, sep="") - print("#" * _LINE_LENGTH) + print(key, ":", " " * (19 - len(key)), value, sep="", file=outfile) + print("#" * _LINE_LENGTH, file=outfile) # Image payload _sectionoff = header["hdr_size"] frame_header_text = f"Payload (offset: {hex(_sectionoff)})" frame_content = "FW image (size: {} Bytes)".format(hex(header["img_size"])) - print_in_frame(frame_header_text, frame_content) + print(_human_format_frame(frame_header_text, frame_content), file=outfile) # TLV area _sectionoff += header["img_size"] + protected_tlv_size = header["protected_tlv_size"] if protected_tlv_size != 0: # Protected TLV area section_name = f"Protected TLV area (offset: {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) + print(_human_format_row(section_name), file=outfile) + print("magic: ", hex(tlv_area["tlv_hdr_prot"]["magic"]), file=outfile) + print("area size:", hex(tlv_area["tlv_hdr_prot"]["tlv_tot"]), file=outfile) + print(_human_format_tlv_records(tlv_area["tlvs_prot"]), file=outfile) + print("#" * _LINE_LENGTH, file=outfile) _sectionoff += protected_tlv_size section_name = f"TLV area (offset: {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: + print(_human_format_row(section_name), file=outfile) + print("magic: ", hex(tlv_area["tlv_hdr"]["magic"]), file=outfile) + print("area size:", hex(tlv_area["tlv_hdr"]["tlv_tot"]), file=outfile) + print(_human_format_tlv_records(tlv_area["tlvs"]), file=outfile) + print("#" * _LINE_LENGTH, file=outfile) + + # Check if trailer has data (for image padding and trailer info) + if trailer.get("magic"): + trailer_magic = trailer["magic"] _sectionoff += tlv_area["tlv_hdr"]["tlv_tot"] - _erased_val = b[_sectionoff] - frame_header_text = f"Image padding (offset: {hex(_sectionoff)})" - frame_content = f"padding ({hex(_erased_val)})" - print_in_frame(frame_header_text, frame_content) + # Note: We don't have access to original binary data here, so skip padding details # Image trailer section_name = "Image trailer (offset: unknown)" - print_in_row(section_name) + print(_human_format_row(section_name), file=outfile) 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() + print(notice, file=outfile) + print("swap status: (len: unknown)", file=outfile) + print("enc. keys: ", parse_enc(trailer.get("key_field_len")), file=outfile) + + # Only print trailer fields if they exist + if "swap_size" in trailer: + print("swap size: ", parse_size(hex(trailer["swap_size"])), file=outfile) + if "swap_info" in trailer: + print("swap_info: ", parse_status(hex(trailer["swap_info"])), file=outfile) + if "copy_done" in trailer: + print("copy_done: ", parse_status(hex(trailer["copy_done"])), file=outfile) + if "image_ok" in trailer: + print("image_ok: ", parse_status(hex(trailer["image_ok"])), file=outfile) + print("boot magic: ", parse_boot_magic(trailer_magic), file=outfile) + print(file=outfile) footer = "End of Image " - print_in_row(footer) + print(_human_format_row(footer), file=outfile) + +def _json_default_serializer(obj): + """Convert non-JSON-serializable objects to JSON-serializable format.""" + if isinstance(obj, (bytes, bytearray)): + return obj.hex() + raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") + + +def _write_format(imginfo, output_format, out): + """Write image info in the specified format to the output stream.""" + if output_format == 'human': + _write_format_human(imginfo, out) + + elif output_format == 'yaml': + yaml.dump(imginfo, out, sort_keys=False) + + elif output_format == 'json': + json.dump(imginfo, out, indent=2, default=_json_default_serializer) + if out == sys.stdout: + print() # Add newline after JSON output to stdout + else: + raise ValueError(f"Invalid output format: {output_format}") + + +def dump_imginfo(imgfile, outfile=None, output_format=None, silent=False): + """Parse a signed image binary and print/save the available information.""" + + # Note: silent parameter is kept for backward compatibility but is ignored. + # The function's purpose is to output data, so silent doesn't make sense here. + + # Determine output format based on backward compatibility rules + if output_format is None: + if outfile is None: + output_format = 'human' # no --outfile defaults to human-friendly + else: + output_format = 'yaml' # --outfile without --format defaults to yaml + + imginfo = _read_imginfo(imgfile) + + if outfile: + with open(outfile, "w") as out: + _write_format(imginfo, output_format, out) + else: + _write_format(imginfo, output_format, sys.stdout) + diff --git a/scripts/imgtool/image.py b/scripts/imgtool/image.py index c2f51ae7e4..264120a2d9 100755 --- a/scripts/imgtool/image.py +++ b/scripts/imgtool/image.py @@ -188,6 +188,7 @@ def tlv_sha_to_sha(tlv): keys.RSAPublic : ['256'], # This two are set to 256 for compatibility, the right would be 512 keys.Ed25519 : ['256', '512'], + keys.Ed25519Public : ['256', '512'], keys.X25519 : ['256', '512'] } @@ -349,8 +350,8 @@ def __repr__(self): class Image: def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE, - pad_header=False, pad=False, confirm=False, align=1, - slot_size=0, max_sectors=DEFAULT_MAX_SECTORS, + pad_header=False, pad=False, confirm=False, test=False, + align=1, slot_size=0, max_sectors=DEFAULT_MAX_SECTORS, overwrite_only=False, endian="little", load_addr=0, rom_fixed=None, erased_val=None, save_enctlv=False, security_counter=None, max_align=None, @@ -368,6 +369,7 @@ def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE, self.pad_header = pad_header self.pad = pad self.confirm = confirm + self.test = test self.align = align self.slot_size = slot_size self.max_sectors = max_sectors @@ -506,12 +508,14 @@ def save(self, path, hex_addr=None): self.save_enctlv, self.enctlv_len) trailer_addr = (self.base_addr + self.slot_size) - trailer_size - if self.confirm and not self.overwrite_only: + if (self.test or self.confirm) and not self.overwrite_only: magic_align_size = align_up(len(self.boot_magic), self.max_align) image_ok_idx = -(magic_align_size + self.max_align) + # If test is set, we leave image_ok at the erased value flag = bytearray([self.erased_val] * self.max_align) - flag[0] = 0x01 # image_ok = 0x01 + if self.confirm: + flag[0] = 0x01 # image_ok = 0x01 h.puts(trailer_addr + trailer_size + image_ok_idx, bytes(flag)) h.puts(trailer_addr + (trailer_size - len(self.boot_magic)), @@ -950,11 +954,13 @@ def pad_to(self, size): pbytes = bytearray([self.erased_val] * padding) pbytes += bytearray([self.erased_val] * (tsize - len(self.boot_magic))) pbytes += self.boot_magic - if self.confirm and not self.overwrite_only: + if (self.test or self.confirm) and not self.overwrite_only: magic_size = 16 magic_align_size = align_up(magic_size, self.max_align) image_ok_idx = -(magic_align_size + self.max_align) - pbytes[image_ok_idx] = 0x01 # image_ok = 0x01 + # If test is set, set leave image_ok at the erased value + if self.confirm: + pbytes[image_ok_idx] = 0x01 # image_ok = 0x01 self.payload += pbytes @staticmethod diff --git a/scripts/imgtool/main.py b/scripts/imgtool/main.py index 5560a4612e..aa85ce6316 100755 --- a/scripts/imgtool/main.py +++ b/scripts/imgtool/main.py @@ -246,15 +246,16 @@ def verify(key, imgfile): @click.argument('imgfile') @click.option('-o', '--outfile', metavar='filename', required=False, - help='Save image information to outfile in YAML format') + help='Save image information to outfile') +@click.option('-f', '--format', 'output_format', + type=click.Choice(['human', 'yaml', 'json']), + help='Output format (human, yaml, json). Default: human for stdout, yaml for file') @click.option('-s', '--silent', default=False, is_flag=True, help='Do not print image information to output') @click.command(help='Print header, TLV area and trailer information ' 'of a signed image') -def dumpinfo(imgfile, outfile, silent): - dump_imginfo(imgfile, outfile, silent) - if not silent: - print("dumpinfo has run successfully") +def dumpinfo(imgfile, outfile, output_format, silent): + dump_imginfo(imgfile, outfile, output_format, silent) def validate_version(ctx, param, value): @@ -388,6 +389,9 @@ def convert(self, value, param, ctx): @click.option('--confirm', default=False, is_flag=True, help='When padding the image, mark it as confirmed (implies ' '--pad)') +@click.option('--test', default=False, is_flag=True, + help='When padding the image, mark it for a test swap (implies ' + '--pad)') @click.option('--pad', default=False, is_flag=True, help='Pad image to --slot-size bytes, adding trailer magic') @click.option('-S', '--slot-size', type=BasedIntParamType(), required=True, @@ -464,7 +468,7 @@ def convert(self, value, param, ctx): @click.option('--compression-lzma-preset', type=int, default=9, help='LZMA - compression level preset', show_default=True) def sign(key, public_key_format, align, version, pad_sig, header_size, - pad_header, slot_size, pad, confirm, max_sectors, overwrite_only, + pad_header, slot_size, pad, confirm, test, max_sectors, overwrite_only, endian, encrypt_keylen, encrypt, compression, infile, outfile, dependencies, load_addr, hex_addr, erased_val, save_enctlv, security_counter, boot_record, custom_tlv, custom_tlv_file, rom_fixed, max_align, @@ -472,13 +476,13 @@ def sign(key, public_key_format, align, version, pad_sig, header_size, vector_to_sign, non_bootable, vid, cid, edt_config, manifest, compression_lzma_dictsize, compression_lzma_pb, compression_lzma_lc, compression_lzma_lp, compression_lzma_preset): - if confirm: + if confirm or test: # Confirmed but non-padded images don't make much sense, because # otherwise there's no trailer area for writing the confirmed status. pad = True img = image.Image(version=decode_version(version), header_size=header_size, pad_header=pad_header, pad=pad, confirm=confirm, - align=int(align), slot_size=slot_size, + test=test, align=int(align), slot_size=slot_size, max_sectors=max_sectors, overwrite_only=overwrite_only, endian=endian, load_addr=load_addr, rom_fixed=rom_fixed, erased_val=erased_val, save_enctlv=save_enctlv, diff --git a/scripts/imgtool/keys/ecdsa_test.py b/scripts/tests/keys/test_ecdsa.py similarity index 100% rename from scripts/imgtool/keys/ecdsa_test.py rename to scripts/tests/keys/test_ecdsa.py diff --git a/scripts/imgtool/keys/ed25519_test.py b/scripts/tests/keys/test_ed25519.py similarity index 100% rename from scripts/imgtool/keys/ed25519_test.py rename to scripts/tests/keys/test_ed25519.py diff --git a/scripts/imgtool/keys/rsa_test.py b/scripts/tests/keys/test_rsa.py similarity index 100% rename from scripts/imgtool/keys/rsa_test.py rename to scripts/tests/keys/test_rsa.py