Signing the Nerves kernel on i.MX8M Plus, clean on the first try
The same kernel-signing procedure as track A, on a different build — and it passed first time, because the two things that went wrong last time were checked rather than assumed. A 691,712-byte padding gap, and a signed range that goes through the IVT.
Track A's kernel signing took two
attempts: the first produced HAB_FAILURE with HAB_INV_ASSERTION, because the signed range
stopped one IVT short.
This one passed first time. Not luck — the two things that failed last time are the two things I checked rather than assumed.
Check one: read the real header, do not reuse the number
The signed kernel here is a different file from track A's, built by a different build system. Its padding requirement is its own.
import struct
with open(image, 'rb') as f:
data = f.read(64)
magic = struct.unpack_from('<I', data, 0x38)[0]
assert magic == 0x644d5241, f'not an arm64 Image: {magic:#x}'
image_size = struct.unpack_from('<Q', data, 0x10)[0] # 8 bytes at 0x10, not 4 at 0x4
The file on disk is 35,549,696 bytes and the header declares more — a genuine 691,712-byte gap. HAB signs a contiguous range, so that has to be padded out for real.
A different number from track A's 755,200, on the same board, for the same reason. Which is exactly why the script recomputes it from the input file every run rather than carrying a constant.
Check two: the signed range goes through the IVT
The bug from track A, applied as a lesson rather than relearned:
Blocks = <load_addr> 0x0 <padded_size + 0x20> "Image_padded.bin"
padded_size + 0x20. The IVT is 32 bytes, it sits immediately after the padded image, and it
must be inside the signed range — not up to it. NXP's diagram reads the other way at a
glance, and reading it that way is what produced HAB_INV_ASSERTION last time.
Check three: the load address is this fork's, not the other one
./sign_kernel.sh <input-Image> 0x40480000 <output-Image>
Track A signs at 0x40400000. This system loads at 0x40480000. Same board, different
build. The script takes it as an argument, read fresh from this fork's own uboot.env,
because a hard-coded load address produces a signature that is perfectly valid over the wrong
memory.
The budget check earns its place
Before any dd, the same guard as the bootloader:
stat -c %s csf_kernel.bin # must fit CSF_PAD_SIZE
This CSF came out at 6,464 bytes against an 8 KB pad — comfortable. But the check is not about this run. It is about the run where a key size or a block list changes and the CSF grows past the reserved padding, at which point inserting it overwrites real image data instead of padding, and produces a file that is corrupt in a way nothing downstream will notice.
The result
hab_auth_img at the U-Boot prompt, first attempt:
Calling authenticate_image in ROM
No HAB Events Found!
No HAB Event block. As track A's part 9
notes, success here is an absence rather than a phrase — Calling authenticate_image in ROM
prints either way.
What made this uneventful
Three constants that could have been copied from the previous series, and all three would have been wrong: the padding gap, the load address, and — had I not already fixed it — the signed range.
None of them are wrong in an interesting way. They are wrong in the way that produces a valid signature over slightly the wrong bytes, which fails at boot with an error that sends you to look at your keys.
The discipline that matters is not "sign carefully". It is "derive every number from the artefact in front of you, every run." Writing that as a script rather than a checklist is what makes it hold.
Next: making the build do this by itself, and a correction about which layer that belongs in.