It boots signed — and the HAB gate in booti I did not know I had

The signed Nerves firmware boots. And a finding that deleted an open task: enabling CONFIG_IMX_HAB wires an automatic fail-closed authentication gate into booti itself, for every kernel boot, with no bootcmd changes at all. Plus a fleet gotcha about who can SSH in.

The signed firmware boots. Kernel, rootfs, IEx prompt, the lot — on a board whose bootloader and kernel are both signed with my own keys and verified by the chip on the way up.

But the interesting part of this post is a thing I got wrong, and only found by reading source rather than trusting my own earlier grep.

The claim I had made, and it was wrong

I had written, in my own notes and in the project TODO:

"Nothing in the automatic boot path verifies anything today, so a fail-closed bootcmd still has to be hand-written."

That came from grepping uboot.env, which is genuinely what it looks like. bootcmd is:

run nerves_init load_image load_fdt boot_mmc

and boot_mmc is:

run set_bootargs; booti ${loadaddr} - ${fdt_addr};

There is no hab_auth_img anywhere in that environment. The grep was correct.

The conclusion drawn from it was not.

What the boot log showed

A full HAB authentication ran anyway, on every boot, without anything asking for it.

The mechanism: it is inside booti

Not in the environment — in U-Boot's own cmd/booti.c, in the vendored u-boot-compulab tree:

#if defined(CONFIG_IMX_HAB) && !defined(CONFIG_AVB_SUPPORT)
extern int authenticate_image(uint32_t ddr_start, uint32_t raw_image_size);
if (authenticate_image(ld, image_size) != 0) {
printf("Authenticate Image Fail, Please check\n");
return 1; /* booti ABORTS; the kernel never runs */
}
#endif

So CONFIG_IMX_HAB=y does not merely make the hab_auth_img command available. It wires an automatic, fail-closed authentication gate into booti for every kernel boot, with no environment changes whatsoever.

Proving it is the mechanism, not a coincidence

The automatic wrapper computes its own byte range, in hab.c:

ivt_offset = (raw_image_size + ALIGN_SIZE - 1) & ~(ALIGN_SIZE - 1);
bytes = ivt_offset + IVT_SIZE + CSF_PAD_SIZE;

Work it through: 0x2290000 + 0x20 + 0x2000 = 0x2292020 — exactly the bytes = 0x2292020 printed in the boot log.

It is deliberately a little larger than the real file's 36,247,904 bytes, because it assumes the full 8 KB CSF_PAD_SIZE rather than this CSF's actual 6,464. Harmless: it covers a few unused bytes of RAM.

Arithmetic agreeing to the digit is how you tell a mechanism from a guess.

What this changes

The open task dissolves. There is nothing to write. Any hand-rolled if hab_auth_img …; then booti … wrapper would be redundant with — and weaker than — what booti already does internally.

Tamper-evidence is live today, for free. An unsigned or modified kernel produces real, decoded HAB_FAILURE events on the console at every boot, right now, with no configuration. That is genuine detection on an open device.

Enforcement still waits on the fuse. authenticate_image() returns success whenever imx_hab_is_enabled() is false, so on this open board the gate always passes and booti proceeds. On a closed device the identical code prints Authenticate Image Fail, Please check and refuses.

So: the wiring is complete; only the authority is missing — and granting that authority is the one step this project deliberately has not taken.

A fleet gotcha that has nothing to do with HAB

One more thing, because it will bite anyone doing this on a team, and it is not about signing at all.

Whoever builds the firmware decides who can log in. Nerves bakes SSH authorized keys into the image at build time, typically from whatever is lying in the builder's ~/.ssh. Build it on your laptop and you get in; build it on CI and you do not; build it on a colleague's machine and the fleet quietly answers to them instead of you.

The fix is to make it explicit and reviewable rather than ambient:

config/authorized_keys # checked in, reviewed, the same for every build

Pointing the build at that file instead of at ~/.ssh means access to the fleet is a thing somebody approves in a diff, not a side effect of which machine ran mix firmware.

On a device you are about to make unrecoverable except by signed firmware, knowing exactly who can reach it stops being a nicety.

Where this leaves things

Both tracks are functionally complete on an open device. The bootloader is signed, the kernel is signed, both verify clean, the build signs itself, and the fail-closed gate is present and proven — waiting only for a fuse I have chosen not to burn.

That is the honest end of this work: proven, not deployed. The mechanism is understood, the procedure is reproducible from a script rather than from memory, and the board on my bench is still recoverable. Everything that remains is a decision rather than an engineering problem.