Sitemap
Raphael Krauthann

Raphael Krauthann

Verified/Secure Boot and Encryption on Raspberry Pi 4 “from scratch”

15 min readMar 20, 2026

--

I am focussing on Security for Smart Devices and learning a lot in this context. Sadly, many manufacturers do have their secure boot flows and One Time Programmable (OTP) / e-fuse writing process under a strict NDA and are not publicly available.

Currently, I am professionally dealing with Texas Instrument’s (TI) line of Jacinto™ 7 processors, and honestly, their documentation is a pain. I wouldn’t dare to say that it is incomplete, just so extensive and bulky that it's utterly impossible to find anything if not guided by their customer support. Also, their security features are locked behind a special subset of their processors — so-called High Security (HS) chips — that are not easily obtainable, making experimentation completely impossible. This means, that some boards that claim to have secure boot support (like the Beaglebone AI-64, do not offer these features, since they use the General Purpose (GP) version of the TDA4VM processor.

Since this is really irritating, I want to show in this blog post how easy it is to implement a secure boot flow if you deal with a good supplier and documentation. This still took me quite a while, but I want to show how we can implement all of this in a real-world scenario. This means we include:

  • Ensuring only OUR software runs (often called secure boot); since tamper resistance is required by many regulations like IEC 62443–4, Security Level 2 and above
  • Encrypting the root file system (rootfs); since this is where many manufacturers store their Intellectual Property (IP)
  • Keeping the system updateable (OTA); since almost no system on the market survives without regular features and security updates

This guide will be a step-by-step showcase. Feel free to fast-forward if you are familiar with embedded Linux, or just care for a specific chapter.

I do not claim to be all-knowing, if at any point you feel like you have a better solution, please share so we can all learn from each other!

Also two more important points:

  • I am completely developing this on Linux (An Ubuntu container running on Apple M1 Mac Pro with Parallels virtualization). Please do not ask me how to do it on any other Operating Systems
  • If done correctly, you will break your Raspberry PI, this is after all the point when you want to create a tamper-resistant system

(Optional) Implementing a basic image

Okay, before we can get started we create a very simple image from scratch. Maybe you do not need to do this, since your image might be created with a toolchain like yocto, Elbe, or buildroot (I would actually recommend doing it with them instead of creating everything from yourself, but I want this article to be as generic as possible).

Basically, we will create three artifacts:

  • Our boot files (will be called BOOT)
  • Our root filesystem (will be called ROOTFS)
  • Our opt directory (will be called OPT)

OPT will be a persistent partition on our system, surviving updates as we go further. Often, you have more persistent partitions (good candidates are /home and /var). We also create more partitions, to keep our system AB-Updateable.

BOOT Files

To keep it really simple to boot into the system we will use u-boot and the raspberry pi kernel along with some dtbs. We will use the generic aarch64-linux-gnu cross-compilation tools. To build u-boot for raspberry pi 4:

git clone https://source.denx.de/u-boot/u-boot.git
cd u-boot
export CROSS_COMPILE=aarch64-linux-gnu-
make rpi_arm64_defconfig
make -j

We also need u-boot to create a boot script:

fatload mmc 0:1 ${kernel_addr_r} Image
setenv bootargs 8250.nr_uarts=1 console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait init=/bin/bash
booti ${kernel_addr_r} - ${fdt_addr}

And compile it to make it readable for u-boot:

./u-boot/tools/mkimage -A arm64 -O linux -T script -C none -d boot_cmd.txt boot.scr

We also download and compile the kernel files:

git clone -b rpi-6.2.y https://github.com/raspberrypi/linux
cd linux
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
make -j

ROOTFS
For convenience, I will use a basic Debian rootfs which I create with debootstrap. This has the additional advantage that I can easily chroot into this directory from my Ubuntu development environment. We use the current bookworm version.

sudo debootstrap --arch=arm64 --variant=minbase bookworm rootfs

And that’s already it! We will need some changes later, but for a basic bootable image that is already enough.

Gluing it all together
We create a simple partition scheme:

dd if=/dev/zero of=test.img bs=1M count=512
LOOP_DEVICE=$(sudo losetup -fP --show test.img)
sudo fdisk $LOOP_DEVICE <<EOF
o
n
p
1

+128m
a
t
c
n
p
2


w
EOF

sudo fdisk $LOOP_DEVICE <<EOF
n
p
1


w
EOF

sudo mkfs.vfat ${LOOP_DEVICE}p1
sudo fatlabel ${LOOP_DEVICE}p1 BOOT
sudo mkdir /mnt/boot
sudo mount ${LOOP_DEVICE}p1 /mnt/boot
sudo cp linux/arch/arm64/boot/Image /mnt/boot
sudo cp linux/arch/arm64/boot/dts/broadcom/bcm2711-rpi-cm4.dtb /mnt/boot
sudo cp u-boot/u-boot.bin /mnt/boot
sudo cp firmware/boot/{bootcode.bin,start4.elf} /mnt/boot/

sudo cp boot.scr /mnt/boot

cat << EOF > config.txt
enable_uart=1
arm_64bit=1
kernel=u-boot.bin
EOF
sudo mv config.txt /mnt/boot/
sudo umount /mnt/boot

sudo mkfs.ext4 ${LOOP_DEVICE}p2
sudo mkdir /mnt/rootfs
sudo mount ${LOOP_DEVICE}p2 /mnt/rootfs
sudo cp -r rootfs/* /mnt/rootfs/
sudo umount /mnt/rootfs
sudo losetup -D

We now simply flash the generated test.img file onto the device or SD card — either directly with dd or with a tool like balena etcher or the official raspberry pi imager — and see if it boots. If we did everything correctly we can use serial pins 8 and 10 to see a complete boot log.

Secure Boot (with EEPROM)

For now, we want to implement it very safely without already blowing the OTP e-fuses. This means this is not irreversible, you will see in a moment how these steps can easily be revoked. Still, it is a good starting point to see if we use the toolchain correctly.

The Raspberry Pi Foundation has some excellent guides in their repositories, so I will just stick to them (this section is referring to this example). We start by cloning the usbboot repository and installing a Python package we will need in a second:

python3 -m pip install pycryptodomex
git clone https://github.com/raspberrypi/usbboot.git
cd usbboot
make

You might already know this repository if you are using a compute module since it contains the rpiboot utility that is mostly used to bring the Pi into mass storage for flashing.

IMPORTANT: This is where this guide may be a bit different to what you need to do on a Raspberry Pi 4 B or CM4 LITE. You can still use the tooling but instead of using the allmighty rpiboot command, you might need to copy the filed onto the boot partition of your firmware SDCard. I am developing on a normal Raspberry Pi CM4 with eMMC.

Of course, since we are securing the boot, we will need a key, you can generate it wherever you want, just make sure the export its location in the $KEY_FILE environment variable. We will use this key for the entirety of this blog, so do not lose it!

openssl genrsa 2048 > secure-boot.key
export KEY_FILE=$(pwd)/secure-boot.key

Within the usbboot repository, we go into secure-boot-recovery and generate boot files that flash the EEPROM:

cd secure-boot-recovery
# Generate the signed EEPROM image.
../tools/update-pieeprom.sh -k "${KEY_FILE}"
cd ..
./rpiboot -d secure-boot-recovery

The -d parameter makes the Pi boot into the boot files of a specific directory, which makes it easier to use than copying the files onto the boot partition and rebooting the Pi every time.

If we do now reset our boot mode jumper or switch on our compute module board we will get the following output on the serial:

...
secure-boot
Loading boot.img ...
Error 6 loading boot.img
...

Wait… what is a boot.img? Well, according to the official Pi hardware documentation, it is a ramdisk that is primarily meant to be used for secure boot and network boot modes.

Luckily it is very easy to create such a ramdisk:

# you are limited to 96MB for a boot.img file
dd if=/dev/zero of=boot.img bs=1M count=8
LOOP_DEVICE=$(sudo losetup -fP --show boot.img)

sudo mkfs.vfat ${LOOP_DEVICE}
sudo fatlabel ${LOOP_DEVICE} BOOT
sudo mkdir /mnt/boot
sudo mount ${LOOP_DEVICE} /mnt/boot
sudo cp u-boot/u-boot.bin /mnt/boot
sudo cp firmware/boot/{bootcode.bin,start4.elf} /mnt/boot/
sudo cp config.txt /mnt/boot/
sudo cp linux/arch/arm64/boot/dts/broadcom/bcm2711-rpi-cm4.dtb /mnt/boot

sudo umount /mnt/boot
sudo losetup -D

So not every file got moved into the boot.img file. A quick visualization will help:

Current File Structure on the Raspberry Pi CM4

Note that we need the .dtb file twice. This is necessary because the initial boot code and the u-boot boot flow need it, but u-boot cannot access the boot.img files easily. More on that later!

And yes, this means with this step the u-boot script, kernel image, and rootfs are not yet signed, we will get to that in a second.

Now there is only one problem to solve: how do we get the system into mass storage mode again to update our newly generated boot.img file? There are two options, you can either revoke the EEPROM changes we did with:

./rpiboot -d recovery

… or you can use the secure-boot-msd subdirectory of usbboot and sign the boot.img there. (This is a spoiler on how to sign images, but we will revisit this in a moment).

cd secure-boot-recovery
../tools/rpi-eeprom-digest -i boot.img -o boot.sig -k "${KEY_FILE}"
cd ..
./rpiboot -d secure-boot-msd

Well, time to actually sign our boot.img file. Go to the directory containing boot.img and call the usbboot tool:

<path to usbboot>/tools/rpi-eeprom-digest -i boot.img -o boot.sig -k "${KEY_FILE}"

Now you can copy all the files to the boot partition and boot up the Pi:

...
secure-boot
Loading boot.img ...
SIG boot.sig 64cf8389a52ea705d756fa8f05f033e8bd6a2795698c01973762fa9cb38dfbca9
Verifying
RSA verify
rsa-verify pass (0x0)
...

IMPORTANT: You will need to repeat the sigining process everytime you recompile u-boot. Maybe you want to make a script for mounting the boot.img, updating u-boot, unmounting and signing, such that you can experiment more easily on this!

Securing The Kernel

Preparing for Using FitImages

We now have established our root of trust, the problem is, that — as far as I know — U-Boot cannot simply load the kernel from within the boot.img file we just created. This means we need to store the kernel image on the boot partition and we need to come up with a different solution to secure it.

Get Raphael Krauthann’s stories in your inbox

Join Medium for free to get updates from this writer.

Let’s start by modifying u-boot to accept fitimages. Basically a fitimage bundles many boot files in a u-boot-readable single binary. This happens by using the device tree blob parsing already common in kernel and driver development.

CONFIG_FIT = y

Now of course we need to create a fitimage to replace our kernel and FDT. We will use the pre-build cm4 FDT we are already familiar with and add our kernel. Create a fitimage.its file like so:

/dts-v1/;
/ {
description = "Raspberry Pi Example Fitimage";
images {
kernel {
description = "kernel";
data = /incbin/("./linux/arch/arm64/boot/Image");
type = "kernel";
arch = "arm64";
os = "linux";
compression = "none";
load = <0x00080000>;
entry = <0x00080000>;
hash {
algo = "sha1";
};
};

fdt {
description = "device tree";
data = /incbin/("./linux/arch/arm64/boot/dts/broadcom/bcm2711-rpi-cm4.dtb");
type = "flat_dt";
arch = "arm64";
compression = "none";
hash {
algo = "sha1";
};
};

};

configurations {
default = "standard";
standard {
description = "standard";
kernel = "kernel";
fdt = "fdt";
};
};
};

… and use the u-boot tooling to create an actually flattened tree:

./u-boot/tools/mkimage -f fitimage.its image.itb

For now, delete the old kernel and script and move the generated image.itb to the boot partition, now your filesystem should look like this:

Replaced the kernel with image.itb

Boot the system once. You will notice, that we are currently not automatically booting into the rootfs! You can try out the fitimage boot for now by using the U-Boot terminal:

setenv bootargs 8250.nr_uarts=1 console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait init=/bin/bash
fatload mmc 0:1 0x1F00000 image.itb
bootm 0x1F00000

You should now see your system booting normally into the kernel and load the rootfs.

To reimplement the automatism, we do not want to use the script feature but compile to boot command into u-boot such that it gets verified by our chain of trust.

For this set the CONFIG_BOOTCOMMAND variable in the u-boot build to:

usb start; setenv bootargs 8250.nr_uarts=1 console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait init=/bin/bash; fatload mmc 0:1 0x1F00000 image.itb; bootm 0x1F00000;

Now this should work again, and you should not need to enter anything to u-boot in order for it to boot properly!

Signing the FitImages

Okay that out of the way, we can reveal why we use FitImages in the first place: They already have a signing-process build-in! You probably already noticed, that our .its file already contains SHA-1 signatures for error detection, we can modify this mechanism a bit to actually use RSA-2048 signatures as well!

We start by creating a certificate from our previously used private key (yes we will reuse the key that also signs our boot.img):

# navigate to the directory you created the secure-boot.key in earlier
openssl req -batch -new -x509 -key secure-boot.key -out secure-boot.crt
export KEY_DIR=$(pwd)

Then you need to add the signature to your fitimage.its, notice that we only sign the configuration we are using:

/dts-v1/;
/ {
...
configurations {
default = "standard";
standard {
description = "standard";
kernel = "kernel";
fdt = "fdt";
signature-1 {
algo = "sha1,rsa2048";
key-name-hint = "secure-boot";
sign-images = "fdt", "kernel";
};

};
};
};

Now use a modified version of u-boots mkimage command we also used earlier:

cp firmware/boot/bcm2711-rpi-cm4.dtb ./
./u-boot/tools/mkimage -f fitimage.its -K bcm2711-rpi-cm4.dtb.dtb -k $KEY_DIR -r image.itb

Almost done! Now we need to have a unique copy of the cm4’s dtb; in it, the public key for verification is written. We need to add support for signature verification:

# navigate to u-boot repo
# you might want to use make menuconfig
CONFIG_FIT_SIGNATURE=y
CONFIG_RSA=y

… and recompile u-boot:

make -j

IMPORTANT: You might have heard about using EXT_DTB to add the new dtb to the u-boot binary (or simply concatenating the dtb to it). In our case that is not needed, since we simply update the .dtb in the boot.img which is already verified! This is due to us using the rpi_arm64_defconfig in the u-boot build that reuses the dtb from the proprietary bootloader. You might need to add the dtb to your u-boot.bin if you are using another variant (like the Pi4-B)!

If we update the image.itb, and the boot{.img, .sig} (containing the new u-boot.img AND bcm2711-rpi-cm4.dtb) we should find something like this in our u-boot messages:

23306259 bytes read in 1179 ms (18.9 MiB/s)
## Loading kernel from FIT Image at 01f00000 ...
Using 'standard' configuration
Verifying Hash Integrity ... sha1,rsa2048:secure-boot+ OK
Trying 'kernel' kernel subimage
Description: kernel
Type: Kernel Image
Compression: uncompressed
Data Start: 0x01f000b4
Data Size: 23249408 Bytes = 22.2 MiB
Architecture: AArch64
OS: Linux
Load Address: 0x00080000
Entry Point: 0x00080000
Hash algo: sha1
Hash value: 6fa8314974c0aa132bc95697aacb41f75423d005
Verifying Hash Integrity ... sha1+ OK
## Loading fdt from FIT Image at 01f00000 ...
Using 'standard' configuration
Verifying Hash Integrity ... sha1,rsa2048:secure-boot+ OK
Trying 'fdt' fdt subimage
Description: device tree
Type: Flat Device Tree
Compression: uncompressed
Data Start: 0x0352c398
Data Size: 55027 Bytes = 53.7 KiB
Architecture: AArch64
Hash algo: sha1
Hash value: 0d2f48c9b6bfbc1b11a37041ddf9c9d288185f85
Verifying Hash Integrity ... sha1+ OK
Booting using the fdt blob at 0x352c398
Working FDT set to 352c398
Loading Kernel Image
Using Device Tree in place at 000000000352c398, end 000000000353ca8a
Working FDT set to 352c398

Starting kernel ...

And can see that our verification was successful!

Securing the ROOTFS

Okay, that was quite the journey until we got here, but we are not done yet! for now, we have verified all steps until the kernel, but it will load any rootfs that lie on mmcblk0p2 in ext4 format. Also, our rootfs is just here unencrypted, meaning that anyone with a soldering iron can easily steal all of our precious IP! Also remember, we want our system to be updatable, so probably an A/B rootfs partition layout should be considered!

At this point we basically have two options:

  • Using an initial ram disk to manage all of this in our own code
  • Implementing everything with kernel options (and maybe some custom patches)

Even though implementing the required mechanisms with the kernel has the major advantages of being much faster and more space-efficient. We will go for the initial ram disk (or initramfs) solution. This maximizes versatility and allows us to just reuse the standard kernel. Additionally, there is good news: we do not need an extra step to verify the initramfs since we can simply add it to the fitimage. This has the additional feature that we do not need to compile the hash of our rootfs into our kernel, allowing us the update our bootloader and the rootfs independently!

Basically, we need two technologies for this:

  • dm-verity: Device Mapper support to verify the integrity of our filesystems
  • dm-crypt: Device Mapper support to encrypt or partitions

IMPORTANT: Why using crypt and verity? Well we are going basically for an AES256 encryption. It should be noted that an attacker can make assumptions about our encryption key, if they replace the encrypted blocks with their own. Therefore we need to use both, starting with the verification.

The new Partition Layout

Our old partition layout just doesn’t cut it anymore, we need a new one, also including the ability to perform the A-/B- updates:

# todo insert IMAGE

We use the opportunity to create and make our BOOT partition a little bigger since we will add an additional filesystem as an initramfs soon!

DOING!

Since we are getting closer to realistic structure I am switching from MBR to GPT. This would also allow us to have more partitions, if we decide to do so:

dd if=/dev/zero of=test.img bs=1M count=1024
LOOP_DEVICE=$(sudo losetup -fP --show test.img)
sudo fdisk $LOOP_DEVICE << EOF
g
n
1

+256m
t
1
n
2

+256m
n
3

+256m
n
4


w
EOF


sudo mkfs.vfat ${LOOP_DEVICE}p1
sudo fatlabel ${LOOP_DEVICE}p1 BOOT
sudo mkdir /mnt/boot
sudo mount ${LOOP_DEVICE}p1 /mnt/boot
sudo cp boot.img /mnt/boot
sudo cp boot.sig /mnt/boot
sudo cp image.itb /mnt/boot
sudo umount /mnt/boot
sudo rm -rf /mnt/boot



sudo mkfs.ext4 ${LOOP_DEVICE}p2
sudo mkdir /mnt/rootfs
sudo cp -r rootfs/* /mnt/rootfs/
sudo umount /mnt/rootfs
sudo losetup -D

Verification

We will use dm-integrity to verify the integrity of our system. This happens indirectly by using dm-crypt (see documentation). It should be mentioned that one could also use dm-verity to do this, having the draw-back, that the system becomes read-only and only being tamper-ressistant and not encrypted.

First of all we need an additional (symmetric) key for this:

TODO ADD command to generate generic key!

IMPORTANT: If this key is device-unique, or should be the same for all devices, is entirely up to the use case. An advantage of a common key is a simpler update process, since a distributor can provide an already encrypted file-system along with its according hashes. But that also means that if this common key is leaked the IP of all devices (and therefore also the user-data) is unprotected. Using unique keys on the other hand will make the update process much harder, requiring the verity hashes to be calculated for each device, probably on the device, mid-update.

This raises the question of where to store this secret. Well, once again we have multiple options, but it boils down to:

  • Using the RPIs one-time programmable memory (OTP registers), that are specifically made for this.
  • Using additional hardware like TPMs

For this demo, I decided on using the OTP registers. Using a TPM has a severe drawback: In order to protect the keys in the TPM you will need a policy to protect them. A policy can be seen as a state of the system that must be fulfilled in order to access areas of the TPM. The state is commonly defined by a measured boot process. U-Boot does support this already, but that means updating the bootloader is a critical step. If the bootloader updates but the policy is not (e.g. power shortage), the device is locked until recovery steps have taken effect. That can be done by using an unencrypted recovery partition (or within the initramfs), that re-instantiates and corrects the policy if needed, but this would completely blow the scope of this — already very long — article!

Recap

Before we conclude this Blog Post I want to recap the entire chain of trust and what we actually did:

  • We wrote Keys into the Raspberry Pi OTP area, this acts as our Root of Trust. We used an RSA public key for verifying our initial u-boot setup and a symmetric TODO key for our disk encryption.
  • Within the verified boot.img, which the raspberry Pi uses to load u-boot and a management dtb, we added our public key once again, such that u-boot knows about it.
  • u-boot loads a signed fitimage that contains our fdt, kernel, and initramfs
  • The initramfs verifies the integrity of our system by loading the public key in the OTP, verifying the root hash of the current partition, and decrypting it by again loading the symmetric TODO key from the OTP and decrypting the rootfs
  • The rootfs uses the same encryption key to load its data partition on bootup

REFERENCES

--

--