Tutorial

Secure Boot and Firmware Integrity Verification

Implement a verified boot chain on embedded Linux with U-Boot FIT image signing, then attack it: downgrade attacks, unsigned image rejection, and bypasses.

6 min read advanced

Prerequisites

  • Completion of the Buildroot/QEMU cross-compiling tutorial
  • Completion of the UART and serial console tutorial
  • Basic understanding of public-key cryptography
  • Familiarity with U-Boot

Part 6 of 7 in Embedded Systems & Firmware

Table of Contents

The previous tutorials in this series attacked embedded systems: extracting firmware, auditing attack surfaces, exploiting services, and accessing serial consoles. This tutorial flips perspective. You’ll build the defenses, implementing a verified boot chain that rejects tampered firmware, then put on the attacker hat again and try to break them.

Secure boot is the foundation of firmware integrity. Without it, an attacker with physical access (or a firmware update vulnerability) can replace the device’s software entirely. With it, each stage of the boot process cryptographically verifies the next stage before executing it, forming a chain of trust from hardware to application.

Chain of trust:

  ┌─────────────┐  verifies   ┌─────────────┐  verifies   ┌─────────────┐
  │ Boot ROM    │ ───────────→ │ U-Boot      │ ───────────→ │ Linux       │
  │ (immutable) │  signature   │ (bootloader)│  signature   │ Kernel      │
  └─────────────┘              └─────────────┘              └──────┬──────┘
                                                                   │ verifies
                                                            ┌──────▼──────┐
                                                            │ Root FS /   │
                                                            │ Init System │
                                                            └─────────────┘

We use U-Boot’s FIT (Flattened Image Tree) format with RSA signatures for the verification mechanism. FIT images bundle the kernel, device tree, and optional ramdisk into a single signed package that U-Boot can verify before booting.

Understanding the boot trust chain

What each stage verifies

StageStored inVerifiesTrust anchor
Boot ROMSoC (read-only silicon)U-Boot SPLOTP fuses (hash or key)
U-Boot SPLFlash (first stage)U-Boot properPublic key embedded in SPL
U-BootFlash (second stage)FIT image (kernel + DTB)Public key in U-Boot binary
KernelFIT imageRoot filesystem (dm-verity)dm-verity root hash

In QEMU, we don’t have a real Boot ROM or OTP fuses. We’ll focus on the U-Boot → kernel stage: signing FIT images and configuring U-Boot to reject unsigned or tampered images.

FIT image structure

A FIT image is a device tree blob (.itb file) that contains:

FIT image (.itb):
  /images
    /kernel
      data = <compressed kernel binary>
      type = "kernel"
      arch = "arm"
      compression = "gzip"
      hash { algo = "sha256"; value = <hash>; }
    /fdt
      data = <device tree blob>
      type = "flat_dt"
      hash { algo = "sha256"; value = <hash>; }
  /configurations
    /conf-1
      kernel = "kernel"
      fdt = "fdt"
      signature {
        algo = "sha256,rsa4096"
        key-name-hint = "dev-key"
        sign-images = "kernel", "fdt"
        value = <RSA signature>
      }

U-Boot verifies the RSA signature over the configuration’s hash, which covers the kernel and device tree hashes. If any component is modified, the signature is invalid and U-Boot refuses to boot.

Generating signing keys

Create an RSA key pair for signing FIT images.

mkdir -p ~/secure-boot-lab/keys
cd ~/secure-boot-lab/keys

# Generate an RSA 4096-bit private key. U-Boot's mkimage derives the
# filename from key-name-hint, so this must match "dev-key" in the ITS.
openssl genpkey -algorithm RSA -out dev-key.key \
    -pkeyopt rsa_keygen_bits:4096 \
    -pkeyopt rsa_keygen_pubexp:65537

# Create a self-signed certificate containing the public key.
# mkimage uses this when it writes the public key parameters into U-Boot's DTB.
openssl req -batch -new -x509 -key dev-key.key -subj "/CN=dev-key" -out dev-key.crt

# Optional: view the public key in PEM format
openssl rsa -in dev-key.key -pubout -out dev-key-pub.pem

# View key details
openssl rsa -in dev-key.key -text -noout | head -5
Private-Key: (4096 bit, 2 primes)
modulus:
    00:b3:4f:...
publicExponent: 65537 (0x10001)

Warning

Key management In production, the private signing key must be stored in a Hardware Security Module (HSM) or at minimum an air-gapped machine. Anyone with the private key can sign arbitrary firmware that the device will accept. For this tutorial, a local key is fine.

Creating a signed FIT image

Writing the FIT image description

Create an Image Tree Source (.its) file that describes the FIT image contents:

cat > ~/secure-boot-lab/fit-image.its << 'EOF'
/dts-v1/;

/ {
    description = "Signed ARM kernel + DTB";
    #address-cells = <1>;

    images {
        kernel {
            description = "Linux kernel";
            data = /incbin/("zImage");
            type = "kernel";
            arch = "arm";
            os = "linux";
            compression = "none";
            load = <0x60000000>;
            entry = <0x60000000>;
            hash-1 {
                algo = "sha256";
            };
        };
        fdt-1 {
            description = "Device tree";
            data = /incbin/("vexpress-v2p-ca9.dtb");
            type = "flat_dt";
            arch = "arm";
            compression = "none";
            hash-1 {
                algo = "sha256";
            };
        };
    };

    configurations {
        default = "conf-1";
        conf-1 {
            description = "Signed boot configuration";
            kernel = "kernel";
            fdt = "fdt-1";
            signature-1 {
                algo = "sha256,rsa4096";
                key-name-hint = "dev-key";
                sign-images = "kernel", "fdt";
            };
        };
    };
};
EOF

Building the FIT image

Copy the kernel and DTB to the working directory, then build and sign:

cd ~/secure-boot-lab

# Copy build outputs
cp ~/buildroot/output/images/zImage .
cp ~/buildroot/output/images/vexpress-v2p-ca9.dtb .
UBOOT_BUILD="$(find ~/buildroot/output/build -maxdepth 1 -type d -name 'uboot-*' | head -1)"
cp "$UBOOT_BUILD/u-boot.dtb" .
mkdir -p tftp

# Create the FIT image (unsigned first)
mkimage -f fit-image.its fit-image.itb

# Sign the FIT image with our key
# This also adds the public key to the U-Boot device tree
mkimage -F -k keys/ -K u-boot.dtb -r fit-image.itb

# -F: sign an existing FIT image (don't recreate)
# -k keys/: directory containing dev-key.key and dev-key.crt
# -K u-boot.dtb: U-Boot's *control* DTB (the one U-Boot itself runs against),
#                NOT the kernel DTB you might also have on disk. mkimage rewrites
#                this file in place to add /signature/key-dev-key. If you point
#                -K at the kernel DTB by mistake, the bootloader still has no
#                trust anchor and signature verification will silently fail.
# -r: mark the key as "required" (U-Boot must verify)

# Put the signed artifact somewhere the QEMU guest can fetch it
cp fit-image.itb tftp/

The -r flag is critical. Without it, U-Boot treats the signature as optional; it will warn on failure but boot anyway. With -r, a failed signature check prevents booting entirely.

Verifying the signature

# Inspect the FIT image
mkimage -l fit-image.itb
FIT description: Signed ARM kernel + DTB
 Image 0 (kernel)
  Description:  Linux kernel
  Type:         Kernel Image
  ...
  Hash algo:    sha256
  Hash value:   a3b4c5d6...
 Image 1 (fdt-1)
  Description:  Device tree
  ...
 Configuration 0 (conf-1)
  Description:  Signed boot configuration
  Kernel:       kernel
  FDT:          fdt-1
  Sign algo:    sha256,rsa4096:dev-key
  Sign value:   (RSA signature present)

Configuring U-Boot for verified boot

U-Boot must be compiled with verified boot support and the public key embedded.

U-Boot configuration

In Buildroot’s U-Boot configuration (or standalone U-Boot menuconfig):

cd ~/buildroot
make uboot-menuconfig

Enable:

  • CONFIG_FIT=y, FIT image support
  • CONFIG_FIT_SIGNATURE=y, FIT signature verification
  • CONFIG_RSA=y, RSA algorithm support
  • CONFIG_FIT_VERBOSE=y, verbose signature checking output (for debugging)

Embedding the public key

The public key is embedded in U-Boot’s control device tree (the .dtb that U-Boot itself uses). The mkimage -K u-boot.dtb invocation from the previous step modified that DTB in place, adding a /signature/key-dev-key node containing the public key parameters. We now need U-Boot’s binary to ship with that updated DTB.

arch/arm/dts/ contains the DTS source, not compiled DTBs, so dropping a .dtb there has no effect, the build will recompile from source and overwrite our changes. The supported flow is to re-link U-Boot against an external DTB using EXT_DTB:

# Copy the modified DTB into U-Boot's build directory
UBOOT_BUILD="$(find ~/buildroot/output/build -maxdepth 1 -type d -name 'uboot-*' | head -1)"
cp ~/secure-boot-lab/u-boot.dtb "$UBOOT_BUILD/u-boot-with-pubkey.dtb"

# Re-link U-Boot using the external DTB containing the public key.
# EXT_DTB tells U-Boot's makefile to skip DTS compilation and append
# the supplied DTB to the final binary.
CROSS_COMPILE="$(find ~/buildroot/output/host/bin -maxdepth 1 -type f -name '*-gcc' | head -1 | sed 's/gcc$//')"
cd "$UBOOT_BUILD"
make EXT_DTB=u-boot-with-pubkey.dtb \
     CROSS_COMPILE="$CROSS_COMPILE"

# Replace the Buildroot output artifact with the re-linked binary
cp u-boot ~/buildroot/output/images/u-boot

Note

Verifying the key landed in the binary Confirm the public key is actually present in the rebuilt U-Boot before testing:

fdtget -l "$UBOOT_BUILD/u-boot-with-pubkey.dtb" /signature
# Expected output: key-dev-key

Then confirm the booted U-Boot sees the same control FDT:

U-Boot> fdt addr $fdtcontroladdr
U-Boot> fdt list /signature

If the /signature node is missing from the control FDT U-Boot is actually using, U-Boot will behave as if no key were configured, and signed images will fail with “no signature node found.”

Testing verified boot

Boot QEMU with the signed FIT image:

qemu-system-arm \
    -M vexpress-a9 \
    -m 256M \
    -kernel output/images/u-boot \
    -drive file=output/images/rootfs.ext2,if=sd,format=raw \
    -net nic -net user,tftp=$HOME/secure-boot-lab/tftp \
    -nographic

In U-Boot, fetch and boot the signed FIT image over TFTP:

U-Boot> dhcp
U-Boot> tftpboot 0x62000000 fit-image.itb
U-Boot> bootm 0x62000000
## Loading kernel from FIT Image at 62000000 ...
   Using 'conf-1' configuration
   Verifying Hash Integrity ... sha256,rsa4096:dev-key+ OK
   Verifying Hash Integrity ... sha256+ OK
## Loading fdt from FIT Image at 62000000 ...
   Verifying Hash Integrity ... sha256+ OK
## Booting kernel from FIT Image at 62000000 ...
   Starting kernel ...

The sha256,rsa4096:dev-key+ indicates the signature was verified successfully.

Attacking the verified boot

Now switch to the attacker’s perspective. Try to bypass the verified boot chain.

Attack 1: Tampered kernel

Modify a single byte in the kernel and try to boot it.

cp fit-image.itb fit-image-tampered.itb

# Locate the kernel data inside the FIT and flip one byte.
# zImage carries a magic word (0x016f2818) at offset 0x24 from its start,
# so we can find the kernel's location inside the FIT without parsing
# the FDT structure manually.
python3 << 'EOF'
with open('fit-image-tampered.itb', 'r+b') as f:
    blob = f.read()
    magic = b'\x18\x28\x6f\x01'  # little-endian 0x016f2818
    idx = blob.find(magic)
    if idx < 0:
        raise SystemExit('zImage magic not found in FIT — check arch/compression')
    kernel_start = idx - 0x24
    target = kernel_start + 0x100  # well inside the kernel payload
    f.seek(target)
    original = f.read(1)
    f.seek(target)
    f.write(bytes([original[0] ^ 0xFF]))
    print(f'Flipped byte at offset {hex(target)} ({hex(original[0])} -> {hex(original[0] ^ 0xFF)})')
EOF

Copy the tampered image into the TFTP directory, then load it in U-Boot:

cp fit-image-tampered.itb tftp/
U-Boot> tftpboot 0x62000000 fit-image-tampered.itb
U-Boot> bootm 0x62000000
## Loading kernel from FIT Image at 62000000 ...
   Using 'conf-1' configuration
   Verifying Hash Integrity ... sha256,rsa4096:dev-key-
ERROR: RSA signature verification failed
Bad FIT kernel image format!

The - after dev-key indicates verification failure. U-Boot refuses to boot. The secure boot chain correctly detected the tampered kernel.

Attack 2: Unsigned image

Create a FIT image without a signature:

# Create .its without the signature node
cat > fit-unsigned.its << 'EOF'
/dts-v1/;
/ {
    description = "Unsigned kernel";
    images {
        kernel {
            data = /incbin/("zImage");
            type = "kernel";
            arch = "arm";
            os = "linux";
            compression = "none";
            load = <0x60000000>;
            entry = <0x60000000>;
        };
        fdt-1 {
            data = /incbin/("vexpress-v2p-ca9.dtb");
            type = "flat_dt";
            arch = "arm";
        };
    };
    configurations {
        default = "conf-1";
        conf-1 {
            kernel = "kernel";
            fdt = "fdt-1";
        };
    };
};
EOF

mkimage -f fit-unsigned.its fit-unsigned.itb
cp fit-unsigned.itb tftp/
U-Boot> tftpboot 0x62000000 fit-unsigned.itb
U-Boot> bootm 0x62000000
## Loading kernel from FIT Image at 62000000 ...
   Using 'conf-1' configuration
   Signature is required but not found
ERROR: FIT image not signed

Because we used -r (required) when embedding the key, U-Boot won’t boot unsigned images at all.

Attack 3: Downgrade attack

A downgrade attack replaces the current firmware with an older, vulnerable version that was legitimately signed. The old signature is still valid because the signing key hasn’t changed.

# An old FIT image, signed with the same key
cp fit-image-v1.0.itb tftp/
U-Boot> tftpboot 0x62000000 fit-image-v1.0.itb
U-Boot> bootm 0x62000000
## Loading kernel from FIT Image at 62000000 ...
   Verifying Hash Integrity ... sha256,rsa4096:dev-key+ OK
## Booting kernel ...

It boots. The secure boot chain has no version awareness; it only checks the signature, and the old image’s signature is valid.

Defending against downgrade attacks

Rollback protection requires a monotonic counter: a value stored in non-volatile, tamper-resistant storage that only increases. Each signed image includes a version number, and the boot policy refuses to boot images with a version lower than the current counter value.

Rollback protection flow:

  FIT image version: 3
  Stored counter:    3    → 3 >= 3 → BOOT ALLOWED

  FIT image version: 2   (old image)
  Stored counter:    3    → 2 < 3  → BOOT REJECTED

On real hardware, the counter is stored in OTP (One-Time Programmable) fuses or a Trusted Platform Module (TPM). In QEMU, you can simulate this with a file-backed counter.

U-Boot itself doesn’t ship a single, portable rollback-protection switch, the mechanism is platform-specific because it depends on where the monotonic counter lives. In practice you’ll see one of:

  • OP-TEE-backed counters on ARM TrustZone-capable SoCs, exposed to U-Boot via the tee uclass and queried before bootm.
  • Vendor secure-storage drivers such as NXP’s CAAM/SNVS (CONFIG_FSL_CAAM), TI’s CONFIG_TI_SECURE_DEVICE, or i.MX’s HAB.
  • TPM 2.0 NV counters on platforms with a discrete TPM, accessed through U-Boot’s tpm2 command set.

Whichever backend you use, the implementation pattern is the same, but the comparison is done by board-specific code, a protected boot script, or a vendor secure-boot integration:

  1. Add a rollback-index property to each FIT configuration
  2. Store the current minimum version in secure storage
  3. Compare the image’s rollback index against the stored minimum before bootm and refuse to boot lower-versioned images

Attack 4: Bootloader replacement

If the attacker can replace U-Boot itself (via flash access), the entire chain of trust collapses: a modified U-Boot can skip signature verification.

Defense: The boot ROM (immutable code in silicon) must verify U-Boot before executing it. This is the hardware root of trust. On real SoCs (NXP i.MX, TI AM335x, STM32MP1), the boot ROM checks a hash or signature of the first-stage bootloader against a value burned into OTP fuses.

In QEMU, there’s no hardware root of trust. This is a fundamental limitation of software-only testing: the first link in the chain must be anchored in hardware.

Attack 5: Glitching

Fault injection (voltage glitching, clock glitching, electromagnetic pulses) can cause the processor to skip instructions, including the signature verification check. A well-timed glitch during the if (verify_signature() == FAIL) { halt(); } branch can cause U-Boot to fall through to the boot path.

This is a hardware attack beyond the scope of this tutorial, but it’s the most practical bypass against properly implemented secure boot on real devices. Defenses include:

  • Double-checking verification results
  • Adding timing randomization before checks
  • Using redundant comparison loops
  • Hardware-level glitch detection circuits

Extending the chain: dm-verity for the root filesystem

The FIT signature covers the kernel and device tree, but not the root filesystem. An attacker who modifies /etc/shadow or plants a backdoor binary in the rootfs bypasses kernel-level verification.

dm-verity extends the chain of trust to the filesystem. It’s a Linux device-mapper target that verifies each block of the filesystem against a pre-computed hash tree (Merkle tree) at read time.

# Generate the dm-verity hash tree
veritysetup format /dev/mmcblk0p2 /dev/mmcblk0p3
# Writes hash tree to partition 3
# Outputs: Root hash: a1b2c3d4...

# The root hash must be protected by the verified boot chain. One common
# approach is to embed it in bootargs inside the signed FIT or signed DTB.
# Modern kernels (>= 5.4 with CONFIG_DM_INIT) accept dm-mod.create on the cmdline:
# bootargs=... root=/dev/dm-0 dm-mod.create="vroot,,,ro,0 <data_sectors>
#   verity 1 /dev/mmcblk0p2 /dev/mmcblk0p3 4096 4096 <data_blocks>
#   <hash_start_block> sha256 <root-hash> <salt>"
# See Documentation/admin-guide/device-mapper/verity.rst for the full
# parameter list and the older dmsetup-style syntax.

With dm-verity:

  1. The FIT signature verifies the kernel and the signed configuration data that carries the dm-verity root hash
  2. dm-verity verifies every filesystem block at read time against the hash tree
  3. Any modification to any file on the rootfs causes a hash mismatch → I/O error

This only holds if the kernel command line cannot be changed by an attacker. If U-Boot still loads mutable environment variables or accepts interactive setenv bootargs changes before bootm, the root hash is not protected. Production systems usually disable or lock down the U-Boot console and keep boot arguments in signed FIT configuration data, a signed device tree, or another authenticated source.

Complete chain:

  Boot ROM → U-Boot → FIT (kernel + DTB + bootargs) → dm-verity → rootfs
  hardware    signed    signed (RSA)                    hash tree   verified
  trust       binary                                    (Merkle)    per-block

Practical exercise

  1. Generate an RSA 4096-bit key pair and create a signed FIT image containing the Buildroot kernel and device tree
  2. Configure U-Boot with CONFIG_FIT_SIGNATURE and embed the public key with the required flag
  3. Boot the signed image in QEMU and verify that signature checking passes
  4. Tamper with one byte of the kernel in the FIT image and confirm U-Boot rejects it
  5. Create an unsigned FIT image and confirm U-Boot rejects it
  6. Sign an “old version” image with the same key and confirm it boots (demonstrating the downgrade attack)
  7. Discuss: how would you defend against the downgrade attack in a production device?

This builder-and-breaker exercise develops the dual perspective essential for embedded security work, understanding both how defenses are implemented and where they fail.