Secure Boot
Secure Boot refers to hardware and software that does not allow an attacker to obtain sensitive data or boot altered firmware. This can be accomplished on modern embedded System on Chip devices by creating a Chain of Trust.
Chain of Trust
A Chain of Trust is established by validating each component of software to ensure that only trusted software can be used.
For a typical embedded Linux board the chain of trust may look like:
- Boot firmware (ie SPL) validated by embedded SoC BOOT ROM against signature hashes fused into one-time-programmable memory
- U-Boot validated by the Boot firmware (ie SPL)
- Kernel / FDT / initramfs images validated by U-Boot
- Filesystem encryption (if needed) unlocked by the kernel+initramfs
Secure the Boot firmware
Securing your product boot involves using SoC specific methods to verify the initial boot code that is fetched from the storage device and executed.
The initial boot code and how the various SoC manages secure boot varies per processor:
- Venice: IMX8M HABv4: initial boot code is U-Boot SPL; see venice/secure_boot
- Newport: CN803x Trusted Boot; initial boot code is Marvell BDK; see newport/secure_boot
- Ventana: IMX6 HABv4; initial boot code is U-Boot SPL
Secure U-Boot
For a secure U-Boot you want to disable the ability to stop autoboot and get to a U-Boot console. Additionally you do not want to use env variables that can be used by an attacker to affect the boot sequence.
To do this you need to understand where U-Boot env comes from:
- static default env embedded in uboot itself is from include/env_default.h default_environment and is built up depending on a lot of various configs. You can see this by doing a 'make u-boot-initial-env' which will produce a 'u-boot-initial-env' file
- programmatic env vars set at runtime such as eth*addr, fdtcontroladdr, stdin, stdout, stderr (and more depending on your config)
- FLASH storage backed env such as MMC
First and foremost you need to disable the ability to stop U-Boot's autoboot for security. This is done by setting CONFIG_BOOTDELAY=-2 which will disallow stopping from user input as well as disable any delay. Additionally your bootcmd should either be in an endless loop or end with a 'reset' so that any failure of the bootcmd does not leave you in an insecure U-Boot console environment. You probably want to do this step last however after debugging and development cycles as you may need to break into your bootloader during design and development.
Secondly you need to ensure there are no env vars that can be set outside of U-Boot that can alter the boot path. There are two common cases you may fall into:
- Your Linux environment never needs to access or alter U-Boot env (ie via fw_setenv/fw_printenv). In this case you can:
- define the following in your U-Boot config:
CONFIG_ENV_IS_NOWHERE=y # CONFIG_ENV_IS_IN_EEPROM is not set # CONFIG_ENV_IS_IN_FAT is not set # CONFIG_ENV_IS_IN_EXT4 is not set # CONFIG_ENV_IS_IN_FLASH is not set # CONFIG_ENV_IS_IN_MMC is not set # CONFIG_ENV_IS_IN_NAND is not set # CONFIG_ENV_IS_IN_NVRAM is not set # CONFIG_ENV_IS_IN_ONENAND is not set # CONFIG_ENV_IS_IN_REMOTE is not set CONFIG_USE_BOOTCOMMAND=y CONFIG_BOOTCOMMAND="<your simple boot-command which uses 'root' variable>; reset" # CONFIG_LEGACY_IMAGE_FORMAT is not set CONFIG_FIT_SIGNATURE=y
- Note the 'reset' at the end of the bootcmd. This is to ensure that a failure to boot does not drop you into an insecure U-boot console
- define the following in your U-Boot config:
- Your Linux environment needs to alter a variable (ie SWUpdate may need to alter a variable indicating which partition your rootfs is on if you are using a A/B ping-pong update method). In this case to allow U-boot to import the variable 'root' treated as a decimal you can define:
- define the following in your U-Boot config:
CONFIG_ENV_IS_NOWHERE=y CONFIG_ENV_IS_MMC=y CONFIG_ENV_WRITEABLE_LIST=y CONFIG_USE_BOOTCOMMAND=y CONFIG_BOOTCOMMAND="<your simple boot-command which uses 'root' variable>; reset" # CONFIG_LEGACY_IMAGE_FORMAT is not set CONFIG_FIT_SIGNATURE=y
- Note the 'reset' at the end of the bootcmd. This is to ensure that a failure to boot does not drop you into an insecure U-boot console
- patch the appropriate board config file in include/configs (ie include/configs/imx8mm_venice.h for Venice, include/configs/octeontx_common.h for Newport, or include/configs/gw_ventana.h for Ventana) to set:
CONFIG_ENV_FLAGS_LIST_DEFAULT="root:dw" CONFIG_ENV_FLAGS_LIST_STATIC="root:dw"
- ENV_FLAGS_LIST_DEFAULT is the default environment .flags variable that sets up permissions and validations
- ENV_FLAGS_LIST_STATIC is used when CONFIG_ENV_WRITEABLE_LIST=y and is an unchangeable list of vars with permissions and validations
- make sure your writable variables do not appear in the static default env as that will always override what gets imported from a FLASH based env
- use U-Boot 'mkenvimage' on your development host to create a binary FLASH env that sets necessary defaults for any writeable vars you declare
- define the following in your U-Boot config:
For additional details on securing U-Boot see the following excellent presentation by F-Secure:
Securing the Kernel, FDT, ramdisk via FIT images
The simplest way to secure the Kernel, the FDT, and the (optional) ramdisk image used to boot a Linux based OS is to use a U-Boot FIT image to contain signed versions of these.
A FIT (Flattened Image Tree) file is a container designed to hold one or more binary objects. It also can contain meta-data about each object such as addresses, hash values, and signing key information which can be used to validate the binary objects.
For more info on FIT images see:
- http://www.denx.de/wiki/pub/U-Boot/Documentation/multi_image_booting_scenarios.pdf
- https://elixir.bootlin.com/u-boot/latest/source/doc/uImage.FIT
FIT images are created using the U-Boot mkimage
application and use a template file (.its extension typically) which describes the image. The output is a binary image (.itb extension typically) which can be booted in U-Boot with the bootm
command. The syntax of the its template is well documented in U-Boot source:
There are two parts to using signed FIT images:
- the FDT used by U-Boot itself needs to have a signature node that describes the public key details needed to verify the FIT image components
- the FIT image needs to contain signatures covering the images inside as well as the configuration
Both steps above are achieved by using the mkimage
application from U-Boot which is built for your host OS in the tools directory. Note that it is important to use the mkimage app built for your U-Boot to ensure it has FIT signatures enabled and is compatible. In other words, do not use a mkimage binary that came from a package in your development host distro.
Procedure for using a FIT image:
- Create a key. It is recommended to use a sha256,rsa2048 key:
openssl genpkey -algorithm RSA -out fit.key -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537
- see verified-boot.txt for more info on key options
- add a signature node to an existing compiled FDT (myboard.dtb in this example) with a key named $KEY_NAME in the $KEY_DIR directory:
cat << EOF > dummy.its /dts-v1/; / { description = "Dummy file for signing board .dtb files"; #address-cells = <1>; /* dummy image just to keep mkimage tool happy */ images { kernel@1 { description = "dummy kernel image"; data = /incbin/("dummy.bin"); type = "kernel"; arch = "arm64"; os = "linux"; compression = "none"; load = <0x0>; entry = <0x0>; hash@1 { algo = "sha256"; }; }; }; configurations { default = "config@1"; config@1 { description = "Linux configuration"; kernel = "kernel@1"; signature@1 { algo = "sha256,rsa2048"; key-name-hint = "dummy"; }; }; }; }; EOF touch dummy.bin # replace dummy placeholders for key-name sed -i "s;key-name-hint = \"dummy\";key-name-hint = \"$KEY_NAME\";g" dummy.its # create cert for key openssl req -batch -new -x509 -key $KEY_DIR/$KEY_NAME.key -out $KEY_DIR/$KEY_NAME.crt # add signature node to myboard.dtb mkimage -r -k $KEY_DIR -K myboard.dtb -f dummy.its dummy.itb
- Note that the kernel image defined above and the created dummy.itb is just a dummy config to keep mkimage happy - the important thing above is the signature node in the configurations
- Note that if you are using U-Boot SPL with a FIT image (such as Venice) you will need to perform a 'make flash.bin' again following the update of any dtb's to re-create the FIT image.
- create a signed FIT image containing $KERNEL, $FDT, $INITRAMFS with a key named $KEY_NAME in the $KEY_DIR directory:
cat << EOF > fit.its /dts-v1/; / { description = "Image for Linux Kernel"; #address-cells = <1>; images { kernel { description = "Linux Kernel"; data = /incbin/("kernel"); type = "kernel"; arch = "arm64"; os = "linux"; compression = "gzip"; load = <0x20080000>; entry = <0x20080000>; hash@1 { algo = "sha256"; }; }; ramdisk { description = "ramdisk"; data = /incbin/("ramdisk"); type = "ramdisk"; arch = "arm64"; os = "linux"; compression = "none"; hash@1 { algo = "sha256"; }; }; fdt { description = "fdt"; data = /incbin/("fdt"); type = "flat_dt"; arch = "arm64"; compression = "none"; hash@1 { algo = "sha256"; }; }; }; configurations { default = "config@1"; config@1 { description = "Linux configuration"; kernel = "kernel"; ramdisk = "ramdisk"; fdt = "fdt"; signature@1 { algo = "sha256,rsa2048"; key-name-hint = "key"; }; }; }; }; EOF # replace dummy placeholders for kernel, ramdisk, fdt binaries sed -i "s;data = /incbin/(\"kernel\");data = /incbin/(\"$KERNEL\");g" fit.its sed -i "s;data = /incbin/(\"ramdisk\");data = /incbin/(\"$RAMDISK\");g" fit.its sed -i "s;data = /incbin/(\"fdt\");data = /incbin/(\"$FDT\");g" fit.its # replace dummy placeholders for key-name sed -i "s;key-name-hint = \"key\";key-name-hint = \"$KEY_NAME\";g" fit.its mkimage -k $KEY_DIR -f fit.its fit.itb
- If not using a ramdisk or fdt you can remove those sections above including the references in config and sign-images
- Note the kernel compression is set to gzip so you will want to make sure your $KERNEL points to a compressed kernel image (Image.gz)
- Note there is no compression type for a ramdisk as the kernel supports self-decompression of ramdisks. Use whatever compression type you want as long as you have enabled that compression type in your kernel.
- Note that you need a valid load/entry address above which varies depending on your processor (0x40080000 for Venice, 0x20080000 for Newport, 0x10008000 for Ventana)
- boot a FIT image using the U-boot
bootm
command:- from network
tftp $loadaddr fit.itb && bootm $loadaddr
- from mmc device 0, partition 1, /boot/fit.itb:
load mmc 0:1 $loadaddr /boot/fit.itb && bootm $loadaddr
- Make sure the address you are loading the FIT image to does not cause an overlap in memory with where the kernel load/entry point is. The bootm command will copy or uncompress the kernel to the load/entry point and copy the initramfs and the fdt to a location following that. You may need to alter loadaddr to somewhere in memory other than the default.
- from network
Secure filesystem with dm_crypt
Linux dm-crypt is a transparent disk encryption sybsystem. It is part of the device mapper infrastructure and uses the kernel crypto API. Being implemented at the device mapper layer means it can be stacked on top of other devices or even other device mappers thus it can be used to encrypted whole disks, partitions, software RAID volumes, and logical volumes. Linux Unified Key Step (LUKS) is the format used on the device in place of a file system which provides a whole host of key options.
Kernel requirements for dm-crypt:
- CONFIG_MD (RAID and LVM)
- CONFIG_BLK_DEV_DM (device-mapper)
- CONFIG_DM_CRYPT (dm-crypt)
- CONFIG_CRYPTO_* options for various cipher/hash you want to use, for example:
- CONFIG_ARM64_CRYPTO
- CONFIG_CRYPTO_XTS (XTS algo)
- CONFIG_CRYPTO_CBC (CBC algo)
- CONFIG_CRYPTO_TWOFISH (twofish cipher)
- CONFIG_CRYPTO_SERPENT (serpent cipher)
- CONFIG_CRYPTO_SHA256 (PBKDF2-sha256)
- CONFIG_CRYPTO_SHA512 (PBKDF2-sha512)
- CONFIG_CRYPTO_RMD160 (PBKDF2-ripemd160)
- CONFIG_CRYPTO_SHA1_ARM64_CE (PBKDF2-sha1)
- CONFIG_CRYPTO_SHA2_ARM64_CE
- CONFIG_CRYPTO_SHA512_ARM64_CE
- CONFIG_CRYPTO_AES_ARM64_CE_CCM
- CONFIG_CRYPTO_AES_ARM64_CE_BLK
- CONFIG_CRYPTO_USER_API_HASH
- CONFIG_CRYPTO_USER_API_SKCIPHER
Userspace requirements for dm-crypt:
- cryptsetup (Buildroot BR2_PACKAGE_CRYPTSETUP)
For more info:
Example:
- Create a key to use for encryption:
dd if=/dev/urandom of=$KEY_DIR/fs.key bs=1 count=4096
- Boot a Linux provisioning kernel+ramdisk such as the prebuilt images at http://dev.gateworks.com/buildroot/ (see wiki:buildroot)
- Create encrypted device using dm-crypt
# get key file, ie via network ifconfig eth0 192.168.1.20 cd /tmp wget http://server/fs.key # format a LUKS device echo "YES" | cryptsetup luksFormat /dev/mmcblk0p1 fs.key -
- use 'cryptsetup benchmark' to show all cipher and hash algos available in your running kernel as well as their performance
- use 'cryptsetup --help' to see options; options you may wish to change are --cipher (default aes-xts-plain64), --key-size (default is 256) --hash (default is sha256) and --use-urandom (default is --use-random)
- Open (unlock) the LUKS device
# open (unlock) LUKS device and map it to /dev/mapper/rootfs cryptsetup luksOpen /dev/mmcblk0p1 rootfs --key-file=fs.key
- Create your filesystem:
wget http://server/rootfs.tar.xz mkfs.ext4 -q -F -L rootfs /dev/mapper/rootfs mount /dev/mapper/rootfs /mnt tar -C /mnt -xf rootfs.tar.xz --keep-directory-symlink umount /dev/mapper/rootfs
- Close (lock) LUKS device
cryptsetup luksClose rootfs
- Create a simple initramdisk responsible for unlocking dm-crypt via buildroot:
cat <<EOF >output/target/init #!/bin/sh # Mount things needed by this script mount -n -t devtmpfs devtmpfs /dev mount -n -t proc proc /proc mount -n -t sysfs sysfs /sys mount -n -t tmpfs tmpfs /run init="/sbin/init" root="mmcblk0p1" key=/fs.key # Wait for device to exist echo "Waiting for /dev/${root}..." while [ ! -b "/dev/${root}" ]; do sleep 1 echo -n . done #Open encrypted partition mkdir -p /run/cryptsetup echo "Opening /dev/$root..." cryptsetup luksOpen "/dev/${root}" "${root}" --key-file=$key #Mount the root device echo "Mounting /dev/mapper/${root}..." mkdir /newroot mount "/dev/mapper/${root}" /newroot #Switch to the new root and execute init echo "Switching to new root..." cd /newroot exec switch_root . "${init}" "$@" #This will only be run if the above line failed echo "Failed to switch_root" EOF chmod +x output/target/init
- this is intended for use with a buildroot based rootfs where /sbin/init is busybox and we place our own script in /init (which the kernel will look for before /sbin/init) to be used as an intermediate init
- Create a FIT image (see above) containing your kernel fdt and initramfs and boot it with boom or built it as a kernel+ramdisk