SPL verified boot on QEMU RISC-V means building with qemu-riscv64_spl_defconfig, so a machine-mode SPL loads OpenSBI and U-Boot proper from one u-boot.itb and checks that image’s signature first. Setting CONFIG_SPL_FIT_SIGNATURE is the easy half. The hard half: binman generates no signature node here, an omitted sign-images property silently covers no images, and SPL reads its public key from the devicetree QEMU hands it rather than from anything you built.
This series has built a RISC-V boot chain one link at a time. Part 4 packaged the kernel, devicetree and boot script into one signed FIT that U-Boot checks before booting, and Part 5 fetched that image over TFTP. Both share a gap: nothing checked U-Boot itself. This part closes it with SPL verified boot, and is honest about the link it cannot close.
What you need before you start
Everything Part 4 produced, in the same ~/rv directory: the RSA key pair, the signed fit.itb, and the copy of QEMU’s devicetree carrying the public key. You also need an OpenSBI source tree, because the SPL build consumes OpenSBI rather than producing it. All quoted source and Kconfig text is from the U-Boot v2026.10 tag.
Where SPL verified boot sits in the chain
Consider what Parts 1 to 5 proved. QEMU was started with -bios u-boot.bin, U-Boot came up, and U-Boot verified a signed kernel. The check was real. But nothing checked u-boot.bin: replace that file on the host and the board boots the replacement without complaint.
That matters because U-Boot is not merely the next program in the sequence; it is the program holding the key and performing the check. An attacker who replaces it need not defeat the kernel signature, because they can ship a U-Boot that never checks. Part 5 noted that setenv verify n disables every FIT check silently.
SPL verified boot supplies the missing link. U-Boot’s SPL is a small first-stage loader, and on RISC-V it also has a privilege job: it runs in machine mode, loads OpenSBI and U-Boot proper, and hands U-Boot’s address to OpenSBI, which starts it in supervisor mode. Because SPL loads U-Boot, it is where U-Boot gets checked.
Building SPL: OpenSBI first, then U-Boot
The order is fixed: the U-Boot build packages the OpenSBI binary into its output, so OpenSBI must exist first.
raghu@techveda.org:~$ cd ~/rv
raghu@techveda.org:~$ git clone https://github.com/riscv/opensbi.git
raghu@techveda.org:~$ cd opensbi
raghu@techveda.org:~$ make PLATFORM=genericThat produces build/platform/generic/firmware/fw_dynamic.bin. U-Boot finds it either in its own root directory or through the OPENSBI environment variable.
raghu@techveda.org:~$ export OPENSBI=~/rv/opensbi/build/platform/generic/firmware/fw_dynamic.bin
raghu@techveda.org:~$ cd ~/rv/u-boot
raghu@techveda.org:~$ make qemu-riscv64_spl_defconfig
raghu@techveda.org:~$ make -j$(nproc)Two outputs matter: spl/u-boot-spl.bin, which replaces u-boot.bin as the thing QEMU loads, and u-boot.itb, holding OpenSBI and U-Boot proper. QEMU has no storage to read the second from, so it goes into memory directly.
raghu@techveda.org:~$ qemu-system-riscv64 -nographic -machine virt \
-bios spl/u-boot-spl.bin \
-device loader,file=u-boot.itb,addr=0x80200000The memory map the SPL build commits you to
Read these addresses from the configuration rather than a command line. board/emulation/qemu-riscv/Kconfig sets SPL_TEXT_BASE to 0x80000000, where QEMU loads a -bios binary, and SPL_OPENSBI_LOAD_ADDR to 0x80100000. The defconfig sets CONFIG_SPL_LOAD_FIT_ADDRESS=0x80200000, which is why -device loader uses that number. The fourth address catches people:
config TEXT_BASE
default 0x81200000 if SPL
default 0x80000000 if !RISCV_SMODE
default 0x80200000 if RISCV_SMODE && ARCH_RV64IU-Boot proper moves from 0x80200000 in the supervisor-mode builds of Parts 1 to 5 to 0x81200000 here, and its old address is now where the FIT is loaded. A u-boot.bin left from Part 2 is linked for an address this layout uses for something else. Rebuild rather than reuse.
What is actually inside u-boot.itb
On this board the FIT is not written by hand and there is no .its file to edit. binman generates it from arch/riscv/dts/binman.dtsi, which qemu-virt64.dts includes. Reading that file is how you learn what a signature will cover:
images {
uboot {
description = "U-Boot";
type = "standalone";
os = "U-Boot";
arch = "riscv";
compression = "none";
load = /bits/ 64 <CONFIG_TEXT_BASE>;
uboot_blob: u-boot-nodtb {
};
};
opensbi {
description = "OpenSBI fw_dynamic Firmware";
type = "firmware";
os = "opensbi";
arch = "riscv";
compression = "none";
load = /bits/ 64 <CONFIG_SPL_OPENSBI_LOAD_ADDR>;
entry = /bits/ 64 <CONFIG_SPL_OPENSBI_LOAD_ADDR>;
opensbi_blob: opensbi {
filename = "fw_dynamic.bin";
missing-msg = "opensbi";
};
};
};
configurations {
default = "conf-1";
conf-1 {
description = "NAME";
firmware = "opensbi";
loadables = "uboot";
};
};Two images, named by a firmware property and a loadables property. There is no devicetree image and no fdt property. That absence is conditional: in the real file both sit inside #if !defined(CONFIG_OF_BOARD) || defined(CONFIG_MULTI_DTB_FIT). Here CONFIG_OF_BOARD is enabled, because the board Kconfig carries imply OF_HAS_PRIOR_STAGE and dts/Kconfig defaults OF_BOARD to y if SANDBOX || OF_HAS_PRIOR_STAGE. Remember those two property names.
Turning on SPL verified boot in the config
The option is CONFIG_SPL_FIT_SIGNATURE, and its entry in boot/Kconfig reads:
config SPL_FIT_SIGNATURE
bool "Enable signature verification of FIT firmware within SPL"
depends on SPL_DM
depends on SPL_LOAD_FIT
select FIT_SIGNATURE
select SPL_FIT
select SPL_CRYPTO
select SPL_HASH
imply SPL_RSA
imply SPL_RSA_VERIFY
select SPL_IMAGE_SIGN_INFO
select SPL_FIT_FULL_CHECKMost of that is select, which cannot be switched off while the parent is on. Two lines are imply, which only sets a default: SPL_RSA and SPL_RSA_VERIFY can be turned off afterwards, leaving a build that still reports SPL verified boot as enabled while carrying no RSA verification. The check itself is one block in common/spl/spl_fit.c, inside spl_simple_fit_parse():
if (CONFIG_IS_ENABLED(FIT_SIGNATURE)) {
printf("## Checking hash(es) for config %s ... ",
fit_get_name(ctx->fit, ctx->conf_node, NULL));
if (fit_config_verify(ctx->fit, ctx->conf_node))
return -EPERM;
puts("OK\n");
}Verification runs on the configuration node before any image is loaded, and a failure returns -EPERM rather than continuing.
Adding the signature node binman does not generate
Here is the first thing that will stop you. The binman description above contains no signature node, so a plain make produces an unsigned u-boot.itb: enable SPL verified boot, boot it, and SPL refuses the image built alongside it. Add a signature node under the configuration and ask binman to sign:
conf-1 {
description = "NAME";
firmware = "opensbi";
loadables = "uboot";
signature {
algo = "sha256,rsa2048";
key-name-hint = "dev";
sign-images = "firmware", "loadables";
};
};With fit,sign; on the enclosing fit node, binman passes a key directory to mkimage through -k. It locates that directory from key-name-hint, looking for dev.key, and fails if a FIT’s keys span more than one directory.
The second problem is worse, because it is silent. Omit sign-images and signing still succeeds, covering nothing. tools/image-host.c builds its list of images to hash like this:
static const char default_list[] = FIT_KERNEL_PROP "\0"
FIT_FDT_PROP "\0" FIT_SCRIPT_PROP;
/* If there is an "sign-image" property, use that */
prop = fdt_getprop(fit, noffset, "sign-images", lenp);
if (prop) {
*allow_missingp = 0;
return *lenp ? prop : NULL;
}
/* Default image list */
*allow_missingp = 1;
*lenp = sizeof(default_list);
return default_list;Those three constants are "kernel", "fdt" and "script". This configuration has none of them; it has firmware and loadables. And with the default list allow_missing is set to 1, so absent entries are not an error. The signature then covers the configuration node’s own properties and not one byte of OpenSBI or U-Boot, and SPL prints OK. Naming both properties explicitly is what makes SPL verified boot cover the payload.
Where SPL looks for the public key on this board
The documented way to sign a FIT that SPL verifies writes the public key into SPL’s own compiled devicetree. The example in doc/develop/falcon.rst passes -K build/spl/dts/ti/k3-am625-sk.dtb to mkimage. The flags: -k is the private key directory, -K writes public keys into the named .dtb, -r marks keys required, -F re-signs an existing FIT.
That assumes SPL reads the devicetree it was built with. On this board it does not. The override is in lib/fdtdec.c:
/* Allow the board to override the fdt address. */
if (IS_ENABLED(CONFIG_OF_BOARD)) {
void *blob;
blob = (void *)gd->fdt_blob;
ret = board_fdt_blob_setup(&blob);
if (ret) {
if (ret != -EEXIST)
return ret;
} else {
gd->fdt_src = FDTSRC_BOARD;
gd->fdt_blob = blob;
}
}The guard is IS_ENABLED, not CONFIG_IS_ENABLED, and that difference decides the question. CONFIG_IS_ENABLED picks the symbol for the phase being built, so it can be on in U-Boot proper and off in SPL; IS_ENABLED reads one symbol in every phase. It is not an accident of style: the next block in the same function excludes SPL deliberately with if (!IS_ENABLED(CONFIG_XPL_BUILD)). The devicetree override has no such exclusion.
arch/riscv/lib/board.c provides the weak implementation for every RISC-V board:
__weak int board_fdt_blob_setup(void **fdtp)
{
if (!gd->arch.firmware_fdt_addr)
return -EEXIST;
*fdtp = (ulong *)(uintptr_t)gd->arch.firmware_fdt_addr;
return 0;
}And firmware_fdt_addr comes from the register the previous stage used: in arch/riscv/cpu/start.S, _start runs mv s1, a1 and later stores SREG s1, GD_FIRMWARE_FDT_ADDR(gp). That file is compiled for SPL and U-Boot proper alike, and QEMU passes the devicetree pointer in a1 to whatever it loads with -bios.
So SPL’s control devicetree here is QEMU’s. A public key written into spl/u-boot-spl.dtb is not the tree SPL reads, and -K pointed at that file achieves nothing. For SPL verified boot to work the key must go into the tree QEMU hands over, which is the -dtb file Part 4 created: a host file anyone with write access can change.
Which devicetree reaches U-Boot proper, and whether it is signed
The missing fdt property has a second consequence, and it is the one to take away. When SPL finds no devicetree in the FIT, spl_fit_append_fdt() falls back:
/*
* U-Boot did not find a device tree inside the FIT image. Use
* the U-Boot device tree instead.
*/
if (!gd->fdt_blob)
return node;
size = fdt_totalsize(gd->fdt_blob);
spl_image->fdt_addr = map_sysmem(image_info.load_addr, size);
memcpy(spl_image->fdt_addr, gd->fdt_blob, size);SPL copies its own control devicetree, which we established is QEMU’s, to just after U-Boot proper, then adds a /fit-images node recording what it loaded. spl_invoke_opensbi() walks that node for the U-Boot entry, fills opensbi_info with next_mode = FW_DYNAMIC_INFO_NEXT_MODE_S, and jumps to OpenSBI with that copied tree.
So SPL verified boot protects U-Boot’s code but not the devicetree U-Boot proper runs on, because that tree never travelled inside the signed FIT. And that tree is where Part 4 put the public key U-Boot uses to verify the kernel. Signing u-boot.itb secures the verifier without securing the key the verifier uses.
What this leaves the boot ROM to do
Every chain needs a first link that is trusted rather than checked. U-Boot’s documentation is blunt: the public key must be stored where it cannot be changed, because if it can be changed then the verification is worthless. On the virt machine QEMU is the boot ROM, and it loads spl/u-boot-spl.bin at 0x80000000, passes a devicetree pointer, and checks nothing. So the chain here runs: an unverified SPL, which verifies u-boot.itb, which contains a U-Boot that verifies the kernel.
On silicon the ROM holds a key or key hash in one-time-programmable memory or eFuses, verifies SPL before running it, and cannot be reflashed. QEMU lets you build and test the middle links properly and cannot give you the first at all. Knowing which link is missing is more useful than a setup that looks complete. For teams doing this on real hardware, our training tracks cover the same boot chain on boards where the ROM participates.
How to check this on your own board
Four checks, fastest first. Confirm the options survived the build and that the two imply lines took effect:
raghu@techveda.org:~$ grep -E 'SPL_FIT_SIGNATURE|SPL_LOAD_FIT|SPL_RSA|OF_BOARD' .config
CONFIG_SPL_LOAD_FIT=y
CONFIG_SPL_FIT_SIGNATURE=y
CONFIG_SPL_RSA=y
CONFIG_OF_BOARD=ySecond, confirm the signature node reached the generated FIT and names the right images. An empty result is the silent failure above:
raghu@techveda.org:~$ fdtget -l u-boot.itb /configurations/conf-1
signature
raghu@techveda.org:~$ fdtget u-boot.itb /configurations/conf-1/signature sign-images
firmware loadablesThird, watch the SPL console line. A configuration signature that verified names the algorithm and key hint:
## Checking hash(es) for config conf-1 ... sha256,rsa2048:dev+ OKFourth, in U-Boot proper, confirm which devicetree you landed on. bdinfo prints the boot hart and, when a previous stage supplied a tree, a firmware fdt line with its address. If that line is present, your control devicetree came from the stage before you, not from your build.
raghu@techveda.org:~$ bdinfoLimits, and what was not verified
The Kconfig values, binman description, signing-tool logic and SPL source paths here were read from the U-Boot v2026.10 tree and are quoted from it. The build and the boot were not executed, because the environment used would not compile the tree. The console line shown is the format string from common/spl/spl_fit.c with this signature node’s algorithm and key hint substituted, in the shape the upstream Falcon-mode documentation shows for a real board; it is not a captured transcript. Run the sequence yourself before trusting it on hardware.
Two narrower limits: the sign-images = "firmware", "loadables" form follows the documented pattern that the property lists configuration property names, confirmed against the signing code but not against a published RISC-V example; and binman passes -k to mkimage but not -K, so writing a public key into a devicetree is a separate step on any board.
Key takeaways
- SPL verified boot adds the link Parts 1 to 5 lacked: something that checks U-Boot before running it.
- U-Boot proper moves to
0x81200000in an SPL build, and its old0x80200000becomes the FIT load address. Rebuild rather than reuse. - binman generates
u-boot.itbwith no signature node, so a stock build produces an image SPL refuses. - Omitting
sign-imagesfalls back tokernel,fdtandscriptwith missing entries allowed, so the signature covers no images and SPL still reports OK. Namefirmwareandloadables. CONFIG_OF_BOARDis guarded byIS_ENABLED, so it applies in SPL. A key inspl/u-boot-spl.dtbis not the key SPL uses.- With no devicetree in the FIT, SPL copies its control tree onward. That tree holds Part 4’s kernel key and the signature does not cover it.
- QEMU checks nothing before SPL, so these links still need a boot ROM anchored in OTP or eFuses.
Frequently asked questions
Why does a stock SPL build produce an image SPL will not accept?
Because arch/riscv/dts/binman.dtsi, which binman uses to generate u-boot.itb, has no signature node. The image is unsigned, so fit_config_verify() fails and spl_simple_fit_parse() returns -EPERM.
What happens if I leave out the sign-images property?
Signing succeeds but covers nothing useful. tools/image-host.c falls back to a default list of kernel, fdt and script and sets allow_missing to 1. This configuration names its images through firmware and loadables, so no image is hashed and SPL still prints OK.
Can I put the public key in spl/u-boot-spl.dtb on QEMU RISC-V?
You can write it there, but SPL will not read it. CONFIG_OF_BOARD is guarded with IS_ENABLED in lib/fdtdec.c, so it applies in SPL, and board_fdt_blob_setup() returns the pointer QEMU passed in a1.
Does SPL verified boot also protect the key U-Boot uses to verify the kernel?
No. The FIT holds no devicetree here, so spl_fit_append_fdt() copies SPL’s control tree onward to U-Boot proper. Part 4’s public key lives in that tree, which the signature does not cover.
Further reading
- U-Boot docs: QEMU RISC-V, Running U-Boot SPL
- U-Boot docs: Verified Boot
- U-Boot docs: FIT Signature Verification
- U-Boot docs: Signed configurations
- U-Boot docs: Falcon Mode, with a signed-FIT SPL example
- v2026.10: arch/riscv/dts/binman.dtsi
- v2026.10: board/emulation/qemu-riscv/Kconfig
- v2026.10: common/spl/spl_fit.c
- v2026.10: common/spl/spl_opensbi.c
- v2026.10: lib/fdtdec.c
- v2026.10: arch/riscv/lib/board.c
- v2026.10: tools/image-host.c
- v2026.10: tools/binman/etype/fit.py
- OpenSBI docs: QEMU RISC-V virt platform




