Skip to main content

TECH VEDA

Embedded Linux on Edge-AI 23rd Sept 2026 enrollingLinux kernel & Device drivers starts on 24th Oct 2026 enrollingCorporate on-site training - Submit proposal Pick your modulesSharpen your kernel skills: deep dives, drivers, Yocto, CVEs, careers — updated daily. Read the blog →Embedded Linux fast track starts 23rd sept 2026 enrollingEmbedded Linux Mastery track starts 23rd sept 2026 enrollingLinux systems engineering starts 23rd sept 2026 enrolling
Debug Stories

The Bootloader That Said OK: A Verified Boot That Verified Nothing

A board with verified boot enabled happily booted a tampered kernel and printed OK. The only clue was one missing word on the boot console. How U-Boot decides what to verify, where the /signature node went, and the two tests that prove enforcement is real.

The Bootloader That Said OK: A Verified Boot That Verified Nothing

The security review of an industrial gateway was going well until the auditor asked the one question nobody had rehearsed: “Show me a boot that fails.”

Verified boot had been ticked off months earlier. The FIT image was signed. The signing key was locked away properly. The boot console printed Verifying Hash Integrity ... OK on every boot, and it had printed it on every boot since bring-up. So the engineer flipped one byte in the middle of the kernel inside the FIT, wrote it back to the eMMC, and powered the board — expecting the satisfying thud of a refused boot.

The board booted. Cleanly. OK and all.

No error. No warning. Penguin, login prompt. The tampered kernel ran as if it had been blessed by the signing ceremony itself. Somewhere between the signing script and the shipped image, verified boot had turned itself off — and nothing, at any point, had said so.

The setting: keys in the control DTB

This gateway boots the classic U-Boot verified-boot arrangement: the kernel, device tree and ramdisk travel together in a FIT image, and each configuration in that FIT is signed. U-Boot holds the public half of the story. The RSA public key is embedded in U-Boot’s own control devicetree — the DTB that describes U-Boot’s world, not the one handed to Linux — under a node called /signature. Each key lives in a subnode (key-dev, say) and carries a property that does all the heavy lifting:

signature {
    key-dev {
        algo = "sha256,rsa2048";
        key-name-hint = "dev";
        required = "conf";
        rsa,modulus = <...>;
        ...
    };
};

required = "conf" says: the selected configuration of any FIT this board boots must carry a valid signature made with this key. mkimage writes all of this for you at signing time — the -K option points at the control DTB to receive the public key, and -r marks the key required:

raghu@techveda.org:~/build$ mkimage -f kernel.its -k keys/ -K u-boot.dtb -r image.fit

That one command is the entire trust anchor. (Where the private half of the key should live is its own decision, which we covered in Secure Boot Key Custody: Who Holds the Signing Key?.) And the command is exactly the problem, because it modifies a file — u-boot.dtb — that the rest of the build pipeline also thinks it owns.

The symptom: two consoles, one word apart

Put a healthy verified-boot board next to the audited gateway and boot both. The healthy board says:

   Using 'conf-1' configuration
   Verifying Hash Integrity ... sha256,rsa2048:dev+ OK
   Trying 'kernel' kernel subimage

The gateway says:

   Using 'conf-1' configuration
   Verifying Hash Integrity ... OK
   Trying 'kernel' kernel subimage

Same banner. Same OK. The only difference is the missing sha256,rsa2048:dev+ between the ellipsis and the OK — the name of the algorithm and key that was actually checked. On the healthy board, that string is printed by the code that verifies the configuration signature against a required key. On the gateway, nothing printed it, because nothing was verified. OK does not mean “signature valid”. It means “the verification stage did not return an error” — and as we are about to see, those are very different statements.

iminfo is just as cheerful on the broken board:

=> iminfo ${loadaddr}
## Checking hash(es) for FIT Image at 82000000 ...
   Hash(es) for Image 0 (kernel): sha256+
   Hash(es) for Image 1 (fdt-1): sha256+

A + after every hash, on a board that holds no keys at all. The hashes are genuine — the image is internally consistent — but internal consistency is exactly what an attacker who modifies the image will preserve.

The mechanism: how U-Boot works out what to verify

The behaviour lives in boot/image-fit-sig.c, and it is short enough to read in full. When U-Boot verifies the selected FIT configuration, fit_config_verify_required_keys() starts like this (current mainline):

	/* Work out what we need to verify */
	key_node = fdt_subnode_offset(key_blob, 0, FIT_SIG_NODENAME);
	if (key_node < 0) {
		debug("%s: No signature node found: %s\n", __func__,
		      fdt_strerror(key_node));
		return 0;
	}

key_blob is the control DTB. FIT_SIG_NODENAME is "signature". If the node is missing, the function returns 0 — success — after a debug() that is compiled out of every normal build. No keys means nothing is required, and nothing required means nothing can fail.

If the node is present, the function walks its subnodes looking for keys with required = "conf", counts them in reqd_sigs, and verifies against each. The closing check is:

	if (reqd_sigs && !verified) {
		printf("Failed to verify 'any' of the required signature(s)\n");
		return -EPERM;
	}

	return 0;

Note the guard: reqd_sigs && !verified. A /signature node whose keys lack the required property — say, because someone dropped -r from the mkimage invocation — leaves reqd_sigs at zero, and the check cannot fire. Two different mistakes, one silent outcome.

The image-level gate, fit_image_verify_required_sigs(), opens with the identical early return, so iminfo and per-image verification sail through the same way. And the + signs it prints are deliberate: the per-signature results in fit_image_verify_with_data() are explicitly non-fatal, with a comment that tells you the whole design in three lines:

	/*
	 * Show an indication on failure, but do not return
	 * an error. Only keys marked 'required' can cause
	 * an image validation failure. See the call to
	 * fit_image_verify_required_sigs() above.
	 */

Only required keys can fail a boot. No /signature node, no required keys. No required keys, no possible failure. The chain of custody for your entire secure boot rests on one devicetree node that nothing checks the existence of.

This is not an accident of refactoring, either — but hold that thought for the postscript.

The real puzzle: where did the node go?

The signing script was correct. Running it by hand produced a u-boot.dtb with a fully populated /signature node; fdtget proved it:

raghu@techveda.org:~/build$ fdtget u-boot.dtb /signature/key-dev required
conf

The shipped boot binary told a different story:

raghu@techveda.org:~/build$ fdtget shipped-u-boot.dtb /signature required
Error at '/signature': FDT_ERR_NOTFOUND

The gap between those two commands was the build pipeline. The signing step ran mkimage -K against the freshly built u-boot.dtb, exactly as documented. But signing had been bolted on as a separate CI stage, and the packaging stage that assembled the final boot binary re-ran the U-Boot build — which regenerated u-boot.dtb from source, byte-for-byte identical to a virgin build, minus the keys that mkimage had injected into a copy the packaging step never saw. The signed FIT shipped. The keyed DTB did not.

Nothing failed, because nothing checks. mkimage -K patches keys into whatever DTB you point it at; it has no opinion about whether that file is the one your board will boot with. U-Boot at runtime looks in the DTB it actually has and, finding nothing, enforces nothing — by design, quietly.

Once you know the shape of this failure, you see the family resemblance everywhere: a devtool/bbappend that rebuilds U-Boot after the signing hook; an A/B updater that “helpfully” refreshes the bootloader partition from a vanilla artifact; a factory that flashes boot firmware from a different build than the rootfs; a downgrade to a DTB that predates the signing rollout. Every one of them produces a board that boots anything, says OK, and passes every test except the one nobody runs.

The fix, part one: make the build refuse to ship it

The durable fix is to treat “keys present and required in the shipped DTB” as a release artifact check, exactly like a checksum. Three lines of shell in the packaging stage close the entire class:

# Gate: the DTB we are about to ship must enforce verified boot
DTB=shipped-u-boot.dtb
if ! fdtget -l "$DTB" /signature >/dev/null 2>&1; then
    echo "FATAL: $DTB has no /signature node - verified boot is OFF" >&2
    exit 1
fi
for key in $(fdtget -l "$DTB" /signature); do
    fdtget "$DTB" "/signature/$key" required >/dev/null 2>&1 || {
        echo "FATAL: /signature/$key has no 'required' property" >&2
        exit 1
    }
done

The first check catches the regenerated-DTB failure. The second catches the forgotten -r. Wire it into the same stage that produces the final image, after every step that could touch the DTB — the whole point is that it runs on the artifact that ships, not on an intermediate.

And fix the ordering bug itself, of course: sign against the final DTB, or better, make key injection the last operation that touches it. If your build system rebuilds U-Boot for packaging, the signing step must run inside or after that rebuild, never in parallel with it.

The fix, part two: prove it on the bench

A build gate checks the artifact. Only a boot test checks the behaviour. The test the auditor asked for should be in your release checklist permanently, and it takes two minutes:

=> load mmc 0:1 ${loadaddr} image.fit
=> bootm ${loadaddr}

First with the genuine image — expect the key name on the console:

   Verifying Hash Integrity ... sha256,rsa2048:dev+ OK

Then with a deliberately tampered image (flip one byte of the kernel data on the host, dd conv=notrunc it back in). A board that is actually enforcing refuses loudly:

   Verifying Hash Integrity ... sha256,rsa2048:dev-  error!
Verification failed for 'conf-1' config node
Failed to verify required signature 'key-dev'
Bad Data Hash

If the tampered image boots, you have this article’s bug — or one of its cousins — no matter what the build logs say. The negative test is the only statement about verified boot that cannot lie to you.

Two habits worth keeping from the fallback side: learn to read the Verifying Hash Integrity line rather than pattern-matching on OK — the algorithm:key-name pair is the receipt, and its absence is the tell; and remember that iminfo‘s + marks are internal-consistency checks, not authorization, unless a required key was actually in play.

Postscript: thirteen years of documented behaviour

While root-causing this we did the git archaeology, and it is worth recording because the trail is mildly boobytrapped. A pickaxe search on today’s file stops in 2021 and offers you a commit that has nothing to do with signatures:

raghu@techveda.org:~/u-boot$ git log --oneline --reverse \
      -S 'No signature node found' -- boot/image-fit-sig.c
19a91f2464 Create a new boot/ directory

That is not an origin, just the commit that moved the file into boot/ — a path-scoped pickaxe stops wherever the path began. Chase the file backwards (common/image-fit-sig.c dead-ends the same way at the 2020 commit that split it out of common/image-sig.c) and rerun the search on the original path, and the real history finally surfaces:

raghu@techveda.org:~/u-boot$ git log --oneline --reverse \
      -S 'No signature node found' -- common/image-sig.c
56518e7104 image: Support signing of images
4d0985295b image: Add support for signing of FIT configurations
b983cc2da0 lib: rsa: decouple rsa from FIT image verification

The first two hits are the origins of both gates. The early return is present, verbatim, in commit 56518e7104 — Simon Glass, June 2013, the commit that introduced required-signature verification itself. Its configuration-gate twin arrived eight commits later in the same series (4d0985295b) — a series that also introduced the -k, -K and -r mkimage options this whole workflow rests on. (The third hit is the 2020 file split carrying the string away again.) And the documentation added alongside them states the contract plainly, in words that survive unchanged in doc/usage/fit/signature.rst today: only required keys are normally verified, and images must verify against required keys if there are any. The fail-open is not a regression. It is the original, documented design — a design that assumed the integrator would put the keys where they belong, and never budgeted for the case where a build pipeline quietly takes them back out.

One closing caution to keep the picture symmetric: everything in this story is about enforcement being silently absent. The verifier can also fail in the opposite direction — parser bugs in the code that reads an untrusted FIT before the signature check completes — which is the failure class we examined in How U-Boot Verified Boot Was Broken by Its Own Image Parser. A complete verified-boot audit covers both: is enforcement present, and is the code enforcing it sound?

That is the real lesson. The mechanism is fine print, and fine print is exactly what secure-boot bring-up is made of. Enforcement you have not watched refuse a bad image is enforcement you do not have.

If you are integrating U-Boot, signing, and image pipelines and want to build this kind of failure literacy systematically — signing flows, key custody, and the negative tests that keep both honest — our Embedded Linux BSP Development course walks through verified boot end to end on real hardware.

Key takeaways

  • In U-Boot’s FIT verified boot, only keys marked required in the control DTB can fail a boot. A missing /signature node — or keys without the required property — silently disables enforcement; the only runtime indication is a debug() message compiled out of normal builds.
  • Verifying Hash Integrity ... OK without an algo:keyname+ receipt in the middle means nothing was verified. iminfo‘s sha256+ marks are internal consistency, not authorization.
  • mkimage -K injects keys into whatever DTB you point it at. Any later build step that regenerates or replaces the control DTB ships a board with verified boot off. Gate the shipped artifact with fdtget, and make the signing step the last thing that touches the DTB.
  • The negative boot test — tampered image must refuse to boot — is the only reliable proof of enforcement. Put it in the release checklist.
  • The fail-open dates, verbatim, to the 2013 commits that introduced FIT signature verification, and matches the documentation written with them. Expect it in every U-Boot you will ever ship; audit accordingly.

Further reading

Was this worth your time?
RB
Raghu Bharadwaj

Founder, TECH VEDA — 20+ years teaching the Linux kernel, device drivers and embedded systems.

Follow on LinkedIn

Get new posts by email

Kernel, embedded Linux and AI-era engineering — a few sharp reads a month. No spam.

We email occasionally and never share your address.