What secure boot actually is on an STM32MP1: the header, field by field
Signing is not encrypting — the thing burned into silicon is public and you could print it on a billboard. The chicken-and-egg the 32 bytes solve, the STM32 header field by field, four locks wearing one name, and the two things I got wrong and had to correct.
Part 2 was about the four different promises people mean when they say "secure." This post zooms all the way into one of them — authenticity, the one called secure boot — because I've spent the last few days discovering that most of what I thought I knew about it was either imprecise or flatly wrong.
Two things in particular. But let me build up to those.
Signing is not encrypting
This is the one that trips everybody, including me for longer than I'd like to admit.
Encryption answers "who is allowed to read this?" Signing answers "who wrote this, and has it been changed?"
Secure boot is signing. Entirely. My bootloader is not a secret — you can pull the SD card out of my gateway, read it, disassemble it, post it on the internet, and nothing bad happens to me. What must be impossible is producing a different bootloader that my chip accepts.
So: secure boot does not hide your firmware. It proves your firmware is yours. If you also want it hidden, that's a separate feature with a separate key, and it's not what I'm building.
I keep repeating this because I've now heard three people say "we have secure boot so the firmware is protected," meaning protected from being read. It isn't. It's protected from being replaced.
The wax seal
The clearest picture I've found for how a signature works:
Imagine sealing a letter with a wax stamp. Only you have the stamp — that's the private key. Everyone has seen your seal design — that's the public key. Anyone can look at a letter and say "yes, that's his seal." And knowing what the seal looks like doesn't let anyone carve a copy of the stamp.
The chip only ever needs the seal design. It never needs the stamp. That's why my private key can live on one machine in my office and never go anywhere near a device in the field, and the device can still verify my firmware. That asymmetry is the whole trick.
The chicken and the egg
Here's where it gets interesting. To check the signature, the chip needs the public key. Where does it keep it?
Not on the SD card — an attacker who can replace the bootloader can replace the public key sitting next to it, sign with their own private key, and everything verifies beautifully. The verification would be completely real and completely worthless.
So the public key has to be pinned somewhere the attacker can't rewrite. On the STM32MP15 that's OTP — fuses inside the chip. And what actually gets burned isn't the key itself but its SHA-256 hash: 32 bytes, called the PKH, the Public Key Hash.
Boot then goes:
- The SD card supplies the bootloader, the public key, and the signature, all in a 256-byte header.
- The ROM hashes the supplied public key and compares it against the 32 bytes in the fuses. Different? Refuse.
- The ROM uses that now-trusted public key to check the signature over the bootloader. Bad? Refuse.
- Run it.
Step 2 is the load-bearing one. An attacker can put any public key they like in the header, but they can't change 32 bytes burned into silicon, and their key won't hash to those 32 bytes.
Only 32 bytes of the entire design are actually immutable. Everything else is software and can be fixed later. Those 32 bytes cannot. It focuses the mind.
The header, field by field
I got hold of the actual layout, and it's satisfyingly simple. 256 bytes:
| Offset | Size | Field |
|---|---|---|
0x00 |
4 | magic — ASCII STM2 |
0x04 |
64 | the ECDSA signature: R then S, 32 bytes each |
0x44 |
4 | image checksum |
0x48 |
4 | header version ← the hash starts here |
| ... | image length, entry point, load address, flags | |
0x68 |
4 | which curve |
0x6C |
64 | the public key: X then Y, raw |
0xAC |
83 | padding |
0xFF |
1 | binary type |
What gets signed is everything from offset 0x48 to the end of the file — the rest of the header and the whole payload. The magic, the signature and the checksum are outside that region, for the obvious reason that a signature can't sign itself.
Two details that will bite anyone implementing this:
The signature is raw R‖S, not DER. OpenSSL's command line emits DER by default. Feed DER into that field and you get a header that looks plausible and fails verification.
The public key is raw X‖Y, 64 bytes, with no 0x04 prefix. Elliptic curve points are normally stored as 65 bytes with a leading 0x04 meaning "uncompressed." That byte is a format tag, not part of the key. Include it in the hash and your PKH is wrong.
I know that second one intimately, because my first attempt at computing the PKH produced 63 bytes instead of 64. I was scraping OpenSSL's text output, where the last byte has no trailing colon, so my pattern match silently dropped it. It printed a confident, wrong answer.
On a development board that's five minutes. On fusing day it's a dead chip. I rewrote it to read the DER encoding and take the trailing 65 bytes properly. The lesson is now a comment at the top of the script: never eyeball key material, and check the tool that checks it.
The layout
0x00 ┌──────────────────────┐
│ magic "STM2" │ ┐
0x04 │ image_signature[64] │ ├─ NOT signed
0x44 │ image_checksum │ ┘
0x48 ├──────────────────────┤ ┐
│ header_version │ │
│ image_length │ │
│ … │ │
│ ecdsa_public_key[64] │ ├─ SHA-256 over all of this,
│ padding[83] │ │ then ECDSA-signed
0xFF │ binary_type │ │
0x100 ├──────────────────────┤ │
│ │ │
│ TF-A BL2 payload │ │
│ │ ┘
└──────────────────────┘
The first three fields are excluded for an obvious reason: a signature cannot sign itself. The magic and the checksum sit alongside it and are outside the protected region too — so neither is a security control. The magic only identifies the format.
Note what is inside the signed region: ecdsa_public_key. The public key is signed by
the key it belongs to. That proves nothing on its own — anyone can self-sign — which is
exactly why the ROM's first action is to hash that key and compare it against the fuses.
Only after that comparison passes is the key trusted enough to check a signature with.
That is the chicken-and-egg resolved, in one ordering.
Two formats that will waste your afternoon
The signature field is two raw 32-byte big-endian integers, concatenated — R then S:
BN_bn2bin(ECDSA_SIG_get0_r(sig), &image_signature[0]); /* R: bytes 0..31 */
BN_bn2bin(ECDSA_SIG_get0_s(sig), &image_signature[32]); /* S: bytes 32..63 */
Not an ASN.1/DER SEQUENCE. This matters because OpenSSL's command-line dgst -sign
emits DER by default. Feed DER into that field and you get a header that looks entirely
plausible and fails verification, with nothing pointing at the format as the cause.
The public key field is the same story: 64 raw bytes, X then Y. Not PEM, and — see part 4 — not the 65-byte form either.
Four locks wearing one name
Here's the thing that reframed the whole project for me.
"Secure boot" on this platform isn't one mechanism. It's four, and they share a name the way "engine" is shared by a car and a search engine.
| Who checks | What gets checked | Needs fuses? | |
|---|---|---|---|
| Link 1 | ST's ROM, in silicon | TF-A BL2, the first bootloader I supply | yes |
| Link 2 | TF-A BL2 | the FIP — secure monitor and U-Boot | no |
| Link 3 | U-Boot | the Linux kernel | no |
| Link 4 | Linux | the root filesystem | no |
Three of the four are software I build, and they enforce for real on completely blank silicon. Nothing about TF-A checking a certificate involves a fuse — it reads the certificate and stops if it's wrong, because that's what the code does.
Which means I can build almost all of this, and break it deliberately, and watch it refuse, without ever doing anything permanent. That's the project.
It also means that when someone says "we enabled secure boot," the useful question is: which of the four? Usually it's Link 1 only, and then they're surprised that swapping the root filesystem still works.
Now the two things I got wrong
I said I'd get to these.
One: I said this chip has no RMA mode. I'd looked for an escape hatch — some silicon families give you a way to hand a bricked part back to the manufacturer for analysis — and I couldn't find one, so I wrote down that it doesn't exist.
It does. Reference manual, OTP word 56: an RMA unlock password and an RMA relock password. The chip has three lifecycle states, not two: secured-open, secured-closed, and RMA.
Does that mean a wrong fuse write is recoverable? No — and this is where I've been careful not to overcorrect. Fuses remain fuses. A bit set to 1 cannot be cleared by any password. RMA on this class of part normally re-opens debug access for failure analysis, which is a very different thing from getting a wrongly-fused key back. So my guidance hasn't changed at all. But my reason has, and the difference would matter to anyone who actually needed a failure-analysis path.
Two: I said ST's signing tool can't use a hardware security module. I'd read several third-party guides, they all passed the private key as a PEM file, so I concluded the tool couldn't do anything else. I wrote it down as a verified fact and built an argument on top of it: we'd have to write our own signer, because nobody with an HSM would export their private key to a file.
Then I actually read ST's manual for the tool. Section 2.4 is titled "PKCS#11 solution." It supports HSMs and smartcards. It has been able to since 2021. ST's own justification is word-for-word the argument I'd been making against them:
"The classic signing command requests that all public and private keys be provided as input files. These are directly accessible by any person who is allowed to execute the signing service. Ultimately, this can be considered to be a security leak."
So that decision is reopened, and probably reversed.
What both mistakes have in common
They're the same mistake. I concluded a feature was absent because secondary sources didn't mention it.
A tutorial that uses file-based keys documents file-based keys. It says nothing about what the tool supports. A project README saying "this replaces a closed-source tool" says nothing about that tool's option list. In both cases the primary document settled the question in about ninety seconds once I had it in my hands.
Absence of mention is not absence of the feature. I've written that at the top of my notes file now. In a project where being wrong costs silicon, "I looked and couldn't find it" and "it doesn't exist" need to stay very clearly different sentences.
The uncomfortable part is that both wrong facts were sitting in my documentation, confidently, propagated across eight or nine files, being used as premises for decisions. That's the actual risk — not the error itself but how fast a wrong fact becomes load-bearing.
The bit I'm most pleased about
The board boots. Signed nothing yet, fused nothing, but it boots, and the console tells me things I didn't know:
CPU: STM32MP157FAC Rev.Z
BL2: v2.10.5(release):lts-v2.10.5
Board: stm32mp1 in trusted - stm32image mode
Clocks:
- MPU : 650 MHz
That last line is interesting. The F variant supports 800 MHz. I'm booting the C variant's device tree, and the C variant's frequency table caps at 650. So I may be giving away nineteen percent of my CPU because of a filename. Haven't confirmed it yet — that needs the datasheet's operating point table, and I'm not going to guess twice in one week.
Next time: generating the keys, signing TF-A, and verifying my own signature offline before letting any silicon near it.