Signing the kernel, and a HAB failure the chip caught

Extending the chain from the bootloader to the kernel. The first signed Image failed with HAB_INV_ASSERTION — because I read NXP's own diagram literally and stopped the signed range one IVT short. How I decoded the event by hand and found the off-by-32-bytes.

A signed bootloader that then loads an unsigned kernel is theatre. The chain has to continue, or the whole exercise stops at the point where it starts to matter.

This post extends it to the kernel — and it contains the single most useful failure of the whole project, because the chip caught a mistake I had made by reading NXP's documentation correctly but literally, and told me precisely enough to find it.

Get the real kernel off the board

Not a kernel you built. The one actually running, so that a signature failure can only mean a signing problem.

Pull it over the ums route from part 7, and use ls -la, not ls:

Image -> Image-6.6.52-4.1

Image is a symlink. Following it blindly is how you end up signing something other than what boots. Confirm the target is genuinely the running kernel by matching the version string against uname -a recorded earlier:

uname -a
# Linux ... 6.6.52-4.1 ...

Filename agreement is a guess. A version-string match is evidence.

While the partition is mounted, back the whole thing up rather than just the two files you need. It costs seconds and you are already there.

A space check worth doing before, not during. I planned to stage the signed image on partition 1 for the verification run. Partition 1 had 26 MB free and the signed kernel is 35 MB. Switching the test to partition 2 — the 29 GB rootfs — fixed it, and was safer anyway: nothing in the normal boot flow ever auto-reads from there.

The arm64 Image header, and padding that is not a formality

An arm64 kernel Image carries a header declaring its own image_size. Read it from the real file rather than trusting an example:

xxd -s 8 -l 8 -e Image-6.6.52-4.1 # text_offset and image_size fields

On this kernel the declared size and the on-disk size differed by 755,200 bytes. HAB signs a contiguous range, so that gap has to be padded out — it is a real requirement here, not a formality that guides mention and you skip.

Build the IVT, and verify it by decoding it back

HAB needs an Image Vector Table: eight 32-bit fields telling the ROM where the image loads and where its signature lives. It is built from this board's own load address:

loadaddr = 0x40400000

Write it, then read it back out and decode all eight fields before trusting it:

xxd -e -g4 -l 32 ivt.bin

Thirty-two bytes is not much to check by hand, and every later failure will be blamed on it if you do not.

Then the CSF, which is shorter than the bootloader's:

[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 = 0x40400000 0x0 0x2360000 "Image_padded.bin"

No [Unlock] block here. The CAAM MID unlock from part 8 is done once, at the SPL stage. Adding it again is not required.

Sign it, and you have a 37,099,872-byte Image_signed.bin.

The test, and the failure

At the U-Boot prompt, load it and ask the ROM directly:

u-boot=> hab_auth_img 0x40400000 37099872 0x2360000
Calling authenticate_image in ROM
HAB_FAILURE

Decoded reason: HAB_INV_ASSERTION (0x0C).

That is not "the signature does not match". It is an internal consistency failure — HAB is saying the request itself is malformed, before any question of whether the cryptography is right. Which is much better news, because a consistency failure points at a specific place.

Decoding the event by hand

Rather than guess, I read the event's own trailing bytes:

Bytes Value Meaning
12–15 0x42760000 An address — and 0x40400000 + 0x2360000 is exactly that
16–19 0x20 32 — the size of an IVT

Both numbers pointed at the same place: the IVT itself. HAB was telling me the IVT sat immediately after the range I had asked it to authenticate.

The cause: reading a diagram literally

My Blocks length was 0x2360000 — the padded image size, stopping exactly at the IVT boundary. That came from a literal reading of NXP's own diagram, where the "Signed Data" bracket appears to end where the IVT row begins.

It does not. The bracket extends through the IVT row. The IVT must be inside the signed range.

I did not want to act on a re-reading of the same diagram that had just misled me, so I cross-checked against something already proven on this hardware — the FIT signing from part 8, which works:

FIT block offset 0x58000
FIT block length 0x1020
0x58000 + 0x1020 = 0x59020 ← exactly the FIT's own CSF offset

So the already-working signed range covered its own IVT, and ended where the CSF begins. Same shape, independently confirmed, from a case I had already proven on silicon.

The fix

- Blocks = 0x40400000 0x0 0x2360000 "Image_padded.bin"
+ Blocks = 0x40400000 0x0 0x2360020 "Image_padded.bin"

Thirty-two bytes. padded_size + 0x20 — through the IVT, up to where the CSF starts.

Re-signed. The file is the same total size (37,099,872 bytes — the CSF area is fixed-size padding), with different content and a different signature:

before 073ef3f2…53b06
after 75b8f83e3ff4129b6d94e1878ce745730c295235e8cd899e0716acec91b2f29c

Same command, same address, same length:

u-boot=> hab_auth_img 0x40400000 37099872 0x2360000
Calling authenticate_image in ROM
No HAB Events Found!

An apples-to-apples contrast against the first attempt at exactly the same point in the output. No HAB Event block at all.

What to actually read in that output

A thing worth internalising, because it is easy to look at the wrong line: success is not a phrase, it is an absence. Calling authenticate_image in ROM prints either way. What tells you the result is whether a HAB Event block appears immediately after it.

hab fuse not enabled in that output is also expected and correct at this stage — the device is still open. It is not a warning that something is wrong.

Why this failure was worth having

Three things came out of it that I would not have got from a clean first run.

The chip is a better reviewer than I am. I had a plausible reading of a vendor diagram, and the silicon disagreed with precision — not "wrong", but "wrong here, by this much, at this address".

A consistency failure is more useful than a signature mismatch. HAB_INV_ASSERTION told me the request was malformed. Had it been a bad key or corrupt data I would have had far less to go on.

Precedent beats documentation when they disagree. The diagram was ambiguous; the working FIT signature was not. When you have something already proven on the same hardware, measure the new thing against it rather than against a picture.

Later I loaded the same signed kernel, authenticated it, loaded the real device tree, and booted it — so this is not just a file that passes a check, it is a working kernel that also passes.

Next: the two fuses that end this, and why I stopped short of burning them.