Series Secure boot on an i.MX8M gateway Part 8 of 10 · all parts

Turning on secure boot, for real this time

A quick note before this post, an update on the note that used to open it: when I first wrote this, it was a roadmap — a plan for what I was about to try,…

A quick note before this post, an update on the note that used to open it: when I first wrote this, it was a roadmap — a plan for what I was about to try, written before I'd actually walked through any of it. Since then, the recovery saga from the last post finally resolved (the board is back, fully working, on the exact software it had before any of this started), and I picked this thread back up. The first two steps below are no longer a plan — they actually happened, mistakes and all. Steps three and four are still ahead of me, so I've kept those sections as written, still honestly labeled as "not yet done."

Step one: prove the build works, before touching signing at all — DONE

I built a completely unsigned bootloader from the sources I pinned earlier in this series, and this time I actually watched it boot properly, from the very first memory-training line onward, instead of just trusting a clean compile. It went straight through, no retries, matching the same clean pattern the original factory software always showed — the real proof that the memory-configuration mistake from the last post is genuinely fixed, not just patched around.

Step two: sign it — DONE, with two more real mistakes along the way

Turning on the actual security feature and rebuilding didn't go cleanly either, which felt almost fitting given how this whole series has gone.

First real snag: the build flatly refused to link, with a blunt error about a section not fitting into its assigned memory region. Adding real signature- checking code to that very first, tiniest boot stage made it grow past a size limit that had been perfectly comfortable before. I didn't want to just guess a bigger number and hope — so I went looking at how the chip vendor's own reference board configurations handle exactly this situation, and found a consistent pattern: every one of their configurations that also turns on this same security feature uses a noticeably larger size budget than the ones that don't, always the same specific value. Not a coincidence, and not a guess — a real, precedented number, already proven to work on the same chip. Applied the same value here, and the build went through cleanly.

Second real snag, more interesting: turning on signing support makes the build print out a set of numbers — offsets, really, just locations inside the final image — that say exactly where the actual signature data needs to be inserted. Except my board's build doesn't print them the way most of the vendor's own documentation assumes, because of a genuinely different underlying tool being used to assemble the final image (a detail I ran into a couple of posts back, when I found out my board's build skips a separate assembly tool most guides assume you're using). I found a much better reference sitting right there in the source code itself — the vendor's own example script for exactly this situation, written to read the real numbers directly out of the finished files instead of off a printed log. Running it turned up one more real, specific bug: it expected a particular internal label inside the build's own configuration data that simply isn't named that way on my board — a silent failure with no error message, just a wrong number I could easily have trusted without checking. Found the real label by digging through the build's own internal symbol table directly, fixed it, and cross-checked the corrected number against something I already independently knew from an entirely different part of the configuration — an exact match, which is about as confident as I can be that it's actually right.

With the real numbers in hand, I wrote out the actual signature-instruction files (starting from the vendor's own reference examples, changing only what genuinely needed to change, exactly like I'd planned), ran them through the signing tool using the keys generated earlier in this series, and inserted the results into the image. A genuinely signed bootloader now exists.

Update, written after actually plugging the board back in: it works. Flashed the signed bootloader onto the real hardware and asked the chip directly whether it thought this image was validly signed — a built-in status command exists purely to answer that question. Completely clean: no complaints, no errors, nothing to explain away. Just to make sure I wasn't fooling myself with a check that would say "fine" no matter what I fed it, I deliberately built and flashed a second, unsigned copy of the exact same bootloader right after, and asked the same question again. This time it complained for real — four separate, specific complaints, each one naming exactly what was wrong, in a way that lined up precisely with what I'd expect from an image that was never signed at all. Then I put the signed one back, asked a third time, and got the same clean answer as the first time. That back-and-forth — clean, broken, clean again — is about as convincing as proof gets without actually locking the device: whatever this checking mechanism is doing, it's a real check, not just a rubber stamp.

Doing all of the above by hand, copying numbers around manually, is exactly the kind of thing that becomes a mistake eventually — so I went ahead and scripted the whole sequence into one repeatable command, and it reproduced the exact same signed result byte-for-byte (aside from a tiny, expected timestamp difference buried inside the signature data itself). No more manual copying required for this part, ever again.

Step three: the point of no return (well, half of it)

This is the part I've been building up to, carefully, through every earlier post in this series. Once I have a signed bootloader that boots cleanly, the plan is to burn that key fingerprint from post 3 into the chip's own one-time-programmable memory.

I want to be precise about what this step actually does and doesn't do, because it's easy to misjudge: burning that fingerprint is permanent — there's no erasing it afterward — but the device stays in "open" mode through this step. An open device with the fingerprint burned still boots anything, signed or unsigned, it just now has something real to compare against, and will honestly report whether a given image matches. So this step is a real, one-way action, but not yet the step that actually locks anything down. I plan to check the board's own reporting before and after this, on both a correctly signed image and a deliberately wrong one, so I can see with my own eyes that the check is really happening, not just trust that it should be.

There's a second, separate one-way switch further down the line — the one that actually makes the device refuse anything unsigned. That one I'm deliberately not touching for a long time, possibly ever on this particular unit, since its whole purpose in this project is to stay open forever as a safe place to keep learning and testing.

Step four: trusting the actual operating system, not just the bootloader — DONE, with a real bug caught along the way

Everything above only covers the bootloader itself. The last piece was extending that same trust one level further, to the actual Linux kernel the bootloader loads — signing the kernel image the same way, and teaching the bootloader to verify that signature before handing off control. This actually happened, hit a genuinely interesting bug on the first real attempt, and got all the way to a signed kernel booting the board to a normal login prompt — full story in the next post, because it deserves its own space.

What comes after this

I still haven't touched either of the two one-way switches described in step three above. Once I decided how far to take this device, this series turns into a closing piece about exactly what those two switches would each require — what changes, what stays the same, and what the honest tradeoffs are — even though I'm not necessarily pulling either one on this particular board. That's its own post too.

Taking everything learned here, on this one disposable lab unit, and carrying it over into the actual production system this gateway normally runs is a big enough topic on its own that it gets its own series, which I've already started outlining separately.

For now, that's the honest state of things: a solid foundation built carefully, one verified step at a time, and a clear plan for the parts that actually matter. I'll be back with how it really went.