The puzzle
The bootloader finds a new image in the secondary slot, and the check value delivered with it matches the one it computes. Is it safe to run? A match tells you that the image is the one the check value was computed for. It does not tell you who computed it. Which checks answer which question, and what must stay secret for the answer to hold?
STEP 1
Checks against accidents: CRCs
A CRC (unit 10, lesson 6) is excellent against random damage: a CRC-32 detects every single-bit error, every error burst up to 32 bits long and all but about one in 2³² random corruptions. The RP2040 boot ROM uses one to reject a blank or half-written boot2 (unit 5, lesson 2). But a CRC is a public, linear function with no secret. Flipping a given bit changes the CRC by a pattern that depends only on the bit’s position and the message length:
for messages of equal length, so whoever changes an image can compute the new CRC, or adjust four bytes to keep the old one. A CRC protects against noise, not against a deliberate attacker.
A CRC is linear: flipping a given bit changes the CRC by a fixed pattern that depends only on the bit’s position and the message length, not on the data. That is why an attacker can patch an image and repair its CRC with a little arithmetic. A cryptographic hash such as SHA-256 is designed so that any change alters about half of its 256 output bits unpredictably, and so that nobody knows a practical way to find a second input with the same hash. The CRC here is the RP2040 boot ROM’s CRC-32/MPEG-2.
STEP 2
Fingerprints: cryptographic hashes
A cryptographic hash such as SHA-256 maps any input to 256 bits so that nobody knows a practical way to find two inputs with the same hash (a collision), or a second input matching a given hash. Any change, even one bit, alters about half the output bits unpredictably. A hash is therefore a trustworthy fingerprint, but only of the image it was computed from, and only if you got the fingerprint from somewhere trustworthy. MCUboot requires a SHA-256 record in every image and compares it with its own computation; that catches corruption, but whoever replaces the image replaces the record too. The hash earns its place because the signature covers it.
STEP 4
Private and public keys: signatures
A digital signature splits the key. The vendor signs with a private key that never leaves its signing system; devices verify with the matching public key, which is not secret. MCUboot hashes the header, payload and protected records, signs that hash (ECDSA, Ed25519 or RSA-PSS, chosen when the bootloader is built), and verifies
with a public key built into the bootloader (or, as lesson 4 shows, one whose hash is stored in hardware).
↑ This step uses the figure at the top of the page.
Be precise about what a valid signature proves: that the image was signed with the private key matching the verifier’s public key. That proves the image’s origin only as long as the private key has stayed secret, and the verifying code and its public key cannot be changed by the attacker. If either can be rewritten, the check can be removed or the key replaced (lesson 4). And a valid signature says nothing about whether the image is bug-free or current: an old, vulnerable release is still validly signed (lesson 6).
| Check | Needed to create a valid one | Stops random damage | Stops someone who can write the image |
|---|---|---|---|
| CRC | nothing | yes | no |
| SHA-256 stored beside the image | nothing | yes | no |
| HMAC | the shared key, present in every verifier | yes | until any verifier’s key leaks |
| Signature | the private key, held only by the signer | yes | while the private key is secret and the verifier and public key are immutable |
STEP 5
Encryption is a different promise
Encryption provides confidentiality: someone who copies the image in transit, or reads it from an external flash chip, cannot read its contents. It does not provide authenticity. MCUboot’s encrypted images use AES in counter mode, where flipping a bit of the ciphertext flips the same bit of the decrypted image, so decryption alone would not notice a change; MCUboot still hashes and signs the unencrypted image. Its documentation is explicit that encryption does not protect an image in internal flash against someone who can read it through the debug port.
STEP 6
Worked example: why a CRC-32 cannot protect an image
Take the 16-byte message “fw 1.2.3 build 7” and flip bit 21 (counting from the most significant bit of the first byte, as the figure does). Its CRC-32 (the RP2040 ROM’s variant) changes from 0x87083FD8 to 0x9C16196B, a difference of
Flip the same bit of “fw 9.9.9 build 0” and the CRC changes by the same 0x1B1E26B3. Nothing about the data was needed: the change is known in advance. Against random corruption the CRC is still good; a random 32-bit check value matches by chance with probability
Against an attacker it is worth nothing. SHA-256 of the two versions of the first message differ in 134 of 256 bits, with no pattern to exploit; but even SHA-256 only helps if the attacker cannot replace the reference hash, which the signature over it provides, under the assumptions above.
MYTHS AND FACTS
Common misconceptions
The CRC matches, so the image is genuine
A CRC only detects accidents; anyone can compute one.
A hash in the image header proves it came from us
Whoever changes the image recomputes the hash; only a secret-keyed check proves origin.
A MAC is as good as a signature
Every verifier holds the MAC key; a signature’s verifiers hold nothing that signs.
Encrypted firmware can’t be tampered with
Encryption hides content; only a MAC or signature detects changes.
A valid signature means the image is safe to run
It proves who signed it, under the stated assumptions, not that it is the latest or bug-free.
Check yourself
Answer in your head, then open the card.
An image carries its CRC-32 and its SHA-256 in the header. An attacker changes a constant in the code. What else must they change for the bootloader to accept it?
Both the CRC and the SHA-256, which they can compute themselves: neither needs a secret. Only a MAC or a signature would stop them.
A product uses HMAC-SHA-256 with one key programmed into every device. Why does opening a single device threaten the whole product line?
Verifying needs the same key as creating. The key read out of one device lets the attacker compute valid tags for any image, which every other device accepts.
Under what conditions does a valid ECDSA signature on an image prove it came from the vendor?
The vendor’s private key must be secret (never leaked, never used by anyone else), and the attacker must not be able to change the verifying code or the public key it uses. Even then it proves origin, not that the image is recent or correct.
Why should firmware compare a MAC with a constant-time function instead of memcmp()?
memcmp() may return at the first differing byte, so its timing can reveal how much of a guessed tag was right, letting an attacker find a valid tag byte by byte.
Sources (6)
- MCUboot v2.1.0, docs/design.md (“Integrity check”, “Security”, “Protected TLVs”) — the bootloader checks the magic, requires a SHA256 TLV and compares it with the computed hash; a signature TLV must come with a KEYHASH TLV; the hash covers header, payload and protected TLVs, and the signature is over the hash; the bootloader “verifies that an image was signed with a private key that corresponds to the embedded KEYHASH TLV”
- MCUboot v2.1.0, boot/bootutil/src/image_validate.c (bootutil_img_validate) — computes bootutil_img_hash, compares it with the SHA-256 record using boot_fih_memequal, looks up the key by its hash, then calls bootutil_verify_sig(hash, …); unprotected TLVs other than the allowed ones cause a failure
- MCUboot, docs/signed_images.md and docs/encrypted_images.md — “This signs the image by computing hash over the image, and then signing that hash … The public key of this keypair must be included in the bootloader”; encrypted images use AES-CTR, “Hashing and signing also remain functionally the same way as before, applied over the un-encrypted data”, and encryption “does not protect against the possibility of attaching a JTAG and reading the internal flash memory”
- Mbed TLS v3.6.0, include/psa/crypto.h (psa_hash_compare, psa_mac_compute, psa_mac_verify, psa_verify_hash) — “Beware that comparing integrity or authenticity data such as MAC values with a function such as memcmp is risky because the time taken by the comparison may leak information about the MAC value”; verify functions compare “in constant time”
- CPython v3.12.0, Lib/hmac.py — “Implements the HMAC algorithm as described by RFC 2104”: the key is hashed if longer than the block size, padded with zeros to 64 bytes, and XORed with 0x36 for the inner and 0x5C for the outer hash
- Linux v6.10, Documentation/admin-guide/module-signing.rst — signing and verifying with a key pair outside embedded systems: the kernel holds public keys and “will only load validly signed modules for which it has a public key”; “Since the private key is used to sign modules, viruses and malware could use the private key to sign modules”