fix: look up any MCUboot image hash TLV, not just SHA256 - #110
Merged
Merged
Conversation
JPHutchins
commented
Aug 27, 2026
JPHutchins
left a comment
Collaborator
Author
There was a problem hiding this comment.
Mostly good, some ammend commit needs to be made to clean up sloppiness.
Comment on lines
+64
to
+65
| except TLVNotFound: | ||
| return None |
Comment on lines
+54
to
+55
| IMAGE_HASH_TLVS: Final = (IMAGE_TLV.SHA256, IMAGE_TLV.SHA384, IMAGE_TLV.SHA512) | ||
| """The image hash TLVs that MCUboot may write, in `CONFIG_BOOT_IMG_HASH_ALG_*` order.""" |
Collaborator
Author
There was a problem hiding this comment.
Upstream is the SSOT, not us
Comment on lines
+71
to
+73
| An MCUboot image carries exactly one of `IMAGE_HASH_TLVS`, whichever | ||
| `CONFIG_BOOT_IMG_HASH_ALG_*` selected at build time, so the first match is the image | ||
| hash that `ImageStatesWrite` marks. |
Comment on lines
+218
to
+219
| image_hash_tlv = get_image_hash_tlv(image_info) | ||
| if image_hash_tlv is None: |
JPHutchins
requested review from
aldenhaase,
davedesro,
intercreate-gab,
kylermconnelly,
robhelvestine and
swoisz
August 27, 2026 21:05
`smpmgr upgrade` asked for `IMAGE_TLV.SHA256` unconditionally and exited 1 when
it was absent, so it rejected every image NCS sysbuild builds for nRF54L /
nRF54H / nRF71 — those are signed with SHA512 by default:
$ smpmgr --ble CA:17:46:38:86:AF upgrade build/ebp/zephyr/zephyr.signed.bin
Could not find IMAGE_TLV_SHA256 in image. If this is not an MCUboot image,
retry with --format=any.
NCS forces SHA512 for those SoC series whenever ED25519 signing is used, which
is itself the default there, overriding upstream MCUboot's SHA256-first choice:
# nrf/sysbuild/Kconfig.mcuboot:207
config BOOT_IMG_HASH_ALG_SHA512
bool "Use SHA512 for image hash calculation"
depends on BOOT_SIGNATURE_TYPE_ED25519
default y if SOC_SERIES_NRF54L || SOC_SERIES_NRF54H || SOC_SERIES_NRF71
`get_image_hash_tlv()` now tries SHA256, SHA384, then SHA512 and returns the
first match, because MCUboot writes exactly one of them (whichever
`CONFIG_BOOT_IMG_HASH_ALG_*` selected at build time). The error message is
derived from the same tuple that is searched, so it cannot drift from what was
actually looked for.
Each TLV that is probed and missed is logged at INFO, so the search trail is
visible rather than a silently swallowed `TLVNotFound`:
$ smpmgr --loglevel INFO upgrade zephyr.signed.bin
INFO SHA256 not found in image - image_management.py:65
INFO SHA384 not found in image - image_management.py:65
INFO Image hash TLV: SHA512=... - main.py:224
The `image state-write HASH` help no longer claims the argument is a SHA256
hash, for the same reason.
Corrects the premise of issue #109 on the dependency bump: `IMAGE_TLV.SHA512 =
0x12` was already present in the pinned smpclient 7.0.1, so no bump is required
to fix the reported failure. Verified against the released wheel:
$ pip download smpclient==7.0.1 --no-deps && unzip -q smpclient-7.0.1-*.whl
$ python -c "from smpclient.mcuboot import IMAGE_TLV; print(hex(IMAGE_TLV.SHA512))"
0x12
smpclient is bumped 7.0.1 -> 7.3.0 for a different reason found while writing
the tests: 7.0.1's `ImageTLVInfo.__post_init__` hard-fails on the protected TLV
region that precedes the trailer, so `ImageInfo.load_file` raises
`MCUBootImageError: TLV info magic is 0x6908, expected 0x6907` for any image
carrying protected TLVs (SEC_CNT, BOOT_RECORD) — the same NCS families this
issue is about, once a security counter or measured boot is enabled.
intercreate/smpclient#114 fixed that in 7.3.0.
`test_get_image_hash_tlv_of_image_with_a_protected_tlv_region` is the executable
record of this: it is the one test of the eleven that fails on 7.0.1.
Note that 7.3.0's `all` extra grows the runtime tree from 36 to 69 packages
(+56 MB installed): bumble, grpcio, protobuf, cryptography, libusb and the
prebuilt Zephyr HCI firmware bundles. `all` is kept because
`feature/bumble-transport` needs exactly those.
The `slot != 0 or confirm` guard that skips `ImageStatesWrite` for a default
`upgrade` is left alone; issue #109 filed that as an observation and deferred
the `--slot` semantics to #3.
Fixes #109
Co-Authored-By: claude-opus-5[1m] <noreply@anthropic.com>
JPHutchins
force-pushed
the
fix/#109/image-hash-tlv
branch
from
August 27, 2026 21:09
5d1bbab to
d776b34
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warning
LLM Disclosure
This post was authored by
claude-opus-5[1m]on behalf of @JPHutchins. JP asked me to open a branch and PR fixing #109, following the repo's CLAUDE.md, and to run the full test suite before committing. While writing the regression tests I found that one premise of the issue is wrong —IMAGE_TLV.SHA512already exists in the pinned smpclient — so the dependency bump is kept for a different, verified reason, documented below.Summary
smpmgr upgradelooked upIMAGE_TLV.SHA256unconditionally and exited 1 when it was absent, rejecting every image NCS sysbuild produces for nRF54L / nRF54H / nRF71 — those are signed with SHA512 by default.get_image_hash_tlv()now triesSHA256,SHA384,SHA512and returns the first match, because MCUboot writes exactly one of them. The error message is derived from the same tuple that is searched, so it can't drift from what was actually looked for:lintIssue #109 says the pin
==7.0.1"predates that fix" and asks for>=7.3.0"soIMAGE_TLV.SHA512exists". It already exists in 7.0.1. The reported failure was entirely smpmgr's hardcoded lookup, and is fixed here without any dependency change.Verified against the released 7.0.1 wheel from PyPI
SHA512 = 0x12is present in theIMAGE_TLVenum of every 7.x release — 7.0.1, 7.1.0, 7.2.0 and 7.3.0 all contain the literalSHA512 = 0x12insmpclient/mcuboot.py. smpclient#83 landed before 7.0.0, not after 7.0.1.Empirically: with the pin still at
==7.0.1, 10 of the 11 tests in this PR pass, including all three oftest_get_image_hash_tlv[SHA256/SHA384/SHA512].Why the bump to
==7.3.0is kept anywayA real reason turned up while writing the tests. smpclient 7.0.1 hard-fails on the protected TLV region that can precede the image trailer:
So
ImageInfo.load_fileraises for any image carrying protected TLVs (SEC_CNT,BOOT_RECORD) — the same NCS families this issue is about, as soon as a security counter or measured boot is enabled. That would fail earlier than the hash lookup, atInspection of FW image failed. intercreate/smpclient#114 fixed it in 7.3.0, which addedImageInfo.protected_tlv_info/protected_tlvs.The one test that fails on 7.0.1 and passes on 7.3.0
test_get_image_hash_tlv_of_image_with_a_protected_tlv_regionis the executable record of the bump — this is the pre-bump run:and post-bump:
The pin uses
==rather than the issue's>=, per the convention d459285 set ("smpmgr is an app, not a library").Dependency impact of the bump — 36 → 69 runtime packages, +56 MB
7.3.0's
allextra is much wider than 7.0.1's. Installedsite-packagesgoes 118 MB → 174 MB, which lands in the PyInstaller portable artifact:smpmgr on
mainimports only theble,serialandudptransports, soextras = ["ble", "serial", "udp"]would keep the tree at its current size.allis kept deliberately:feature/bumble-transportaddssmpmgr/bumble.pyonsmpclient.transport.bumbleand 80977b8 there bundles the Zephyr HCI firmware into the portable artifact on purpose. Narrowing the extras here would fight that branch. Flagging the numbers so the trade is explicit rather than accidental.Changes
smpmgr/image_management.py—IMAGE_HASH_TLVS,IMAGE_HASH_TLV_NAMES,get_image_hash_tlv(). ReturnsOptionalrather than raising, so the caller's error path stays a plain branch.smpmgr/main.py—upgradeuses it; local and log line renamedimage_tlv_sha256→image_hash_tlv;IMAGE_TLV/TLVNotFoundimports dropped.smpmgr/image_management.py— adjacent: theimage state-write HASHargument help no longer claims the hash is SHA256. Same bug class, user-facing; the workaround in upgrade: hardcoded IMAGE_TLV_SHA256 lookup rejects every NCS nRF54L/54H/71 image (SHA512 is the default there) #109 passes a SHA512 to exactly this argument.pyproject.toml/poetry.lock—smpclient ==7.0.1→==7.3.0.tests/test_image_hash_tlv.py— new.Test coverage — 11 tests, and what each one pins down
Images are assembled from smpclient's own
structs (IMAGE_HEADER_STRUCT,IMAGE_TLV_INFO_STRUCT,IMAGE_TLV_STRUCT) so the fixtures cannot drift from the parser, and the trailer shape mirrors what NCS sysbuild emits (KEYHASH+ hash +ED25519).test_get_image_hash_tlv[SHA256/SHA384/SHA512]test_get_image_hash_tlv_of_image_without_a_hashNone, not an exceptiontest_get_image_hash_tlv_takes_the_first_of_IMAGE_HASH_TLVSIMAGE_HASH_TLVSorder wins, not trailer ordertest_get_image_hash_tlv_of_image_with_a_protected_tlv_regiontest_upgrade_inspection_accepts_any_hash[SHA256/SHA384/SHA512]CliRunner:upgradeclears inspection and reaches the transporttest_upgrade_inspection_rejects_an_image_without_a_hashThe two
upgradecases run the real CLI with no transport option, so they assert on the boundary between inspection and connection without needing hardware.Deliberately not changed
The
if slot != 0 or confirm:guard atsmpmgr/main.py:241that skipsImageStatesWritefor a defaultupgrade(--slot 0, no--confirm) — the behaviour #109 reports under "The suggested workaround does not do what it says". #109 filed that as an observation rather than a fix and deferred the--slotsemantics to #3, so it is untouched here. Worth noting the consequence: after this PR a defaultupgradeof a SHA512 image uploads and resets without erroring, but still does not mark the image.Residual risk
Not validated on hardware. 7.0.1 → 7.3.0 spans the serial-transport rework (
max_smp_encoded_frame_sizedeprecated in favour of theBufferSizestrategy, MCUmgr params negotiation). Upstream kept the 7.1.0 constructor and that kwarg working, andsmpmgr/common.pypasses all three ofline_length/line_buffers/max_smp_encoded_frame_size, so it should be source-compatible — but the test suite has no transport coverage and cannot detect a behavioural change in serial fragmentation. A serial DFU smoke test before release is worth it. The BLE path this issue was reported on is unaffected by that rework.Fixes #109