EDIT: One of our readers made us notice that we could simplify some steps regarding mkimage, the changes are now integrated in the tutorial. If you tried the tutorial before this edit, you might want to reapply the patches and to take a look at then new post-image script. This kind of feedback is always welcome ;)

In this tutorial, we will guide you through configuring a fully secure boot chain on the STM32MP157D-DK1 development board. This process ensures the integrity and authenticity of your system at every boot stage.

Our goals include:

  • Configuring Trusted Firmware-A (TF-A) with a chain of trust to secure the initial bootloader stage.
  • Extend the chain of trust to U-boot loading Flattened uImage Tree (FIT) images
  • Verifying the Linux kernel signature using U-Boot
  • Establishing a secure root filesystem with EROFS (Enhanced Read-Only File System) and dm-verity, ensuring immutability and integrity of the root filesystem.
  • Adding all these steps in a Buildroot environment

High level picture

boot-order-diagram

Each component in the boot process is responsible for verifying the integrity of the next one:

  • ROM Code: The ROM code accesses the hardware fuses (containing the public key hash) to verify the authenticity of BL2 during the boot process. The fuses can only be written one time so writing them should be one of the step of setting up a fully verified boot chain.
  • Trusted Firmware-A (TF-A): The complete fip file includes keys and signatures for its components. BL2, as part of this process, verifies BL33 (U-Boot).
  • U-Boot: U-Boot has access to a public key stored in its device tree blob (u-boot.dtb), which is protected by TF-A and can not be tampered by design. This key is used to validate the fit file containing the Linux kernel.
  • the FIT Image: The Linux kernel, its associated full DTB file, and a bootscript are encapsulated in a FIT image. U-Boot verifies the integrity and authenticity of each element in the FIT file before booting.
  • DM-Verity: Once the kernel is running, DM-Verity ensures the integrity of the root filesystem. The kernel verifies the root hash using parameters provided by the bootscript. These parameters and the bootscript itself, were previously validated by U-Boot.

During this tutorial, we will skip the first step to avoid bricking a board by mistake :-)

To achieve secure boot, we need to inject some steps in Buildroot’s build process. In our process, the u-boot.dtb is embedded with a public key. That step is handled by the provided patches after U-Boot is built. The TF-A package itself does not require a post-build script. Instead, it needs to be adapted to include the necessary flags and dependencies to enable trusted boot. Once the build is complete, we can encapsulate the Linux kernel into a FIT image, enabling verification by U-Boot. Additionally, since the root filesystem (rootfs) must include dm-verity hashes, the simplest approach is to create a post-image script that performs all final steps, including generating an SD image. By the end of the tutorial, the build sequence will resemble the following:

Prerequisites

  • A compatible development board: In our case, we tested this tutorial using the STMicroElectronics’s STM32MP157D-DK1 (however the STM32MP157A-DK1 defconfig can be used in buildroot)

  • Clone buildroot (This tutorial was tested on buildroot origin/master commit: b3ddad3d5199fe2443b20a562aac41fa1fc70529) and make your defconfig.

    With our board we could just copy a predefined defconfig:

    #from the buildroot clone directory
    cp buildroot/configs/stm32mp157a_dk1_defconfig .config
    

    And select an external toolchain from the make menuconfig menu (to save time).

  • Buildroot must be configured to generate a zImage, an u-boot.dtb, and an u-boot-nodtb.bin file

You can create two pairs of keys that will be used:

#from the buildroot directory
mkdir keys && cd keys

openssl genpkey -algorithm RSA -out tfa.key -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537
openssl req -batch -new -x509 -key tfa.key -out tfa.crt

openssl genpkey -algorithm RSA -out dev.key -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537
openssl req -batch -new -x509 -key dev.key -out dev.crt

cd ..

⚠️ It is important to keep the key file names as above, the patches we introduce rely on this. Modify the patches if you change the names. Note that it is possible to use the same keypair for every step in the secure boot process. We use different keys here to make it clear which keypair corresponds to which step. Note also that we chose RSA-2048 as the key algorithm here, which is generally not the best choice. Your choice of key algorithm will depend on what is supported by the SoC (because you generally want the TF-A key to be in e-fuses and to be used by the boot ROM), and on whether or not you want to use the same keypair for TF-A and for U-Boot. For example, you’ll often prefer to use an ECDSA keypair if that is supported by the SoC.

To simplify this tutorial, you can apply two patches to the Buildroot source. These patches will be explained in details in the next sections. The first patch modifies TF-A to sign the FIP file, and the second one modifies U-Boot to embed the public key and sign the FIT file. These patches are unfortunately not clean enough yet for submission upstream, although a version of the TF-A patch was sent upstream.

These will add some options in the configuration menus.

git apply tfa.patch
git apply uboot.patch

Be aware that updating U-Boot (using make uboot-menuconfig and make uboot-rebuild) does not automatically update the U-Boot image in the SD card image. This is because TF-A encapsulates U-Boot in the FIP file. Thus, if you have already built an image, to reflect any changes, you must rebuild U-Boot and TF-A and re-generate the SD card image:

make uboot-rebuild arm-trusted-firmware-rebuild world

Step 1: setup TF-A to enable trusted boot.

The TF-A package is used to generate the FIP file containing BL32 (the secure payload, for which a stub is provided by TF-A itself) and U-Boot. TF-A is already present on Buildroot (it’s called ARM Trusted Firmware for historical reasons) but doesn’t fully support trusted boot as is. As for the buildroot, a patch was created to automate the process. With it, you can simply configure it by activating the trusted boot option in menuconfig:

#BR2_TARGET_ARM_TRUSTED_FIRMWARE_CUSTOM_VERSION=y
Bootloaders > ARM Trusted Firmware (ATF) > ATF Version (Custom version)
#BR2_TARGET_ARM_TRUSTED_FIRMWARE_CUSTOM_VERSION_VALUE="lts-v2.10.5"
Bootloaders > ARM Trusted Firmware (ATF) > ATF Version (Custom version) = lts-v2.10.5
#BR2_TARGET_ARM_TRUSTED_FIRMWARE_TRUSTED_BOARD_BOOT=y
Bootloaders > ARM Trusted Firmware (ATF) > Enable Trusted Board boot
#BR2_TARGET_ARM_TRUSTED_FIRMWARE_ROT_KEY="keys"
Bootloaders > ARM Trusted Firmware (ATF) > Path to the ROT private key = keys
#BR2_TARGET_ARM_TRUSTED_FIRMWARE_MBEDTLS_VERSION="3.6.2"
Bootloaders > ARM Trusted Firmware (ATF) > The MbedTLS version number (ex 2.8) needed for the secure boot = 3.6.2

In practice, this patch downloads and installs the specified version of mbedtls inside the TF-A source. It also adds the following parameters to the TF-A build in order to enable trusted boot:

ROT_KEY=<path to the TF-A keys>
MBEDTLS_DIR=<path to the specific version of mbed tls>
GENERATE_COT=1 #cannot be seen from the menu
TRUSTED_BOARD_BOOT=1 #Set to 1 when the BR2_TARGET_ARM_TRUSTED_FIRMWARE_TRUSTED_BOARD_BOOT is selected

Note that we need to specify a specific version because every version of TF-A expects a different, specific version of mbedtls, refer to <path_to_tfa_source_tree>/drivers/auth/mbedtls/mbedtls_common.mk to find the expected version. After flashing the SD card, you should observe a message during boot indicating that the ROM code does not verify the ROT (Root Of Trust) keys. This occurs because trusted boot has not been enabled in the fuses and it can be ignored for the scope of this tutorial. TF-A does embed the ROT key in its binary and checks that the FIP file it loads is signed with it.

Step 2: Adding the public key to U-Boot’s dtb file.

With trusted boot enabled in TF-A, we have verified that the U-Boot binary was indeed signed by the expected private key. For this, we need to enable the support for signing the FIT images in buildroot’s mkimage. The next step is then to configure U-Boot to do the same when loading the Linux kernel. The second patch included above allows us to do this through Buildroot’s menuconfig:

#BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
Host utilities > host u-boot tools > Flattened Image Tree (FIT) support > FIT signature verification support
#BR2_TARGET_UBOOT_SIGN_DTB=y
Bootloaders > U-Boot > Sign u-boot.dtb file with private key and embed the public key.
#BR2_TARGET_UBOOT_KEYS="keys/"
Bootloaders > U-Boot > Path to the directory containing the public key > keys/

Note that we also have added the variable names for those who don’t want to bother themselves with the GUI.

The post-build hook in U-Boot patch will do the public key injection and private key signing. It uses similiar command as below to do the job.

# embed the public key in u-boot.dtb
mkimage -f auto -d /dev/null -k keys_directory -g dev -o sha256,rsa2048 -K u-boot.dtb -r unused.itb
# sign the FIT image in place
mkimage -D "-I dts -O dtb -p 2000" -F -k keys_directory uboot-fitimage.fit

if you decompile the dtb file with dtc -I dtb -O dts u-boot.dtb -q -o - | grep signature -A 12, you will see the configuration node.

In the U-Boot config (make uboot-menuconfig) you must enable:

#CONFIG_FIT_SIGNATURE=y
Boot options > Boot images > Flattened Image Tree (FIT) > Enable signature verification of FIT uImages
#CONFIG_RSA=y
Library routines > Security support > Use RSA Library
#CONFIG_ECDSA=y
Library routines > Security support > Enable ECDSA support

In the buildroot config (make menuconfig) you must enable

#BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
Host utilities > host u-boot tools > Flattened Image Tree (FIT) support
#BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
Host utilities > host u-boot tools > Flattened Image Tree (FIT) support > FIT signature verification support

Now, if you try to boot from a FIT file you will be blocked due to missing signatures.

Step 3 : securing the kernel and rootfs.

Thanks to steps 1 and 2, the initial stage of the boot process is now secured. However, the kernel and root file system (rootfs) remain unsigned and U-Boot refuses to load the kernel.

To enable signature verification, U-Boot requires a Flattened Image Tree (FIT) file. We will create a FIT file that includes the kernel (zImage), the Device Tree Blob (DTB), and a boot script. To ensure the integrity of the root file system, we will use DM-Verity on a read-only file system (EROFS) partition. DM-Verity can be configured either through a kernel command-line argument or an initramfs script. Here, we choose the kernel command-line method. DM-Verity is configured by running veritysetup on the rootfs, which generates:

  • A metadata file that will be included as a separate partition in the SD card.
  • A text file containing the hash of the rootfs directory and related elements.

To further secure the boot process, we can include the necessary DM-Verity arguments within the FIT file by embedding them in a signed boot script. This ensures the hash cannot be tampered with. Buildroot’s STM32MP15 defconfigs use a post-build script to generate the SD card image. This script can also be adapted to generate the DM-Verity hashes, the boot script, and the FIT image in one process.

This script will be run by buildroot, so we need to make sure buildroot has all of the necessary dependencies, so we need to select the following packages in the make menuconfig.

#BR2_PACKAGE_HOST_CRYPTSETUP=y
Host utilities > host cryptsetup
#BR2_TARGET_ROOTFS_EROFS=y
Filesystem images > erofs root filesystem

There are further options to configure the erofs but the defaults work for our purposes.

Below are the files needed to automate the last steps. The script post-image-signed.sh adds steps to generate the dm-verity partition and to sign the FIT image:

#!/bin/sh -eu

#
# atf_image extracts the TF-A binary image from DTB_FILE_NAME that appears in
# BR2_TARGET_ARM_TRUSTED_FIRMWARE_ADDITIONAL_VARIABLES in ${BR_CONFIG},
# then prints the corresponding file name for the genimage
# configuration file
#
atf_image()
{
	ATF_VARIABLES="$(sed -n 's/^BR2_TARGET_ARM_TRUSTED_FIRMWARE_ADDITIONAL_VARIABLES="\([^\"]*\)"$/\1/p' ${BR2_CONFIG})"
	# make sure DTB_FILE_NAME is set
	printf '%s\n' "${ATF_VARIABLES}" | grep -Eq 'DTB_FILE_NAME=[0-9A-Za-z_\-]*'
	# extract the value
	DTB_FILE_NAME="$(printf '%s\n' "${ATF_VARIABLES}" | sed 's/.*DTB_FILE_NAME=\([a-zA-Z0-9_\-]*\)\.dtb.*/\1/')"
	echo "tf-a-${DTB_FILE_NAME}.stm32"
}

main()
{
	ATFBIN="$(atf_image)"
	GENIMAGE_CFG="$(mktemp --suffix genimage.cfg)"

	sed -e "s/%ATFBIN%/${ATFBIN}/" \
		board/stmicroelectronics/common/stm32mp1xx/genimage-signed.cfg.template > ${GENIMAGE_CFG}

	support/scripts/genimage.sh -c ${GENIMAGE_CFG}

	rm -f ${GENIMAGE_CFG}

	exit $?
}

create_dm_verity()
{
    #initialize dm-verity and parse the output + generates the bootscript that will be added later on in the fitimage
    EROFS_FS=${BINARIES_DIR}/rootfs.erofs
    DATA_BLOCK_SIZE=4096
    HASH_BLOCK_SIZE=4096
    METADATA_EROFS=${BINARIES_DIR}/metadata.rootfs.erofs
    LOGFILE=${BINARIES_DIR}/veritysetup.log
    veritysetup format $EROFS_FS $METADATA_EROFS > ${LOGFILE}
    SECTOR_SIZE=512
    #let's extract the output of the logfile.
    DATA_BLOCKS=$(grep "Data blocks:" $LOGFILE | awk '{print $3}')
    echo "Data blocks: $DATA_BLOCKS"
    DATA_BLOCK_SIZE=$(grep "Data block size:" $LOGFILE | awk '{print $4}')
    echo "Data block size: $DATA_BLOCK_SIZE"
    HASH_BLOCK_SIZE=$(grep "Hash block size:" $LOGFILE | awk '{print $4}')
    echo "Hash block size: $HASH_BLOCK_SIZE"
    HASH=$(grep "Root hash:" $LOGFILE| awk '{print $3}')
    echo "Root hash: $HASH"
    SALT=$(grep "Salt:" $LOGFILE | awk '{print $2}')
    echo "Salt: $SALT"
    HASH_ALGO=$(grep "Hash algorithm:" $LOGFILE | awk '{print $3}')
    echo "Hash algorithm: $HASH_ALGO"

    DATA_SECTORS=$(($DATA_BLOCKS * $DATA_BLOCK_SIZE / $SECTOR_SIZE))
    DATA_DEV="/dev/mmcblk0p5"
    DATA_META_DEV="/dev/mmcblk0p6"
    TXT="verity,,,ro,0 ${DATA_SECTORS} verity 1 ${DATA_DEV} ${DATA_META_DEV} ${DATA_BLOCK_SIZE} ${HASH_BLOCK_SIZE} ${DATA_BLOCKS} 1 ${HASH_ALGO} ${HASH} ${SALT} 1 ignore_zero_blocks"
    BOOTARG="dm-mod.waitfor=\"$DATA_META_DEV,$DATA_DEV\" dm-mod.create=\"${TXT}\" root=/dev/dm-0 ro rootfstype=erofs rootwait"
    BOOTARG="setenv bootargs '$BOOTARG'"
    UBOOTSCRIPT_OG="board/stmicroelectronics/common/stm32mp1xx/u-boot.scr"
    UBOOTSCRIPT=${BINARIES_DIR}/u-boot.scr
    cp $UBOOTSCRIPT_OG $UBOOTSCRIPT
    # ##add bootarg as first line in sign/u-boot.scr
    sed -i "1i $BOOTARG" $UBOOTSCRIPT
    #copy sign/u-boot.scr to BINARIES_DIR
    echo "BOOTARG: $BOOTARG"
}


create_fit_image()
{
    MKIMAGE=/usr/bin/mkimage
    if $(grep -q "^BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y" ${BR2_CONFIG})
    then
        MKIMAGE=${HOST_DIR}/bin/mkimage
    fi
    BASE="board/stmicroelectronics/common/stm32mp1xx"
    ###extract BR2_TARGET_UBOOT_KEYS from .config
    KEYS=$(sed -n 's/^BR2_TARGET_UBOOT_KEYS="\([^\"]*\)"$/\1/p' ${BR2_CONFIG})
    echo "KEYS: $KEYS"
    ZIMAGE="${BINARIES_DIR}/zImage"
    DTB="${BINARIES_DIR}/stm32mp157a-dk1.dtb"
    UBOOTSCRIPT="${BINARIES_DIR}/u-boot.scr"
    cp ${BASE}/linux.its ${BASE}/linux-tmp.its
    sed -i "s@ZIMAGE@${ZIMAGE}@g" ${BASE}/linux-tmp.its
    sed -i "s@DTB@${DTB}@g" ${BASE}/linux-tmp.its
    sed -i "s@UBOOTSCRIPT@${UBOOTSCRIPT}@g" ${BASE}/linux-tmp.its

    ${MKIMAGE} -D "-I dts -O dtb -p 2000" -f ${BASE}/linux-tmp.its ${BINARIES_DIR}/fitImage
    ${MKIMAGE} -D "-I dts -O dtb -p 2000" -F -k ${KEYS} -r ${BINARIES_DIR}/fitImage
}
create_dm_verity $@
create_fit_image $@
main $@

The FIT image is defined by the following linux.its file:

/dts-v1/;

/ {
        description = "Linux fitimage";
        #address-cells = <1>;

        images {
                kernel-1 {
                        description = "Linux kernel";
                        data = /incbin/("ZIMAGE");
                        type = "kernel";
                        arch = "arm32";
                        os = "linux";
                        compression = "none";
                        load = <0xc4000000>;
                        entry = <0xc4000000>;
                        hash-1 {
                                algo = "sha256";
                        };
                };
                fdt-1 {
                        description = "Flattened Device Tree blob";
                        data = /incbin/("DTB");
                        type = "flat_dt";
                        arch = "arm32";
                        compression = "none";
                        hash-1 {
                                algo = "sha256";
                        };
                };
                bootscript-1 {
                        description = "Bootscript";
                        data = /incbin/("UBOOTSCRIPT");
                        type = "script";
                        compression = "none";
                        hash-1 {
                                algo = "sha256";
                        };
                };
        };

        configurations {
                default = "conf-1";
                conf-1 {
                        description = "Linux kernel, FDT blob";
                        kernel = "kernel-1";
                        fdt = "fdt-1";
                        bootscript = "bootscript-1";
                        hash-1 {
                                algo = "sha256";
                        };
                        signature {
                                algo = "sha256,rsa2048";
                                key-name-hint = "dev";
                                sign-images = "fdt", "kernel","bootscript";
                                required;
                        };
                };
                conf-2 {
                        description = "uboot-script";
                        script = "bootscript-1";
                        hash-1 {
                                algo = "sha256";
                        };
                        signature {
                                algo = "sha256,rsa2048";
                                key-name-hint = "dev";
                                sign-images = "script";
                                required = "images";
                        };
                };
        };
};

The its file follows the device tree syntax, with the addition of the /incbin/ construct which allows to include a binary file in the “device tree”. The FIT image is actually a device tree binary corresponding to that its file, but you need mkimage to generate it and to add signatures.

The u-boot.scr script contains a script that will load the fit image and run the linux kernel with the bootarg needed by dm-verity: (the first commented out command is ignored and automatically replaced by the post build script, it is here to show you an example)

#setenv bootargs 'dm-mod.waitfor="/dev/mmcblk0p6,/dev/mmcblk0p5" dm_mod.create="verity,,,ro,0 16344 verity 1 \
#/dev/mmcblk0p5 /dev/mmcblk0p6 4096 4096 2043 1 sha256 31eea5fcb0b569ae0f1680b10e3f534814f087b58ec95cc98e057c2b057892bb \
#ca2f9317c378456e42116be90baffab9821120f69fd287afb6f914cd3854a289 1 ignore_zero_blocks" root=/dev/dm-0 ro rootfstype=erofs rootwait'
bootm ${loadaddr}
echo "Bad image or kernel."
reset

we have to adapt the genimage template by adding the fit image in the fourth partition, the rootfs in the fifth and dm-verity’s metadata in the sixth. Create a genimage-signed.cfg.template file:

image secure-sdcard.img {
	hdimage {
		partition-table-type = "gpt"
	}

	partition fsbl1 {
		image = "%ATFBIN%"
	}

	partition fsbl2 {
		image = "%ATFBIN%"
	}

	partition fip {
		image = "fip.bin"
		size = 2M
	}

	partition fit {
		image = "fitImage"
		bootable = "yes"
	}
	partition rootfs {
		image = "rootfs.erofs"
	}
	partition verity-metadata{
		image = "metadata.rootfs.erofs"
	}
}

⚠️ These scripts needs to be in a directory accessible by buildroot.

For our board we placed them into buildroot/board/stmicroelectronics/common/stm32mp1x. Ideally we would need to use a BR2_EXTERNAL (see the buildroot manual) but that is outside the scope of this tutorial so for now we will just keep making our modifications directly within the buildroot source tree. For our board, the directory looked like this:

$ ls board/stmicroelectronics/common/stm32mp1xx/
genimage.cfg.template
genimage-signed.cfg.template
linux.its
patches
post-image.sh
post-image-signed.sh
u-boot.scr

🗒️ Keep in mind that our scripts are hardcoded with paths pointing to that location, so if you place them elsewhere, you need to reflect that change in the scripts.

The only thing left for buildroot to be aware of our scripts is to explicitly execute the post-image-signed.sh after the image has been generated. We need to mark it as executable and to set the corresponding in the make menuconfig menu.

chmod +x board/stmicroelectronics/common/stm32mp1xx/post-image-signed.sh
#BR2_ROOTFS_POST_IMAGE_SCRIPT="board/stmicroelectronics/common/stm32mp1xx/post-image-signed.sh"
System configuration >
        Custom scripts to run after creating filesystem images >
                board/stmicroelectronics/common/stm32mp1xx/post-image-signed.sh

Now that all of our custom files are in place, we can go back to configuring the important build parameters.

The linux kernel needs to be configured to be support EROFS and DM-verity:

⚠️ Be careful that none of the following options are included as modules !

make linux-menuconfig

#CONFIG_EROFS_FS=y
File systems > Miscellaneous filesystems > EROFS filesystem support

#CONFIG_MD=y
Device Drivers > Multiple devices driver support (RAID and LVM)
#CONFIG_BLK_DEV_DM=y
Device Drivers > Multiple devices driver support (RAID and LVM) > Device mapper support
#DM_DEBUG=y
Device Drivers > Multiple devices driver support (RAID and LVM) > Device mapper support > Device mapper debugging support
#DM_INIT=y
Device Drivers > Multiple devices driver support (RAID and LVM) > Device mapper support > DM "dm-mod.create=" parameter support
#DM_UEVENT=y
Device Drivers > Multiple devices driver support (RAID and LVM) > Device mapper support > DM uevents
#DM_VERITY=y
Device Drivers > Multiple devices driver support (RAID and LVM) > Device mapper support > Verity target support
#CRYPTO_SHA256=y
Cryptographic API > Hashes, digests, and MACs > SHA-224 and SHA-256
#CRYPTO_SHA256_ARM=y
Cryptographic API > Accelerated Cryptographic Algorithms for CPU (arm) > Hash functions: SHA-224 and SHA-256 (NEON)
#CRYPTO_RSA=y
Cryptographic API > Public-key cryptography > RSA (Rivest-Shamir-Adleman)

Don’t forget to rebuild the kernel with make linux

As last step we need to change U-Boot’s startup command to use the script located in the fit image. Execute make uboot-menuconfig, and edit the following

#CONFIG_USE_BOOTCOMMAND="mmc dev 0;part start mmc 0 4 fit_start;part size mmc 0 4 fit_size;mmc read ${loadaddr} ${fit_start} ${fit_size};source ${loadaddr}#conf-2"
Boot options >
    Enable a default value for bootcmd
        mmc dev 0;part start mmc 0 4 fit_start;part size mmc 0 4 fit_size;mmc read ${loadaddr} ${fit_start} ${fit_size};source ${loadaddr}#conf-2

This boot command tells U-Boot to automatically load the FIT file from the mmc, and to start the conf-2 node (thus the bootscript). We also have to disable the U-Boot command line, or set a password, but that is left as an exercise for the reader.

When booting, you will see that the fit image signature (RSA) and the hash is verified by U-Boot

During the startup of the kernel, we can observe that dm-verity waits for the rootfs partition + erofs metadata and creates the dm-verity mapper.This mapper is then used as rootfs for the kernel.

Bonus: enforce signature verification of kernel modules

To further enhance security, you can protect against the insertion of unsigned kernel modules (via insmod or modprobe).

To achieve this, first create the keys that will be used:

mkdir -p keys/linux-modules
cd keys/linux-modules
touch x509.genkey

Put the following in x509.genkey

[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
prompt = no
string_mask = utf8only
x509_extensions = myexts

[ req_distinguished_name ]
CN = Modules

[ myexts ]
basicConstraints=critical,CA:FALSE
keyUsage=digitalSignature
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid

Now lets create the keys

openssl req -new -nodes -utf8 -sha512 -days 36500 -batch -x509 -config x509.genkey -outform PEM -out kernel_key.pem -keyout kernel_key.pem
openssl x509 -outform der -in kernel_key.pem -out signing_key.x509

you should now have 3 files in this folder : kernel_key.pem, signing_key.x509 and the x509.genkey Let’s enable the module signature verification in linux-menuconfig

  • Enable loadable module support > “module signature verification”,”Require modules to be validly signed”,”Automatically sign all modules”, “algo SHA-512”
  • enable CRYPTO_SHA512

In order to use our own keys (otherwise keys will be generated automatically), we must include them before the build . Therefore, we adapt linux/linux.mk file with a new pre-build hook:

LINUX_GENKEY_PATH=keys/linux-modules/x509.genkey
LINUX_PEMKEY_PATH=keys/linux-modules/kernel_key.pem

define IMPORT_KEYS_FOR_MODULES
	#adding keys
	 cp $(LINUX_GENKEY_PATH) $(@D)/certs/x509.genkey
	 cp $(LINUX_PEMKEY_PATH) $(@D)/certs/signing_key.pem

endef
LINUX_PRE_BUILD_HOOKS += IMPORT_KEYS_FOR_MODULES

Let’s rebuild linux and a full new sd card image make linux-rebuild world

Testing the signature verification

Let’s test our signature verification by building and inserting a (rapidly made) out of tree kernel module

mkdir test-module && cd test-module
touch Makefile
touch test.c

We can create a dummy kernel module in the c file:

#include <linux/module.h>
#include <linux/kernel.h>

int init_module(void){
    printk(KERN_INFO "The kernel module was launched !\n");
    return 0;
}

void cleanup_module(void){
}

MODULE_LICENSE("GPL");

Creating a minimal Makefile allowing out-of-three build.

CC=$(CROSS_COMPILE)gcc
obj-m += test.o
all:
	$(MAKE) -C $(KDIR) M=$(PWD)

you can then compile the module using buildroot’s gcc

make CROSS_COMPILE=$PWD/../output/host/bin/arm-buildroot-linux-gnueabihf- \
ARCH=arm KDIR=$PWD/../output/build/linux-6.9.8/

you can easily make the module available in your SD card image by applying an overlay to buildroot:

#from the test-module directory
mkdir -p overlay/root
cp test.ko overlay/root/test.ko
cd ..
make menuconfig

Change the value of the following options:

#BR2_ROOTFS_OVERLAY="test-module/overlay"
System configuration > Root filesystem overlay directories = test-module/overlay

run make world and your SD card image will be updated.

If you try to import the module without signing it, the module will be rejected

Let’s sign the module on our host :

cd test-module
../output/build/linux-6.9.8/scripts/sign-file sha512 ../keys/linux-modules/kernel_key.pem \
../keys/linux-modules/signing_key.x509 test.ko

The module should now be accepted when inserted

Conclusion

In conclusion, we successfully set up nearly the entire secure boot chain on the board. As a final step, remember to disable U-Boot’s console to prevent anyone from bypassing secure boot using it. Feel free to take a look at fiptool provided by TF-A, this tools permits to debug and modify FIP files easily.

For this secure boot flow, we needed to make a few changes to Buildroot itself. Ideally, these changes should be upstreamed to the Buildroot project. However, the way they are presented here, they are a bit too invasive and not flexible enough. We should find the minimal changes to Buildroot’s ARM Trusted Firmware, U-Boot and Linux packages to support secure boot with the help of post-build and post-image scripts.

If you want more information about secure boot, you can read this tutorial from timesys, the first step of this post is highly inspired by it.

Lu Dai
Colin Evrard

Also collaborated on this article:

Lu Dai

Colin Evrard