Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 hash

## [0.1.56] - 2026-04-08

### Added
Expand Down
159 changes: 138 additions & 21 deletions ledgerblue/hashApp.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,53 +15,170 @@
* 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
import hashlib
Comment thread
tdejoigny-ledger marked this conversation as resolved.
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")

# 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

# parse
parser = IntelHexParser(args.hex)
# 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))
def main():
args = get_argparser().parse_args()

digest, info = compute_app_hash(
args.hex,
args.targetId,
args.apiLevel,
args.appFlags,
args.dataSize,
args.installparamsSize,
)

if args.targetVersion:
m.update(args.targetVersion)
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}")

# consider areas are ordered by ascending address and non-overlaped
for a in parser.getAreas():
m.update(a.data)
dataToSign = m.digest()

print(dataToSign.hex())
if __name__ == "__main__":
main()
16 changes: 15 additions & 1 deletion ledgerblue/loadApp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)

Expand Down
Loading