Part 8 of 12 in the Embedded Firmware Architecture series · Phase 3: Fail safely · Level: Advanced
Article 07 assumed failures were accidental. Rails dip, peripherals glitch, bits flip, and the architecture responds. Secure boot starts from the opposite assumption.
An attacker is a fault source that reads your recovery documentation and picks the path that helps them, as the close of article 07 put it.
So the question changes from “what happens when this fails” to “what happens when someone makes this fail, deliberately, repeatedly, with the device on their bench.”
This article covers the firmware security architecture that makes a device trustworthy: the root of trust, what secure boot actually verifies and what it costs in milliseconds, key hierarchy and rotation across a ten-year life, anti-rollback and the physical budget it consumes, and debug lock decisions that can’t be undone. Article 12 covers getting a new image onto the device safely. That is a different problem, and the two get conflated constantly.
The Attack Surface You Actually Have
Threat modelling in the abstract produces generic advice. Here’s the KIC-400’s real surface, which is more instructive:
| Interface | Reachable by | What it gets them | Realistic control |
|---|---|---|---|
| Modbus TCP :502 | Anyone on the plant network | Read and write registers | None at the protocol level. Segment the network. |
| Modbus RTU (RS-485) | Physical access to the bus | Same, plus bus disruption | None. Physical security. |
| MQTT over TLS | Network path to the broker | Telemetry, remote commands | Mutual TLS, per-device identity |
| Update endpoint | Network | Code execution, persistence | Signature verification, anti-rollback |
| SWD header | Physical access | Firmware readout, key extraction | RDP level, and it’s a hard choice |
| External EEPROM | Physical access, I2C probe | Configuration, any stored secret | Don’t store secrets there |
The first two rows are the honest part, and security write-ups often skip it: Modbus has no security. Not weak security: none. No authentication, no integrity, no confidentiality, in classic RTU or TCP form. Anyone who can reach port 502 can write any holding register. The Modbus Organization did publish a secure variant, Modbus/TCP Security, which runs over TLS on port 802 with certificate-based roles, but support in field devices and SCADA systems is still limited, so most deployments you will meet are the classic, unprotected kind.
You cannot fix that in firmware. What you can do is state it plainly in your documentation, refuse to let Modbus writes reach anything safety-relevant without a second channel, and design assuming the fieldbus is hostile. A product that claims Modbus TCP is “secured” because the device does secure boot has misunderstood what secure boot is for.
That distinction matters architecturally. Secure boot protects what executes. It does nothing about what a running, authentic image accepts.

Root of Trust: What “Immutable” Actually Means
Every chain of trust terminates somewhere an attacker can’t change. If it terminates somewhere they can, the whole structure is decorative.
On an STM32H7 the candidates are a mix of option-byte protections and, on some parts, dedicated hardware. Here is what the H743 in the KIC-400 has, against the crypto-enabled H753:
| Mechanism | H743 | H753 | What to know |
|---|---|---|---|
| WRP (per-sector write protection) | Yes | Yes | An option byte: running code can remove it below RDP Level 2 |
| PCROP (execute-only areas) | Yes | Yes | Survives RDP regression unless DMEP is set |
| RDP Levels 0, 1, 2 | Yes | Yes | Level 2 is permanent and locks WRP |
| Secure access mode (secure user area) | No | Yes | H75x only; survives regression unless DMES is set |
| OTP area | No | No | Some other H7 lines, such as the H7A3/H7B3, have one |
| HASH and CRYP peripherals | No | Yes | Hardware SHA-256 and AES |
| PKA (public-key accelerator) | No | No | ECDSA verification runs in software on both |
| TRNG | Yes | Yes | On-device key generation |
| TrustZone | No | No | Cortex-M7 is Armv7-M |
So on the H743 the root of trust is write protection, readout protection and an immutable bootloader, and its strength turns on a detail that is easy to miss. WRP is an option byte, and at RDP Level 0 or 1 running code can unlock the option bytes (the OPTKEY values are published in RM0433), clear WRP and rewrite the bootloader. “Only the bootloader can write this sector” therefore holds against an attacker who cannot run code on the device, not against a compromised but authentic application. WRP becomes immutable to running code only at RDP Level 2, and the readout protection section below draws the consequence. The security-version counter has the same dependency: with no OTP, it lives in a secure element’s monotonic counter or in an append-only flash-word log in a WRP-protected sector, whose cost is worked out in the anti-rollback section.
PCROP (proprietary code readout protection). Marks a region execute-only: readable by the CPU as instructions, not as data. Useful for keeping verification code, and with care key material, from being dumped by a firmware image that’s authentic but compromised. The care is this: PCROP blocks every data read, including reads by the code inside the region, so a key stored as data there cannot be used. It has to be encoded into instructions, which needs execute-only code generation (GCC’s -mpure-code, for example), because ordinary literal pools are data reads. Set DMEP on that area too, or an RDP regression erases the rest of flash and leaves the key-bearing code in place for the attacker’s own code to call. If that sounds fragile, it is, and the secure element below is usually the better home for keys.
A separate secure element. An external chip that holds keys and performs operations without ever exposing the private material. Adds a BOM line and an I2C bus, and buys you key storage the MCU genuinely cannot leak.
The architectural question is which of these your part supports and which you’ve actually enabled, and those are frequently different answers. A product with a beautiful signature verification routine, executing from flash that’s readable over SWD because RDP was never raised, has a root of trust an attacker can edit.

One thing to be clear about, because the assumption is widespread: Cortex-M7 has no TrustZone. TrustZone-M is the Armv8-M Security Extension, optional on the Cortex-M23, M33, M35P, M55 and M85; in STM32 terms that means parts such as the L5, U5 and H5. On an H7 your isolation primitives are the MPU and the privileged/unprivileged split, and they’re weaker. Plan accordingly rather than discovering it during a security review.
Integrity, Authenticity, and Why CRC Isn’t Either
Briefly, because this is table stakes and the series has already used CRCs for their proper purpose.
A CRC answers “did this change by accident.” It’s excellent at that, cheap, and article 02 uses it on configuration for exactly that reason.
It answers nothing about deliberate change, because CRC32 is trivially invertible: modify the image, compute the new CRC, write it in. A hash answers “did this change,” including deliberately, but only if the stored hash is itself protected. A signature answers “did this come from someone holding the private key,” which is the question that matters, and it’s the only one of the three that survives an attacker who can rewrite the whole flash image including its metadata.
So: CRC for accidental corruption, signature for authenticity, and never substitute one for the other.

What Secure Boot Actually Verifies
A signature check is the headline, but a bootloader that only checks a signature will happily install a correctly signed image built for a different board revision.
The full check list, in the order you want it, because each cheap check that fails saves you an expensive one:
1. Header magic and structure valid ~free
2. Image size within slot bounds ~free
3. Hardware revision compatible ~free
4. Security version ≥ stored minimum ~free ← anti-rollback
5. Vector table points inside the slot ~free
6. SHA-256 over the image expensive
7. ECDSA verify of the signature over hash moderate
Check 5 is worth calling out. An image whose reset vector points outside its own slot is either corrupt or hostile, and checking it costs two comparisons. Skip it and a partially-written image can transfer control somewhere arbitrary before anything else notices.
One rule ties the list together, and it is the one most often got wrong: the signed digest must cover the header as well as the body, every field except the signature itself. Checks 1 to 5 read header fields before any cryptography has run, which is fine for rejecting early and cheaply, but nothing they read is trustworthy yet. If the hash covers only the image body, an attacker takes an old, validly signed image, edits its security version upward, and walks straight past anti-rollback. So hash header plus body, verify the signature, and make the final accept decision from the authenticated header, never from the copy you parsed first.
And what it costs
This rarely gets a number attached, and it’s a boot-time budget item.
Hashing dominates. Software SHA-256 on a Cortex-M runs in the region of 25 to 40 cycles per byte, so a 1 MB image at 480 MHz is roughly:
1,048,576 bytes × ~30 cycles = ~31 M cycles ÷ 480 MHz ≈ 65 ms
Parts in the family with a hardware HASH peripheral, such as the H753, cut that substantially, because hashing becomes bound by bus throughput rather than the core. The H743 in the KIC-400 has no HASH or CRYP peripheral, so it pays the software figure. ECDSA P-256 verification is several million cycles in an optimised software library, so call it 5 to 20 ms there; a default Mbed TLS build without its speed options can be several times slower. Ed25519 verification is typically faster than P-256 if you’re free to choose.
So secure boot on this product costs somewhere around 70 to 85 ms. On an H753 it is more like 15 to 30 ms: the HASH peripheral takes care of the digest, but the part has no public-key accelerator, so the ECDSA verification stays in software at 5 to 20 ms. Measure yours: these are order-of-magnitude figures to size a budget, not numbers to quote.
That has a real architectural consequence. If your product has a boot-time requirement, such as an industrial controller that must resume control within a defined window after a power blip, 85 ms of hashing is a line item you have to plan for, and discovering it late means either dropping verification or missing the requirement.

The tempting shortcut, and its cost. Verify on update, store a “verified” flag, and skip the hash on every subsequent boot. Boot time drops to a few milliseconds. What you’ve done is move your trust from the signature to storage protection, and the flag alone is not enough. If the hash is skipped on later boots, the image region has to be write-protected as well as the flag; otherwise an attacker leaves the flag untouched and rewrites the code it vouches for. On the H743 that means WRP over the application sectors and the flag, applied by the bootloader, and immutable to running code only at RDP Level 2. You’ve traded a cryptographic guarantee for a storage-protection one. Defensible on a part with strong write protection and a fast boot requirement. Indefensible if the flag or the image sits in ordinary, writable flash.
You don’t have to write this bootloader from scratch. ST’s X-CUBE-SBSFU (Secure Boot and Secure Firmware Update) expansion package supports the STM32H7 and implements this check list on top of the part’s own protections, and MCUboot works without TrustZone, so it runs on a Cortex-M7. Either one still leaves the decisions in this article to you: key slots, where the security version lives, and the RDP level.
Fault Injection: Glitching Past the Check
A signature check ends in a branch, and a branch can be skipped. A voltage glitch on VCORE or a shortened clock cycle, timed to the compare, can make the core skip an instruction or load a wrong value, and one skipped conditional turns “reject” into “boot”. The equipment is cheap and the timing can be found by trial on a bench unit, so the code around the decision deserves as much care as the cryptography. Four countermeasures, all inexpensive:
Check redundantly. Compute the decision twice, by different code paths, and require both to agree. One glitch has to become two, each precisely timed.
Use a non-trivial accept flag. Initialise the result to reject, make accept a constant such as 0x3CA5965A and reject an unrelated one, never 0 and 1. A skipped store or a zeroed register then reads as reject. Compare digests in constant time, accumulating differences instead of returning at the first mismatch, so there is no single early-exit branch to target.
Add random delays. A random number of cycles, drawn from the TRNG, before each critical compare moves the target on every boot, so a timing found once does not repeat.
Verify again after the decision. Just before the jump, re-check the flag and the authenticated header, and on any inconsistency go to a lockout path that records the event rather than a retry loop. A glitch that skips the first check now has to skip the second as well.
None of this makes glitching impossible. It raises the number of precisely timed faults an attacker needs, which on a bench rig is the difference between an afternoon and a project. If one compromised unit could threaten the fleet, keep the keys in the secure element, which has its own glitch detectors.
Key Hierarchy, and Rotation Across Ten Years
The most common key mistake isn’t weak crypto. It’s one key doing four jobs.
| Key | Purpose | Where the private half lives | Rotatable? |
|---|---|---|---|
| Firmware signing | Proves an image is yours | HSM, offline, in your building | Hard (see below) |
| Device identity | Proves this unit is yours | On the device, ideally never exported | Per-device, via re-provisioning |
| TLS client | Authenticates to the broker | Its own key, generated on the device and certified at provisioning | Yes, with a provisioning path |
| Storage encryption | Protects data at rest | Derived from a device-unique secret (not the readable 96-bit UID) | Yes, with re-encryption |
Separation limits blast radius. A compromised storage key exposes one device’s configuration. One caution on “device-unique secret”: the STM32 96-bit unique ID is readable by anyone and is not a secret. The H743 has no hardware unique key, so generate a secret with the TRNG at provisioning and keep it in protected flash or the secure element. A compromised signing key means anyone can produce firmware your entire fleet accepts, which is an unrecoverable event for products that have already shipped, and that asymmetry is why signing keys live in an HSM and never on a build machine.

The rotation problem few teams plan for
Here’s the ten-year question. Your signing key is compromised in year six. What now?
If the public verification key is burned into OTP or baked into an immutable bootloader, you cannot rotate it. Every fielded device will verify against a key an attacker now controls, for the rest of the product’s life, and the only fix is physical access to every unit.
Three designs that keep the option open, in ascending cost:
Multiple key slots. Burn two or three public keys at provisioning, with a revocation bitmap in one-way storage: OTP where the part has it, or on the H743 an append-only flash-word log in a WRP-protected sector, like the anti-rollback log below. Compromise slot 0, mark it revoked, sign with slot 1. Simple and bounded: you get as many rotations as you provisioned slots, and no more.
A signed key-update path. The bootloader accepts a key-update package signed by the current key. Elegant, and it has a bootstrap problem: if the current key is compromised, the attacker can also update keys. Only useful for planned rotation, not for compromise recovery.
A separate, offline root. A root key signs intermediate signing keys with limited validity. The root is used a handful of times in its life and lives in a safe. This is what a real PKI looks like, and it’s proportionate once your fleet is large enough that a signing key compromise is a company-ending event rather than a bad quarter.
Pick one at architecture time. Retrofitting key rotation onto a fielded fleet is not an engineering task, it’s a recall.

Anti-Rollback, and the Budget It Spends
An attacker installs firmware 3.0: correctly signed, genuinely yours, and containing the vulnerability you fixed in 5.0. Signature verification passes, because the signature is valid. It is your firmware.
The defence is a security version separate from the marketing version, stored monotonically, checked at boot.
Separate from the firmware version, and that distinction matters. Firmware version goes 5.0.1, 5.0.2, 5.1.0, and it moves for every release. Security version moves only when a release fixes something that must never be downgraded past. Most releases don’t bump it. A product might ship forty firmware versions and reach security version 6.
That’s not fastidiousness, it’s arithmetic. The security version has to be stored somewhere an attacker can’t decrement: OTP, a secure element’s monotonic counter, or on the H743, which has no OTP, an append-only log in a WRP-protected flash sector that only the bootloader writes. That storage is one-way and finite. A thermometer code, where version n means n units burned, spends one unit per increment, and the unit depends on how the part programs. On OTP that programs individual bits it is one bit. On H7 flash, which programs 256-bit flash words with ECC and cannot program a written word again, it is one 32-byte flash word, so a 128 KB sector gives about 4,096 increments. And “only the bootloader writes it” needs a named mechanism, such as WRP the bootloader applies before it jumps, which per the readout protection section only binds running code at RDP Level 2. Bump the security version on every release and you’ll eventually run out; bump it only on security-relevant releases and you never will.
The check is trivial once the storage decision is right:
if (image_hdr.security_version < secver_read_min()) {
fault_record(SEC_ROLLBACK_ATTEMPT, image_hdr.security_version);
return BOOT_REJECT;
}
secver_read_min() reads whichever monotonic store the part actually has: OTP where it exists, a secure element’s counter, or on the H743 a write-protected flash-word log that only the bootloader appends to.
And the ordering rule: burn the new security version only after the new image has proven itself. Burn it at install time and a failed update leaves you unable to roll back to the working image, because the working image is now below the minimum. That’s a self-inflicted brick, and it’s the tightest coupling point between this article and article 12’s boot confirmation.

Provisioning: The Factory Is a Trust Boundary
Every device needs a unique identity. How it gets one determines whether your private keys ever exist somewhere they can be stolen in bulk.
Injected keys. A provisioning station generates a keypair and writes the private key to the device. Simple, works with any part. It also means every private key in your fleet existed, at some point, on a PC in a factory, possibly one you don’t own, in a country whose legal system you haven’t thought about. Compromise that station and you have the whole production run.
On-device generation. The device generates its own keypair using its hardware TRNG, exports only the public key, and the factory station forwards that to a CA for signing. The private key never leaves the part and never existed anywhere else.
On-device generation is the better design, provided the TRNG is healthy, and the H7 has one. Check the RNG seed and clock error flags during provisioning and reject a unit that reports them. The costs are real but modest: a few hundred milliseconds per unit at test, a provisioning protocol rather than a flash write, and a CA the factory can reach. For anything with a meaningful fleet size, it’s the right answer.

Either way, decide what happens to the provisioning capability afterwards. A device that will re-provision on request has a permanent identity-replacement interface, which is either a service feature or a vulnerability depending entirely on how it’s authenticated.
STM32H7 Readout Protection (RDP): The Debug Lock You Can’t Undo
STM32 readout protection has three levels, and the choice between the last two is one of the more consequential irreversible decisions in a product.
Level 0. No protection. Full SWD access, flash readable. Development only.
Level 1. With a debugger connected, or when booting from RAM or system memory, user flash and backup SRAM are inaccessible. Not everything is: a debugger can still attach, halt the core and read ordinary SRAM and registers, so any key the firmware has unwrapped into RAM is exposed. Regressing to Level 0 triggers a mass erase, but not of everything. PCROP areas survive the regression unless their DMEP bit is set, and on H75x parts the secure user area survives unless DMES is set, which is why the PCROP key advice above comes with DMEP attached.
Level 2. Debug permanently disabled. The JTAG/SWD port is dead. There is no unlock sequence, no ST recovery procedure, no regression path, and WRP can no longer be changed. Permanent.
For the KIC-400, ship at Level 2, and the reason is the root of trust rather than readout. The bootloader is protected by WRP, and so is the anti-rollback log. At Level 1 neither protection binds running code: the OPTKEY values are public, so a compromised but authentic application can unlock the option bytes, clear WRP, rewrite the bootloader and erase the log, and every guarantee earlier in this article goes with them. Level 2 is the only point at which WRP becomes immutable on an H743.
Level 1 is not nothing. It blocks flash readout over SWD by ordinary means, and published attacks on it took real effort on other STM32 families (Obermaier and Tatschner, USENIX WOOT 2017, on the STM32F0, for example), which is exactly why per-device keys matter: extracting one unit should never compromise the fleet. But it leaves SRAM readable by an attached debugger and the option bytes writable by firmware, and on a design rooted in WRP the second point decides it.
What Level 2 costs is every failure analysis you’ll ever want to do. A unit comes back from the field with an unexplained fault. At Level 1 you can, with the right procedure, still work with it. At Level 2 you have a brick with an interesting story and no debug port to hear it through, so the RMA path has to be built into the firmware: article 07’s crash records, read out through a diagnostic command authenticated with a per-device key. The decision is irreversible, and over a ten-year product life with field returns the cost recurs.
There are two alternatives. Stay at Level 1 and document that the bootloader is protected only against attackers who cannot run code on the device, which is a weak claim for a networked product. Or move to an H75x part and put the bootloader in a secure user area under secure access mode, which application code cannot read or modify, and keep Level 1 and its debug access for failure analysis.

Whichever you choose: test the transition on real hardware, early. The option-byte sequence, the timing of when it’s applied in production, and the interaction with your programming tooling all have sharp edges, and finding them during a production ramp is expensive.
Runtime: Authentic Isn’t the Same as Safe
Secure boot guarantees the code that started was yours. It says nothing about what that code does with a malformed Modbus frame.
Without TrustZone on an H7, your runtime tools are the MPU and the privilege split. What they’re genuinely worth:
Keep protocol parsers unprivileged. The Modbus and MQTT parsers handle attacker-controlled bytes. Run them unprivileged with an MPU region covering only their own stack and buffers. A parser bug then faults instead of writing wherever the bug points, and it becomes a recoverable fault from article 07 rather than arbitrary code execution. Budget for the cost: on FreeRTOS this means the FreeRTOS-MPU port and xTaskCreateRestricted(), working within the M7’s 8 or 16 MPU regions, and integration work to keep lwIP and Mbed TLS calls on the right side of the privilege line.
Make key material unreadable from parsing contexts. If the MPU denies the parser’s region access to the key storage area, a memory disclosure bug in the parser cannot leak keys, even if it’s exploitable for other things.
Mark data regions execute-never, the RAM that receives downloaded data and the inactive flash bank until its image is verified, so a buffer can’t become code.
None of this stops a determined attacker who finds a privileged bug. It converts a large class of parser bugs from “device compromise” to “device faults and records the reason,” and given that parser bugs are the most likely vulnerability you’ll actually ship, that conversion is worth the MPU configuration work.

Input validation is the other half, and article 07’s defensive boundary applies unchanged: lengths, indices, counts, ranges, checked at the point untrusted data enters, every time.
When Verification Fails
A production device must do something better than while(1).
The options and their trade-offs:
Boot the previous known-good image. Best for availability. Needs anti-rollback to distinguish “reverting to the image we were running yesterday” from “an attacker forcing a downgrade”, and that distinction is why the previous image’s security version must still be at or above the stored minimum.
Enter recovery mode. Article 07’s minimal firmware. Good, with one caveat worth stating: an attacker who can corrupt flash can force you into recovery mode. If recovery mode has fewer checks than the main image (and it usually does, because it’s small and was written under time pressure), you’ve given them a downgrade attack. Recovery firmware needs the same signature verification and the same scrutiny as the main image.
Halt in a safe state. Right for safety-critical products where operating on unverified code is worse than not operating. Wrong for most industrial gear, where a controller that refuses to boot is itself the incident.
Record the failure either way, with the reason and the rejected image’s version, in the backup-SRAM record article 07 built. A device that has rejected forty images in a week is telling you something that no single boot log would.

And bound the retries. Verification uses only public data, the image and the public key, so there is no secret for a timing oracle to leak. What unlimited attempts do buy an attacker is denial of service (a device busy verifying is not doing its job, and on a battery product it drains the battery), flash wear if each attempt rewrites a slot, and as many tries as they want at glitching the check. Verification failures should back off, and they should count.
The Regulatory Floor
Security is becoming a market-access requirement rather than a differentiator.
The EU Cyber Resilience Act, Regulation (EU) 2024/2847, sets obligations for products with digital elements sold into the EU: security by design, vulnerability handling for a declared support period that should generally be at least five years, coordinated disclosure, and reporting. The dates are staged. The obligation to report actively exploited vulnerabilities and severe incidents affecting the product’s security has applied since 11 September 2026, and the main obligations apply from 11 December 2027, so a product being architected today will ship into both.
The practical consequences reach straight back into this series. You need a way to ship security fixes for the product’s entire declared support life, which is article 12’s OTA architecture and article 10’s ten-year concerns. You need a software bill of materials, which means knowing every third-party component in your image and its version. And you need to know which units are running what, which is article 11’s observability.
None of that is bolt-on. The dates are fixed in the regulation, but check which category your product falls into before committing to a compliance plan, because important and critical products face stricter conformity assessment.
Next
Every mechanism in this article has the same weakness: it’s only as good as its failure path, and its failure path is the least-tested code in the product.
Does your bootloader actually reject an image with a flipped bit in the signature? Does anti-rollback actually block a downgrade, or does the comparison have an off-by-one? Does the parser actually fault instead of writing past its buffer? You can’t answer those by reading the code, and you can’t answer them by testing on hardware alone.
Which raises the question this series has deferred since article 01, when the interface pattern first appeared: how do you build firmware you can actually test: on a host, exhaustively, including the paths that only execute when something is wrong?
Article 09: Designing Firmware for Testability: unit testing, mocks, fault injection, and hardware-in-the-loop.
References
- STM32H743ZI documentation (RM0433 reference manual and datasheet)
- Armv7-M Architecture Reference Manual
- Modbus specifications, including Modbus over Serial Line and Modbus/TCP Security
- Regulation (EU) 2024/2847, the Cyber Resilience Act
- Obermaier and Tatschner, Shedding too much Light on a Microcontroller’s Firmware Protection, USENIX WOOT 2017
Related on this site: Secure Boot and OTA Firmware Updates on Arm Cortex-M covers the implementation side on TrustZone parts with MCUboot; this article covers the architecture decisions that apply to any part.
Series Index
Overview, reading paths and related articles: Embedded Firmware Architecture series guide.
Phase 1: Decide
01. Embedded Firmware Architecture Fundamentals
02. Production-Grade Firmware Architecture
03. Firmware Architecture Anti-Patterns
Phase 2: Build
04. HAL vs BSP vs Drivers vs Middleware
05. RTOS Architecture
06. State Machine Architecture
Phase 3: Fail safely
07. Error Handling & Recovery
08. Firmware Security Architecture (you are here)
Phase 4: Verify
09. Designing Firmware for Testability
Phase 5: Operate
10. Designing Firmware for 10-Year Products
11. Field Diagnostics & Observability
12. OTA & Safe Firmware Updates