Why I am not burning the fuses on an STM32MP157F-DK2
Secure boot on this chip comes down to 32 bytes in one-time-programmable memory, and a bit you can never un-blow. I started the project with a rule forbidding its own last step — and three of the four links in the chain turn out to enforce for real on completely blank silicon.
Starting a project with a rule against finishing it
I'm starting a secure boot project on the STM32MP157F-DK2, and the first thing I wrote down was a rule forbidding the last step.
That sounds like procrastination dressed up as process. Stay with me, because I think it's the opposite, and working out why has already changed how I'm approaching the whole thing.
The one-way door
Secure boot on this chip comes down to 32 bytes.
You generate a key pair. You take the public half, hash it with SHA-256, and get 32 bytes. Those 32 bytes go into OTP — One-Time Programmable memory, a little array of fuses inside the silicon, programmed through a block ST calls BSEC. Then you set one more bit, the "closed device" bit, and from that moment the ROM code refuses to run any bootloader that isn't signed by the matching private key.
Here's the thing about fuses. Every bit starts at 0. You can blow it to 1. You can never, ever blow it back.
There's no erase command. There's no factory reset. There's no ST tool, no jumper, no key combination, no support ticket. And — this is the part that made me sit up. I went looking for an escape hatch — some families give you a way to hand a bricked part back to the manufacturer for analysis — and I could not find one documented anywhere I looked.
(I was wrong about that, and found out later when I actually read the reference manual.
RM0436 Rev 7, Table 18 shows OTP word 56 — XK24 — holding an RMA unlock password
in bits 0–14 and an RMA relock password in bits 15–29, and RM0436 defines three device
states: secured-open, secured-closed and RMA.
This does not make fuse writes reversible. A bit set to 1 still cannot be set back, and what RMA actually grants I have not pinned down. But it is a clean example of why "I could not find it" and "it does not exist" are different sentences — and why a vendor reference manual beats five third-party guides that agree with each other.)
So you get one attempt. Per chip. Forever.
I've been through this door before
I did secure boot on a Compulab gateway a while back. Same shape of problem, different silicon. And what I remember most clearly isn't the cryptography — the cryptography is honestly the easy part, it's forty years old and extremely well understood. What I remember is the feeling in the room before running the command that blows the fuses.
Because up to that point, every mistake had cost me a rebuild. Maybe an afternoon. And after that point, every mistake costs a board.
That asymmetry is the actual engineering problem in secure boot. Not ECDSA. Not certificate chains. The fact that your entire process is reversible right up until the instant it becomes permanent, and there is no gradient in between.
So I wrote the rule
The rule is: on this development board, in this phase, we do not program a
single fuse. Not the key hash. Not the closed-device bit. Not a lock bit. Not
an anti-rollback counter. Not with ST's tool, not from U-Boot, not from a script
with a --yes-i-am-sure flag.
I went a bit further than that, actually. I wrote down that the fusing procedure doesn't get to live in the same documents as everything else. It goes in its own file, every command in it prefixed with a DO-NOT-EXECUTE marker, deliberately mangled so that if I ever copy a block out of it at 1am by accident, it fails instead of doing something permanent.
Reading fuses is fine. fuse read doesn't write anything, and confirming the
OTP is still blank is a genuinely useful thing to do at the start of a session.
It's the writes that are banned.
At the U-Boot prompt, these two are safe and are the first thing I run every session:
fuse read 0 0 1 # word 0 — bit 6 is the closed-device bit
fuse read 0 24 8 # words 24-31 — the 256-bit public key hash
Word 0 should read back with bit 6 clear, and words 24–31 all zero. I capture that output at the start and end of every hardware session. It is the evidence that nothing irreversible happened, and it costs ten seconds.
The commands that write — and that never get run on this board — are fuse prog,
fuse sense -w, stm32key fuse, stm32key close, and STM32CubeProgrammer's -otp write,
-otp lock and -otp fwrite. They live in their own file, every line prefixed with a
DO-NOT-EXECUTE marker and deliberately mangled, so that copying a block out of it at 1am
fails instead of doing something permanent.
The question that made this interesting
Having written the rule, I had to ask the obvious follow-up: what's actually left? If I can't close the device, can I do secure boot at all, or am I just going to spend three months writing documentation about something I can't test?
I spent a while on this, and the answer surprised me enough that it's now the backbone of the whole project.
"Secure boot" on this platform isn't one thing. It's four.
There's the ROM verifying the first-stage bootloader against the fused key hash. That's ST's ROM code, that's the one that needs fuses, and it's exactly one of the four.
Then there's TF-A's BL2 verifying the next stage — the secure monitor and U-Boot — against a certificate chain inside the FIP. That's ARM's trusted board boot. That's software. Software I build.
Then U-Boot verifying the kernel via a FIT image signature. Software.
Then Linux verifying the rootfs with dm-verity. Software.
Three of the four are software. Three of the four enforce for real, with genuine refusals and genuine error messages, on completely blank silicon.
That is not wishful thinking — it is visible in TF-A's own source. On an open device the platform layer flags the root key as undeployed:
/* plat/st/stm32mp1/stm32mp1_security.c */
if ((res == 0) && !stm32mp_is_closed_device()) {
*flags |= ROTPK_NOT_DEPLOYED;
}
and the authentication module then skips one specific check:
/* drivers/auth/auth_mod.c:235 */
if ((flags & ROTPK_NOT_DEPLOYED) != 0U) {
NOTICE("ROTPK is not deployed on platform. "
"Skipping ROTPK verification.\n");
}
Read that carefully, because it is the whole argument. What gets skipped is the check that
the root key in the certificate chain is my key — and it is skipped because there is
nothing fused to compare it against. Everything after that branch still runs,
unconditionally: auth_signature() still verifies every signature in the chain, and a bad
one still stops the boot.
So the chain is real. It just is not yet anchored to me. That distinction is the series.
So I can build the whole chain. I can sign everything. I can corrupt a FIP on purpose and watch the board refuse to boot, and put the good one back, and watch it boot again. I can demonstrate authenticated boot to somebody, on hardware, with a real failure, having risked precisely nothing.
The only thing I can't do is have the ROM enforce the first link. And for that one I can still sign the image correctly, parse the header, extract the public key, verify the signature myself with OpenSSL, and compute the exact 32 bytes that would go into the fuses — and then not put them there.
That's maybe 90% of the work and 100% of the understanding, at 0% of the risk.
The demo I'm most looking forward to
Here's the one I actually want to show people.
Get TF-A verifying the FIP. Corrupt it. Board refuses. Great, secure boot works, everyone nods.
Then modify the root filesystem. Add a file. Change a binary. Boot it.
It boots. Perfectly happily.
Because the signed chain currently ends at U-Boot, and everything after it is unverified until I add FIT signing and dm-verity. And I think that demo is worth more than the first one, because it kills the idea that secure boot is a switch you flip. It's a chain, it ends somewhere specific, and if you can't say exactly where, you don't have a security story — you have a feeling.
I'm going to run that demo deliberately, and I'm going to leave it in the writeup, because the alternative is letting someone believe the chain goes further than it does.
The thing I keep coming back to
There's one more reason not to fuse yet, and it took me a few days to see it.
There's a question I don't know the answer to: if I do secure boot on my manufacturing line, can whoever ends up operating the device later generate their own keys and take control of the device?
At the ROM level, on this chip, no. One key hash, no revocation, no second slot. Whoever's key goes in owns that silicon for its physical life. But above that level — where BL2 decides what to trust — there are genuinely several ways to design it, and some of them let the operator own everything they actually care about while I keep a minimal, auditable presence at the bottom.
I haven't decided which. I haven't even written down my threat model yet.
And every single one of those options stays available for exactly as long as the OTP stays blank.
That's the real argument. Not caution for its own sake. Not fusing keeps the expensive decisions reversible until they've actually been made — and right now they haven't been. The moment I blow those fuses is the moment I've committed to answers I haven't worked out yet.
So: blank fuses, signed images, a chain I can test and break and fix, and a file full of DO-NOT-EXECUTE commands.
(Update, a couple of days on: I've made this permanent rather than "for now." This board never gets fused, full stop. The reason that clinched it wasn't the one-way door — it was realising that closing it would marry the board forever to the throwaway development keys sitting unencrypted on my build VM. Keys I regenerate whenever I feel like it. A development board's entire value is that I can reflash it with anything, and fusing destroys exactly that. There's no upside: a closed dev board teaches me nothing an open one doesn't, and costs me the board.)
Next up: what secure boot actually is, why it doesn't hide your firmware, and why the thing you burn into silicon is public information that you could print on a billboard without losing anything.