Signing imx-boot and turning on HAB on the i.MX8M Plus

Enabling CONFIG_IMX_HAB overflowed the SPL's SRAM budget by 10,440 bytes. Fixing that, computing the real CSF insertion offsets from the build's own binman node rather than NXP's generic one, signing with cst, and getting 'No HAB Events Found!' on hardware.

This is the post where signing actually happens. Everything before it was preparation: keys, backups, pinned sources, an unsigned bootloader that boots.

Four steps, in this order, and the order matters:

  1. Prove the unsigned build boots, before touching signing at all.
  2. Turn on HAB and rebuild — which failed, at the linker.
  3. Compute the real CSF offsets and sign.
  4. Flash, and ask the chip whether it agrees.

Nothing here is irreversible. The device is still open, so it verifies signatures, records the result, and boots regardless. That is exactly the property that makes this stage safe to get wrong.

Step 1: prove the unsigned build first

I rebuilt a completely unsigned bootloader from the pinned sources and watched it boot from the first memory-training line onward, rather than trusting a clean compile.

It went straight through, no retries, matching the pattern the factory software always showed — which was the real proof that the LPDDR4 configuration mistake from part 7 was genuinely fixed rather than papered over.

Do this. If you turn on signing while an unrelated build problem is still live, every failure afterwards is ambiguous.

Step 2: CONFIG_IMX_HAB=y, and a linker failure

CONFIG_IMX_HAB=y

Rebuild, and:

LD spl/u-boot-spl
ld.bfd: region `.sram' overflowed by 10440 bytes

Adding HAB's signature-verification code to the SPL — the first and tiniest boot stage, which runs entirely out of on-chip SRAM — pushed it past a size budget that had been comfortable before. 10,440 bytes over.

The tempting fix is to raise the limit until it links. I did not want a number I had guessed, in a config that decides whether a board boots, so I went looking for precedent instead: what size budget do NXP's own defconfigs use when they enable HAB on this SoC family?

grep -rl 'CONFIG_IMX_HAB=y' configs/ | xargs grep -h 'CONFIG_SPL_MAX_SIZE'

Every NXP Android-family defconfig that enables HAB uses the same larger value:

- CONFIG_SPL_MAX_SIZE=0x26000
+ CONFIG_SPL_MAX_SIZE=0x2C000

Not a coincidence, and not a guess — a precedented value, already proven on the same silicon, by the vendor. Applied it, and the build went through cleanly.

The general move is worth more than the specific number: when you need a magic value, look for one that already ships somewhere credible before inventing your own. A wrong SPL size budget does not fail at build time; it fails when the SPL overruns something at boot.

Step 3: the offsets, and NXP's script that does not fit this board

Signing HAB images means telling cst exactly which byte ranges of flash.bin to authenticate, and exactly where to insert the resulting signature blocks. Those offsets are not stable — they move whenever build settings change.

Most guides tell you to read them out of the imx-mkimage log. That does not work here, because this board's U-Boot assembles flash.bin with binman rather than by invoking imx-mkimage separately, so there is no such log to read.

NXP ship a better answer in the U-Boot tree itself:

doc/imx/habv4/csf_examples/mx8m/csf.sh

It reads the real numbers directly out of the finished build artefacts — .config, u-boot.dtb, u-boot.itb, flash.bin — instead of off a printed log. That is the right approach, and it is already in the source you are building.

The bug in it, and how I found the right value

Running it produced a wrong number, silently — no error, just an offset that would have put the signature in the wrong place.

The script looks up the FIT block's location from a binman node path:

/binman/imx-boot/uboot ← NXP's generic path. Does not exist on this board.

This board's binman layout names it differently. I found the real path by reading the compiled device tree's own symbol table rather than guessing:

fdtdump u-boot.dtb | grep -A20 '__symbols__'
/binman/imx-boot/blob-ext@2 ← the real node on this board

Then cross-checked the offset it produced against a value I already knew independently from a different part of the configuration. Exact match — which is about as confident as you can be before writing a bootloader.

fdtget -t x u-boot.dtb /binman/imx-boot/blob-ext@2 offset

A silent wrong answer is the worst failure mode there is. The only defence is deriving the same number twice, by different routes, and refusing to proceed until they agree.

Step 4: the two CSF files

HAB signs flash.bin in two independent blocks, so there are two Command Sequence Files:

  • SPL — the SPL plus the DDR training firmware.
  • FIT — U-Boot proper, ATF and the device tree.
[Header]
Version = 4.3
Hash Algorithm = sha256
Engine = CAAM
Engine Configuration = 0
Certificate Format = X509
Signature Format = CMS
[Install SRK]
File = ".../SRK_1_2_3_4_table.bin"
Source index = 0
[Install CSFK]
File = ".../CSF1_1_sha256_4096_65537_v3_usr_crt.pem"
[Authenticate CSF]
[Install Key]
Verification index = 0
Target index = 2
File = ".../IMG1_1_sha256_4096_65537_v3_usr_crt.pem"
[Authenticate Data]
Verification index = 2
Blocks = <base> <offset> <size> "flash.bin"

You can see the key hierarchy from part 2 doing its job here: the SRK table is installed, the CSF key is installed and used to authenticate the command sequence itself, then the IMG key is installed to authenticate the actual data. Two keys, two jobs, in one file.

The [Unlock] stanza you will not discover by accident

The SPL CSF needs one extra block, and leaving it out does not fail at signing time — it fails much later, in Linux, in a way you would never connect back to this file:

[Unlock]
# Required on i.MX8M Plus when HDMI/DisplayPort are disabled (they are, on
# this board) -- NXP mx8m_secure_boot.txt guide, section 1.4.1. Without
# this, HAB locks CAAM MID registers in a way that crashes the kernel later.
Engine = CAAM
Features = MID

If you take one thing from this post, take that one. A signing configuration that produces a kernel crash hours later, on a board that booted fine, is the kind of bug that costs days.

Step 5: sign, with a budget check

cst -i csf_spl.txt -o csf_spl.bin
cst -i csf_fit.txt -o csf_fit.bin

Then — before going anywhere near the image — check the signature blocks actually fit in the space U-Boot reserved for them:

grep '^CONFIG_CSF_SIZE=' .config
stat -c %s csf_spl.bin csf_fit.bin

If a CSF is larger than CONFIG_CSF_SIZE, inserting it overwrites real data rather than reserved padding, and you have quietly corrupted the bootloader. This is worth an explicit guard rather than a glance:

[ "$(stat -c %s csf_spl.bin)" -le "$csf_budget" ] || { echo "CSF too big — aborting"; exit 1; }

Step 6: assemble, without touching the original

cp flash.bin signed_flash.bin
dd if=csf_spl.bin of=signed_flash.bin bs=1 seek=$spl_csf_offset conv=notrunc status=none
dd if=csf_fit.bin of=signed_flash.bin bs=1 seek=$fit_csf_offset conv=notrunc status=none

conv=notrunc is doing real work: without it, dd truncates the output file at the end of what it wrote, destroying everything after the insertion point.

Then the invariant that catches almost every mistake in this whole procedure:

[ "$(stat -c %s flash.bin)" = "$(stat -c %s signed_flash.bin)" ] || {
echo "size changed — do NOT flash this"; exit 1; }

Signing must not change the file size. The CSF blocks go into space that was already reserved as padding. If the size moved, an offset is wrong, and flashing it would be worse than not signing at all. Leave flash.bin itself untouched and always work on a copy, so a failed attempt costs a cp rather than a rebuild.

Step 7: ask the chip

Flashing is a human step on real hardware — the fastboot route from part 7. Then, at the U-Boot prompt:

u-boot=> hab_status
No HAB Events Found!

That is the whole point of the device being open. The chip verified the signature, found nothing wrong, and told me so — on a board that would have booted either way. If the signature had been bad, it would have booted anyway and logged the event, and I would have known precisely what to fix, with no risk of bricking anything.

Automate the offsets, not the flashing

Everything from step 3 to step 6 is now a script that recomputes offsets from this build's own artefacts on every run. That is deliberate: the offsets shift whenever build settings change, so hard-coding them, or copying them from old notes, is a bug waiting to happen. Rerun the script; never reuse a printed offset by hand.

Flashing and hab_status stay manual. Producing a file on disk is safe and repeatable. Writing a bootloader to a board is neither, and it is the last place you want a script acting on a stale assumption.

Next: extending the chain to the kernel — where the chip caught a real cryptographic mistake that I would not have caught myself.