wiki:secure_boot

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:

  1. 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
  2. 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

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:

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:

  1. 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
    
  1. 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.
  1. 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)
  1. 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.

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:

  1. Create a key to use for encryption:
    dd if=/dev/urandom of=$KEY_DIR/fs.key bs=1 count=4096
    
  2. Boot a Linux provisioning kernel+ramdisk such as the prebuilt images at http://dev.gateworks.com/buildroot/ (see wiki:buildroot)
  3. 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)
  4. 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
    
  5. 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
    
  6. Close (lock) LUKS device
    cryptsetup luksClose rootfs
    
  7. 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
  8. Create a FIT image (see above) containing your kernel fdt and initramfs and boot it with boom or built it as a kernel+ramdisk
Last modified 3 years ago Last modified on 07/12/2021 07:46:47 PM
Note: See TracWiki for help on using the wiki.