From 1bd8a29782132fa54b5f6ed648ded4b4f1286c41 Mon Sep 17 00:00:00 2001 From: GroM Date: Thu, 11 Jun 2026 15:21:16 +0200 Subject: [PATCH 1/5] Update hashApp to compute application's hash properly --- ledgerblue/hashApp.py | 134 +++++++++++++++++++++++++++++++++++------- 1 file changed, 113 insertions(+), 21 deletions(-) diff --git a/ledgerblue/hashApp.py b/ledgerblue/hashApp.py index 9e330fe..32ec29a 100644 --- a/ledgerblue/hashApp.py +++ b/ledgerblue/hashApp.py @@ -18,50 +18,142 @@ """ import argparse +import hashlib import struct +from .hexParser import IntelHexParser + + +def auto_int(x): + return int(x, 0) + def get_argparser(): parser = argparse.ArgumentParser( - description="Calculate an application hash from the application's hex file." + description="Calculate an application hash from the application's hex file.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""\ +Parameters not stored in the .hex must be supplied on the command line; +retrieve them from the ELF ledger.* sections or the target device SDK: + + --targetId ledger.target_id + --apiLevel ledger.api_level + --appFlags ledger.app_flags + --dataSize _envram_data - _nvram_data (symbol arithmetic) + --installparamsSize _einstall_parameters - _install_parameters + +code_length is derived automatically as: + maxAddr - minAddr - dataSize - installparamsSize + +boot_offset is taken from the HEX start-linear-address record (type 0x05), +made relative to minAddr and OR-ed with 1 (Thumb bit). +""", ) parser.add_argument( "--hex", help="The application hex file to be hashed", required=True ) parser.add_argument( "--targetId", - help="The device's target ID (default is Ledger Blue)", + help="The device's target ID (e.g. 0x33100004)", type=auto_int, + required=True, + ) + parser.add_argument( + "--apiLevel", help="API level (e.g. 26)", type=int, required=True + ) + parser.add_argument( + "--appFlags", + help="Application flags (e.g. 0x800)", + type=auto_int, + required=True, + ) + parser.add_argument( + "--dataSize", + help="_envram_data - _nvram_data in bytes", + type=int, + required=True, + ) + parser.add_argument( + "--installparamsSize", + help="_einstall_parameters - _install_parameters in bytes", + type=int, + required=True, ) - parser.add_argument("--targetVersion", help="Set the chip target version") return parser -def auto_int(x): - return int(x, 0) +def compute_app_hash( + hex_path, target_id, api_level, flags, data_length, install_params_length +): + """Return (digest_hex, info) for the application described by hex_path. + The digest matches the application hash computed device-side: it hashes the + target ID, the createApp parameters (api_level, code_length, data_length, + install_params_length, flags, boot_offset) and the loaded image bytes. + """ + parser = IntelHexParser(hex_path) -if __name__ == "__main__": - from .hexParser import IntelHexParser - import hashlib + min_addr = parser.minAddr() + max_addr = parser.maxAddr() # exclusive end (start + len) + span = max_addr - min_addr - args = get_argparser().parse_args() + code_length = span - data_length - install_params_length + if code_length < 0: + raise ValueError("dataSize + installparamsSize exceeds the image span") - # parse - parser = IntelHexParser(args.hex) + # Build the contiguous image, zero-padding any gaps between areas. + data = bytearray(span) + for area in parser.getAreas(): + offset = area.getStart() - min_addr + chunk = area.getData() + data[offset : offset + len(chunk)] = chunk + + # boot_offset: relative to minAddr, Thumb bit forced. + boot_addr = parser.getBootAddr() + if boot_addr > min_addr: + boot_addr -= min_addr + boot_offset = boot_addr | 1 - # prepare data m = hashlib.sha256() + m.update(struct.pack(">I", target_id)) + m.update(struct.pack(">B", api_level)) + m.update(struct.pack(">I", code_length)) + m.update(struct.pack(">I", data_length)) + m.update(struct.pack(">I", install_params_length)) + m.update(struct.pack(">I", flags)) + m.update(struct.pack(">I", boot_offset)) + m.update(bytes(data)) + + info = { + "target_id": target_id, + "api_level": api_level, + "flags": flags, + "code_length": code_length, + "data_length": data_length, + "install_params_length": install_params_length, + "boot_offset": boot_offset, + "load_length": len(data), + } + return m.hexdigest(), info - if args.targetId: - m.update(struct.pack(">I", args.targetId)) - if args.targetVersion: - m.update(args.targetVersion) +def main(): + args = get_argparser().parse_args() - # consider areas are ordered by ascending address and non-overlaped - for a in parser.getAreas(): - m.update(a.data) - dataToSign = m.digest() + digest, info = compute_app_hash( + args.hex, + args.targetId, + args.apiLevel, + args.appFlags, + args.dataSize, + args.installparamsSize, + ) - print(dataToSign.hex()) + print(f"{'hex':>24} = {args.hex}") + for name, value in info.items(): + print(f"{name:>24} = {value} (0x{value:x})") + print(f"{'sha256':>24} = {digest}") + + +if __name__ == "__main__": + main() From 3c373a63312cf6fc7a8e9578d8a22236c649a691 Mon Sep 17 00:00:00 2001 From: GroM Date: Thu, 11 Jun 2026 16:13:45 +0200 Subject: [PATCH 2/5] loadApp calls hashApp to compute the application hash --- ledgerblue/loadApp.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ledgerblue/loadApp.py b/ledgerblue/loadApp.py index b31b547..622c5cc 100644 --- a/ledgerblue/loadApp.py +++ b/ledgerblue/loadApp.py @@ -421,7 +421,10 @@ def close(self): string_to_bytes(args.appVersion), ) - hash = loader.load( + # loader.load() streams the app to the device; its returned digest is ignored + # in favour of the hashApp module, which is the single source of truth for the + # application hash (see compute_app_hash). + loader.load( 0x0, 0xF0, printer, @@ -430,6 +433,17 @@ def close(self): doCRC=not (args.nocrc or NOCRC), ) + from .hashApp import compute_app_hash + + hash, _ = compute_app_hash( + args.fileName, + args.targetId, + args.apiLevel, + args.appFlags, + args.dataSize or 0, + args.installparamsSize or 0, + ) + if debug: print("Application full hash : " + hash) From 7bc329fcb1919cb9d6ddb6ee3ea917154052ba43 Mon Sep 17 00:00:00 2001 From: GroM Date: Thu, 11 Jun 2026 16:25:31 +0200 Subject: [PATCH 3/5] Update CHANGELOG.md --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f6f6f..08309d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.57] - 2026-06-11 + +### Changed + +- Update appHash module to compute the proper application's hash +- loadApp now use appHash to compute proper hash for both C and Rust apps + ## [0.1.56] - 2026-04-08 ### Added From b99cf312d1080633b5c5847bba08e4c27649c5d4 Mon Sep 17 00:00:00 2001 From: GroM Date: Thu, 11 Jun 2026 16:52:08 +0200 Subject: [PATCH 4/5] Update doc --- ledgerblue/hashApp.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/ledgerblue/hashApp.py b/ledgerblue/hashApp.py index 32ec29a..83e7d96 100644 --- a/ledgerblue/hashApp.py +++ b/ledgerblue/hashApp.py @@ -15,6 +15,31 @@ * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************** + +Compute the SHA-256 application hash from a .hex file. + +The digest matches the value computed device-side at install time. It is built +by hashing, in order: + + target_id (4 bytes, big endian) + api_level (1 byte) + code_length (4 bytes, big endian) + data_length (4 bytes, big endian) + install_params_length(4 bytes, big endian) + flags (4 bytes, big endian) + boot_offset (4 bytes, big endian) + image (the loaded bytes, minAddr..maxAddr, gaps zero-padded) + +code_length is derived as ``maxAddr - minAddr - data_length - install_params_length``. +boot_offset comes from the HEX start-linear-address record (type 0x05), made +relative to minAddr and OR-ed with 1 (Thumb bit). + +Parameters not stored in the .hex (target_id, api_level, flags, data_length, +install_params_length) must be supplied by the caller; retrieve them from the +ELF ledger.* sections or the target device SDK. + +This module is the single source of truth for the application hash and is reused +by :mod:`ledgerblue.loadApp`. """ import argparse From a2bc7b5ad0bc8c907d66de0363e51d2a28c1ad47 Mon Sep 17 00:00:00 2001 From: GroM Date: Thu, 11 Jun 2026 17:31:06 +0200 Subject: [PATCH 5/5] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08309d1..9868aa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Update appHash module to compute the proper application's hash -- loadApp now use appHash to compute proper hash for both C and Rust apps +- loadApp now use appHash to compute hash ## [0.1.56] - 2026-04-08