Version 17 (modified by 8 months ago) ( diff ) | ,
---|
-
Venice Secure boot
- General Notes
- i.MX8M High Assurance Boot (HAB)
- i.MX secure boot
- Trusted Execution Environment (TEE)
- Full Disk Encryption (FDE)
- Locking down U-Boot environment and shell
- HABv4 encrypted boot architecture
Venice Secure boot
General Notes
Secure boot, particularly in the context of our Venice family of Single Board Computers (SBCs), involves several key considerations and procedures. Here are some general notes to keep in mind:
- Signature Node: The process of signing a FIT (Flattened Image Tree) image, as described in our wiki on secure boot, is not overly complex. However, a challenge arises in integrating the signature into the device tree (dt) control for Venice SBCs. Unlike Newport SBCs, where the dtbs are augmented and injected back in, Venice dtbs are contained within the U-Boot ITB (Image Tree Blob) and are built using Binman. As a result, injecting the signature node may require an alternate approach.
- U-Boot Versions: It's crucial to note that this guide was written for U-Boot version v2024.03-venice (or a compatible version), as different U-Boot branches may necessitate distinct configurations.
- SoC specifics: The Gateworks Venice family of Single Board Computers supports IMX8MM, IMX8MN, and IMX8MP SoC's which vary quite a bit and create different binary builds and images for boot firmware. Most of the examples here are for the IMX8MM SoC; if you are using the IMX8MN or IMX8MP it is assumed that you will understand to edit those device-tree files and use those defconfigs for example: arch/arm/dts/imx8m{m,n,p}* configs/imx8m{m,n,p}_venice_defconfig
- Support for Additional Security Measures: Unlike some other systems that support features like Trusted Platform Module (TPM), OP-TEE (Open Portable Trusted Execution Environment), or CAAM (Cryptographic Acceleration and Assurance Module) keys, Venice SBCs primarily rely on the signature node for secure boot operations.
- Building boot firmware stand-alone vs modifying the Venice BSP directory: In the examples here we feel it adds clarity to build the 'secure' boot firmware components ouside of the Venice BSP yet we use the toolchain, environment, and some artifacts of the Venice BSP and thus refer to '$VENICE_BSP' as the directory where you have the Venice BSP built on your development host.
- FIT Verification: If a signature node is absent in the controlling device tree blob (dtb), FIT verification will not occur. This behavior is by design, emphasizing the user's responsibility to ensure the presence of a valid signature node. It's advisable to consider patching U-Boot to prevent this if LEGACY mode is not enabled, thus mitigating potential security vulnerabilities.
Chain of Trust
The secure boot process on Venice SBCs relies on a robust chain of trust to ensure the integrity and authenticity of the boot process. Here's an overview of the chain of trust and the key steps involved:
- Power On: The boot process initiates with the IMX BOOT_ROM, which utilizes One-Time Programmable (OTP) fuses to verify the signed Secondary Program Loader (SPL) image loaded into the SRAM/cache. This verification process necessitates the blowing of fuses into OTP and requires the device to be 'locked'.
- U-Boot SPL: Once the early power management configuration and DRAM configuration are completed, U-Boot SPL loads U-Boot proper's FIT (u-boot.itb) image into DRAM and begins processing it. This FIT image includes device tree blobs (dtbs) for all supported boards, as well as Arm Trusted Firmware (ATF) and U-Boot itself. Verification of this FIT image occurs through HABv4 (High Assurance Boot version 4) functionality, enabled during the signing step.
- U-Boot FIT Image Authentication: The authenticity of the FIT image used by U-Boot itself (u-boot.itb) containing U-Boot proper, the Arm Trusted Firmware (ATF) and the device tree blobs (DTB's) is validated through HABv4, extending the authentication mechanism used by the BOOT_ROM to U-Boot. This authentication process involves signing the flash.bin during the signing step. NXP has extended the use of this authentication mechanism, theoretically allowing for the utilization of CAAM (Cryptographic Acceleration and Assurance Module) functions on a command-line basis.
- Generation of Binaries via Binman: The creation of various binaries used to generate the flash.bin file via Binman has evolved significantly over the years. The Binman tool generates these binaries based on device tree syntax defined in a 'binman' device tree node. This syntax can be found in the imx8mm-u-boot.dtsi file for those that want to know more.
- Control Device Tree: U-Boot SPL utilizes a generic device tree (DT) named 'imx8mm-venice' (CONFIG_DEFAULT_DEVICE_TREE) for control. This DT includes essential components common to all Venice boards (imx8mm, imx8mn, imx8mp) needed for early PMIC and DRAM configuration.
- FIT Image Verification: The FIT image is processed by U-Boot SPL, which passes each encountered dt name to a function determining if the dt should be used. This function selects the appropriate dt based on the EEPROM model of the base/som. The FIT image contains every dtb supported by the flash.bin.
- Signature Node Integration: The 'control dtb' used by U-Boot proper must contain the signature node for external FIT image validation. A external FIT image can be used to contain a kernel, ramdisk, and kernel device-tree blobs (DTB's)
See also Generic Secure Boot Wiki Page for information on securing the rest of your firmware.
i.MX8M High Assurance Boot (HAB)
The i.MX family of processors provides a High Assurance Boot (HAB) feature in the on-chip BOOT ROM responsible for loading the initial program image from the boot media. HAB enables the BOOT ROM to authenticate and/or decrypt the program image by using crypto operations.
The HABv4 secure boot feature uses digital signatures to prevent unauthorized code execution during the device boot sequence. This authentication is based on public key cryptography using RSA where the firmware image data is signed offline using a private key and the resulting signed image data is verified on the processor using the corresponding public key hash value programmed into the SoC fuses for establishing the root of trust.
See also:
- https://elixir.bootlin.com/u-boot/latest/source/doc/imx/habv4/guides/mx8m_spl_secure_boot.txt
- https://elixir.bootlin.com/u-boot/latest/source/doc/imx/habv4/introduction_habv4.txt
- NXP AN4581 - i.MX Secure Boot on HABv4 Supported Devices
Terminology:
- CSF: Command Sequence File (generated off-line using the HAB CST)
- CST: Code-Signing Tool
- DCD: Device Configuration Data
- DEK: Data Encryption Key
- HAB: High Assurance Boot
- IVT: Image Vector Table
- SRK: Super Root Key
In order to use High Assurance Boot (HAB) features you must have the NXP Code Signing Tool (CST): https://www.nxp.com/webapp/Download?colCode=IMX_CST_TOOL_NEW
i.MX secure boot
Boards using U-Boot for boot firmware support using HABv4 authentication for both the SPL and U-Boot stages.
The HAB library is a sub-component of the boot ROM on i.MX processors. It is responsible for verifying the digital signatures included as part of the product software and ensures that, when the processor is configured as a secure device, no unauthenticated code is allowed to run.
On an 'open' device you can see HAB events which will tell you if the image would pass the authentication process. This is useful to test before you 'close' the device.
In General you must:
- Build boot firmware that contains HABv4 support
- CONFIG_IMX_HAB=y ('hab_auto_img', 'hab_status' and 'hab_version' cmds)
- CONFIG_SPL_LOAD_FIT_ADDRESS=0x44000000
- Create a PKI tree and SRK table via the NXP Code Signing Tool
- Construct boot firmware with a proper Command Sequence File (CSF) (CSF blobs are created with the NXP Code Signing Tool)
- Blow One Time Programmable (OTP) fuses on the target board with public keys
- Flash signed firmware
- Boot and verify no HAB events via 'hab_status' U-Boot command
- Close the device to force trusted boot
Detailed Procedure (for Venice) on x86 development host machine with an existing venice bsp build directory on it (VENICE_BSP env var):
- this configuration is for using HABv4 for image authentication only on the Gateworks 2023.04 U-Boot branch. If you instead want to use DEK blob encap/decap support you need to to also add a Trusted Execution Environment (TEE) (see below)
- Creation of Code Signing Key: This is an example - read the CST documentation and tailor to your needs
- Retrieve the NXP Code Signing Tool (CST): https://www.nxp.com/webapp/Download?colCode=IMX_CST_TOOL_NEW (Account required on NXP site)
- Unpack the CST :
tar xvf cst-3.3.2.tgz cd cst-3.3.2/keys
- Create a text file named "serial", which contains 8 digits. OpenSSL uses the contents of this file for the 'certificate serial numbers'.
echo "12345678" > serial
- Create a text file named "key_pass.txt which contains your pass phrase that will protect the HAB code signing private keys. The format is the first pass phrase repeated on the first and second lines:
PASS=mypassphrase printf "$PASS\n$PASS" > key_pass.txt
- Create the signature keys (PKI tree) with hab4_pki_tree.sh (Must do this in the keys dir as the script hard-codes a relative path to certs)
./hab4_pki_tree.sh ... Do you want to use an existing CA key (y/n)?: n Select the key type (possible values: rsa, rsa-pss, ecc)?: rsa Enter key length in bits for PKI tree: 4096 Enter PKI tree duration (years): 10 How many Super Root Keys should be generated? 4 Do you want the SRK certificates to have the CA flag set? (y/n)?: y ...
- this creates the files in the ../crts directory which you can archive away as your 'PKI tree'
- Create the fuse table and binary (to be programmed to IMX OPT fuse blocks) using the SRK*_ca_crt.pem files created in the crts dir with srktool:
cd ../crts ../linux64/bin/srktool -h 4 -t SRK_1_2_3_4_table.bin -e SRK_1_2_3_4_fuse.bin -d sha256 -c ./SRK1_sha256_4096_65537_v3_ca_crt.pem,./SRK2_sha256_4096_65537_v3_ca_crt.pem,./SRK3_sha256_4096_65537_v3_ca_crt.pem,./SRK4_sha256_4096_65537_v3_ca_crt.pem
- creates SRK_1_2_3_4_table.bin SRK_1_2_3_4_fuse.bin and
- Use hexdump to obtain the fuse table (8x 32bit fuse values) in the correct endianness for programming with u-boot 'fuse prog':
$ hexdump -e '/4 "0x"' -e '/4 "%X""\n"' < SRK_1_2_3_4_fuse.bin 0xDCE644DB 0x3900ABA 0x1D00ECF6 0xC4EE5E23 0x5BCA8A8 0x75B0AB86 0xF88753CC 0xDB9B5895
- Note the above fuse values will differ per your serial/passphrase
- Build U-boot with HABv4 enabled and a single DTB:
- Pre-requisite: Build Venice BSP using this link here: venice/bsp
- Export VENICE_BSP variable to your directory:
export VENICE_BSP=/path/to/your/bsp/directory
# checkout a fresh u-boot git clone https://github.com/Gateworks/uboot-venice.git cd u-boot # setup cross toolchain environment (ie source setup-environment in Venice BSP dir) export PATH=$VENICE_BSP/buildroot/output/host/bin:$PATH export CROSS_COMPILE="aarch64-linux-" export ARCH=arm64 # copy necessary artifacts from bsp cp $VENICE_BSP/u-boot/lpddr4*.bin . # DDR firmware cp $VENICE_BSP/atf/build/imx8mm/release/bl31.bin . # ATF # configure for venice board make imx8mm_venice_defconfig make menuconfig # select CONFIG_IMX_HAB=y and CONFIG_SPL_LOAD_FIT_ADDRESS=0x44000000 make flash.bin
- for clarity here are the differences in defconfig:
$ make savedefconfig && diff defconfig configs/imx8mm_venice_defconfig scripts/kconfig/conf --savedefconfig=defconfig Kconfig 21,22d20 < CONFIG_IMX_HAB=y < # CONFIG_CMD_DEKBLOB is not set 31d28 < CONFIG_SPL_LOAD_FIT_ADDRESS=0x44000000
- create a signed_flash.bin
# setup env to point to the CST export CST_DIR=/usr/src/nxp/cst-3.3.2/ export CST_BIN=$CST_DIR/linux64/bin/cst export CSF_KEY=$CST_DIR/crts/CSF1_1_sha256_4096_65537_v3_usr_crt.pem export IMG_KEY=$CST_DIR/crts/IMG1_1_sha256_4096_65537_v3_usr_crt.pem export SRK_TABLE=$CST_DIR/crts/SRK_1_2_3_4_table.bin export PATH=$CST_DIR/linux64/bin:$PATH # sign flash.bin /bin/sh doc/imx/habv4/csf_examples/mx8m/csf.sh # create a JTAG image (if needed) using one of the following (dependent on which SoC you are using) mkimage_jtag --emmc -s --partconf=boot0 \ flash.bin@boot0:erase_none:66-8192 > signed_u-boot_spl-imx8mm.bin # imx8mm emmc boot0 partition mkimage_jtag --emmc -s --partconf=boot0 \ flash.bin@boot0:erase_none:0-8192 > signed_u-boot_spl-imx8mp.bin # imx8mp/imx8mn emmc boot0 partition
- Program signed firmware image:
jtag_usbv4 -p signed_u-boot_spl-imx8mm.bin
- Booting this would look something like the following:
U-Boot SPL 2023.04-00034-g1f567dfbe119 (Jun 23 2023 - 15:53:20 -0700) GSCv3 : v61 0x1d6f RST:VIN Thermal protection:disabled RTC : 1970-01-01 0:00:31 UTC Model : GW7200-01-B1F Serial : 935180 MFGDate : 04-05-2023 PMIC : MP5416 (IMX8MM) DRAM : LPDDR4 4 GiB 3000MT/s 1500MHz Failed to initialize caam_jr: -19 WDT: Started watchdog@30280000 with servicing every 1000ms (60s timeout) Trying to boot from eMMC hab fuse not enabled Authenticate image from DDR location 0x44000000... DTB : imx8mm-venice-gw72xx-0x NOTICE: BL31: v2.4(release):f884ad7b0ba2 NOTICE: BL31: Built : 13:06:09, Oct 20 2021 U-Boot 2023.04-00034-g1f567dfbe119 (Jun 23 2023 - 15:53:20 -0700)
- Note the 'hab fuse not enabled' message which means the SEC_CONFIG[1] fuse is not blown and the device is not locked
- Note the 'Authenticate image from DDR location' messages which shows that image authentication is able to be used
- Booting this would look something like the following:
- Program SRK Hash fuses from Step 1 into IMX OTP (using U-Boot and the keys from fuse bin)
- Do not use the above fuse values - use values generated above from your serial/passphrase
- OTP fuses can only be programmed once - be careful to use the correct values
- IF YOU WISH TO IMPLEMENT DISK ENCRYPTION AS WELL: FINISH FDE SETUP AND COME BACK TO THIS
fuse prog -y 6 0 0xDCE644DB fuse prog -y 6 1 0x3900ABA fuse prog -y 6 2 0x1D00ECF6 fuse prog -y 6 3 0xC4EE5E23 fuse prog -y 7 0 0x5BCA8A8 fuse prog -y 7 1 0x75B0AB86 fuse prog -y 7 2 0xF88753CC fuse prog -y 7 3 0xDB9B5895
- Boot it again and verify no HAB events:
u-boot=> hab_status Secure boot disabled HAB Configuration: 0xf0, HAB State: 0x66 No HAB Events Found!
- Close the device (lock it down!) - this step is irreversible, make sure there are no HAB events from the prior step
u-boot=> fuse prog -y 1 3 0x2000000
- This sets the SEC_CONFIG[1] fuse on the i.MX8M and once done the processor will not load an image that has not been signed using the correct PKI tree
For more info see:
- https://elixir.bootlin.com/u-boot/latest/source/doc/imx/habv4/introduction_habv4.txt
- https://elixir.bootlin.com/u-boot/latest/source/doc/imx/habv4/guides/mx8m_spl_secure_boot.txt
Trusted Execution Environment (TEE)
The Trusted Execution Environment (TEE) is a set of specifications published by the GlobalPlatform association. The purpose of the TEE is to provide a safe environment within the application processor for developing and executing secure applications. We call an application processor a system running a Rich OS like Android or Linux. A Rich environment represents a huge amount of code. It is open to third-party applications and it is an open ecosystem: it makes a Rich OS hard to audit. It is prone to bugs/vulnerability, which may compromise the security and integrity of the entire system. The TEE offers another level of protection against attacks from the rich OS. The TEE is only open to trusted partners, which makes it easier to audit. It executes only trusted and authorized software. All sensitive data are protected from the rest of the application processor and from the outside world.
Many modern devices make use of a Trusted Execution Environment, including smartphones, set-top-boxes, game consoles and Smart TVs. Some example use cases of a TEE:
- biometric authentication (ie facial, fingerprint, voice recognition) code and sensitive data
- e-commerce digital wallet code and sensitive data
- DRM credentials
Open Portable Trusted Execution Environment (OP-TEE)
OP-TEE (Open Portable Trusted Execution Environment) is an open-source TEE designed as a companion to a non-secure Linux kernel running on ARM Cortex-A cores using ARM's TrustZone technology.
The TEE relies on the Arm TrustZone technology. The TrustZone is a system-on-chip security feature available on most Arm Cortex A/M processors. It provides a strict hardware isolation between the secure world (TEE) and the normal world (REE). This technology allows each physical processor core to provide two virtual cores: one for the normal world and one for the secure world.
OP-TEE is an open source stack of the Trusted Execution Environment which includes:
- OP-TEE OS: Trusted side of the TEE
- OP-TEE Client: Normal world client side of the TEE
- OP-TEE Test (or xtest): OP-TEE Test Suite
The OP-TEE project is developed and maintained by Linaro under BSD 2-Clause. The source code is available at https:// github.com/OP-TEE. This stack supports Arm-v7 and Arm-v8 architectures.
The TEE exposes its features through a tandem operation between a Client Application and a Trusted Application. The client application runs in the Rich OS and always initiates the communication with the Trusted Application that runs in the Trusted OS. The Client application interacts with the TEE through the TEE client API interface. The Secure Application interacts with the TEE Core through the TEE Internal API.
OP-TEE is a Trusted Execution Environment (TEE) designed as a companion to a non-secure Linux kernel running on Arm cores using the TrustZone technology. OP-TEE implements TEE Internal Core API v1.1.x which is the API exposed to Trusted Applications and the TEE Client API v1.0, which is the API describing how to communicate with a TEE. Those APIs are defined in the GlobalPlatform API specifications.
The non-secure OS is referred to as the Rich Execution Environment (REE) in TEE specifications. It is typically a Linux OS flavor as a GNU/Linux distribution or the AOSP.
OP-TEE is designed primarily to rely on the Arm TrustZone technology as the underlying hardware isolation mechanism. However, it has been structured to be compatible with any isolation technology suitable for the TEE concept and goals, such as running as a virtual machine or on a dedicated CPU.
The main design goals for OP-TEE are:
- Isolation - the TEE provides isolation from the non-secure OS and protects the loaded Trusted Applications (TAs) from each other using underlying hardware support,
- Small footprint - the TEE should remain small enough to reside in a reasonable amount of on-chip memory as found on Arm based systems,
- Portability - the TEE aims at being easily pluggable to different architectures and available HW and has to support various setups such as multiple client OSes or multiple TEEs.
For more info:
- see - see http://optee.readthedocs.io
- https://www.nxp.com/docs/en/user-guide/IMX_PORTING_GUIDE.pdf
OP-TEE on venice
In general the following things need to be done to use OP-TEE in the 'SPL -> ATF -> OP-TEE -> U-Boot -> Linux' boot flow:
- build ATF (bl31.bin) with OP-TEE support:
- Add 'SPD=opteed' to the env to use a Secure Payload Dispatcher (SPD)
- Add 'BL32_BASE=' to the env to tell the ATF (BL31) where TEE is in memory (BL32)
- build TEE (tee.bin):
- PLATFORM=imx
- PLATFORM_FLAVOR=mx8mmevk|mx8mnevk|mv8mpevk - Use one of these depending on which SOC (imx8mm/imx8mn/imx8mp) you are using. These define CFG_UART_BASE and some memory config which we will override to suit our needs
- CFG_DDR_SIZE= - Specify the DRAM size of the board you are building for. For example 1GiB=0x40000000, 2GiB=0x80000000, 4GiB=0x10000000
- CFG_CORE_LARGE_PHYS_ADDR= - n for 3GiB or less DRAM, and y for larger than 3GiB DRAM
- CFG_CORE_ARM64_PA_BITS= - 32 for 3GiB or less DRAM, and 36 for larger than 3GiB DRAM
- CFG_TZDRAM_START= - the (link/load) address where TEE should run at
- build u-Boot with the following configuration:
- CONFIG_IMX_HAB=y
- CONFIG_CMD_DEKBLOB=y
- CONFIG_SPL_LOAD_FIT_ADDRESS=0x48000000
- CONFIG_OPTEE=y
- CONFIG_OPTEE_LOAD_ADDRESS= the (link/load) address for TEE that should match BL32_BASE for ATF and CFG_TZDRAM_START for OPTEE
Note that you have to tailor the firmware specifically for the DRAM size of your board and that it is very important to ensure that BL32_BASE (used when building ATF), CFG_TZDRAM_START (used when building OPTEE), and CONFIG_OPTEE_LOAD_ADDRESS (U-Boot's .config file) all match. This address should be the top 32MiB of DRAM but because U-Boot is loading this from its FIT image into DRAM it needs to be a 32bit address and should be adjusted down if you are on a board with 4GiB:
- 1GiB DRAM use 0x7e000000
- 2GiB DRAM use 0xbe000000
- 3GiB or larger use 0xfe000000
Here are the detailed steps to build secure boot firmware for imx8mm:
- setup cross compile environment
# setup cross compile environment - here we will use the venice bsp toolchain/config cd venice/bsp . ./setup-environment
- setup additional SOC and Board specific environment:
- for imx8mm:
export PLATFORM_FLAVOR=mx8mmevk # used for OPTEE build export PLAT=imx8mm # used for ATF build
- for imx8mp:
export PLATFORM_FLAVOR=mx8mpevk # used for OPTEE build export PLAT=imx8mp # used for ATF build
- for 1GiB DRAM boards:
export CFG_DDR_SIZE=0x40000000 export CFG_CORE_LARGE_PHYS_ADDR=n export CFG_CORE_ARM64_PA_BITS=32 export CFG_TZDRAM_START=0x7f000000
- for 2GiB DRAM boards:
export CFG_DDR_SIZE=0x80000000 export CFG_CORE_LARGE_PHYS_ADDR=n export CFG_CORE_ARM64_PA_BITS=32 export CFG_TZDRAM_START=0xbf000000
- for 4GiB DRAM boards:
export CFG_DDR_SIZE=0x100000000 export CFG_CORE_LARGE_PHYS_ADDR=y export CFG_CORE_ARM64_PA_BITS=36 export CFG_TZDRAM_START=0xfe000000
- for imx8mm:
- Create directories: We will create a 'secure-boot' directory containing u-boot, atf, and optee:
git clone https://github.com/Gateworks/uboot-venice.git secure-boot git clone https://github.com/nxp-imx/imx-optee-os -b lf-6.1.1_1.0.0 secure-boot/tee git clone http://github.com/Gateworks/atf-venice -b lf_v2.6 secure-boot/atf cd secure-boot
- Get IMX DDR training firmware:
wget https://www.nxp.com/lgfiles/NMG/MAD/YOCTO/firmware-imx-8.10.bin /bin/sh firmware-imx-8.10.bin cp firmware-imx-8.10/firmware/ddr/synopsys/lpddr4*.bin .
- Build OP-TEE
make -j8 -C tee \ ARCH=arm \ CROSS_COMPILE64=$CROSS_COMPILE \ CFG_TEE_CORE_LOG_LEVEL=2 \ PLATFORM=imx \ PLATFORM_FLAVOR=$PLATFORM_FLAVOR \ O=out && ${CROSS_COMPILE}objcopy -O binary tee/out/core/tee.elf ./tee.bin
- Build ATF:
make -j8 -C atf SPD=opteed PLAT=$PLAT BL32_BASE=$CFG_TZDRAM_START && \ cp atf/build/$PLAT/release/bl31.bin ./bl31.bin
- Build U-Boot:
# start with standard venice defconfig make imx8mm_venice_defconfig # choose imx8mm/imx8mn/imx8mp depending on board SOC # enable IMX_HAB/OPTEE make menuconfig # search for (with /) and set CONFIG_IMX_HAB=y CONFIG_CMD_DEKBLOB=y CONFIG_SPL_LOAD_FIT_ADDRESS=0x48000000 CONFIG_OPTEE=y and CONFIG_OPTEE_LOAD_ADDRESS= set to the value of CFG_TZDRAM_START make -j8 flash.bin
- Sign it:
# setup env to point to the CST export CST_DIR=/usr/src/nxp/cst-3.3.2/ export CST_BIN=$CST_DIR/linux64/bin/cst export CSF_KEY=$CST_DIR/crts/CSF1_1_sha256_4096_65537_v3_usr_crt.pem export IMG_KEY=$CST_DIR/crts/IMG1_1_sha256_4096_65537_v3_usr_crt.pem export SRK_TABLE=$CST_DIR/crts/SRK_1_2_3_4_table.bin export PATH=$CST_DIR/linux64/bin:$PATH # sign flash.bin /bin/sh doc/imx/habv4/csf_examples/mx8m/csf.sh # create a JTAG image (if needed) using one of the following depending on your SoC mkimage_jtag --emmc -s --partconf=boot0 \ flash.bin@boot0:erase_all:66-8192 > signed_u-boot_spl-imx8mm.bin # imx8mm emmc boot0 partition mkimage_jtag --emmc -s --partconf=boot0 \ flash.bin@boot0:erase_all:0-8192 > signed_u-boot_spl-imx8mp.bin # imx8mp emmc boot0 partition mkimage_jtag --emmc -s --partconf=boot0 \ flash.bin@boot0:erase_all:0-8192 > signed_u-boot_spl-imx8mn.bin # imx8mn emmc boot0 partition
Full Disk Encryption (FDE)
While i.MX secure boot ensures a secure boot process and prevents unauthorized code from executing during the initial stages, maintaining a secure environment throughout the entire system lifecycle requires additional measures. One crucial aspect is protecting sensitive data stored on persistent storage devices by encrypting the file system. To achieve this, we will utilize Full Disk Encryption (FDE) using Linux Unifified Key Setup (LUKS). For this we need an initial ramdisk (aka 'initrd') containing an init script that can be used for provisioning an encrypted storage device, or unlocking the encrypted storage device for use. This approach ensures data-at-rest protection and establishes a secure Linux environment, complementing the security foundation established by i.MX secure boot.
Components of FDE using LUKS
In our approach to achieving Full Disk Encryption (FDE), we employ Linux Unified Key Setup (LUKS) paired with a ramdisk using a custom init script to mount the encrypted filesystem. LUKS serves as a robust encryption tool, offering comprehensive solutions for securing data on Linux systems. By utilizing LUKS, we ensure the entire storage device is encrypted, thereby safeguarding sensitive information from unauthorized access.
- Why use LUKS?
Robust Encryption Mechanisms: LUKS offers a comprehensive solution for Full Disk Encryption, providing robust encryption mechanisms that balance security, usability, and compatibility effectively.
Trusted Tool: As a prominent tool within Linux systems, LUKS is widely trusted and adopted, ensuring reliability and compatibility across various platforms and distributions.-
- Why use a ramdisk?
We need a 'shim' between the kernel and the encrypted root filesystem as the kernel does not have the custom support needed to mount an encrypted filesystem (such as the key).
Volatility: RAM disks are volatile, meaning their contents are lost when the system powers down. As a result, any sensitive data stored in them, such as encryption keys, won't persist across reboots. This limits the window of opportunity for attackers to extract the keys.
Isolation: By storing the encryption key in a RAM disk, you isolate it from the main storage device. This reduces the likelihood of unauthorized access or tampering since the key isn't stored alongside the encrypted data.
Speed: Accessing data from a RAM disk is faster compared to accessing data from traditional storage devices like hard drives or SSDs. This can potentially speed up the decryption process.
This demonstration's main steps for implementing FDE will include:
1) Using Buildroot for Ramdisk Creation
2) Using the Ramdisk for Provisioning and unlocking the storage device via LUKS
Kernel and DTB
We are going to need a kernel and DTB for your board. For this demonstration we will use the kernel and DTB's from the pre-built Gateworks Linux tarball:
mkdir blobs # grab the prebuilt gateworks kernel wget https://dev.gateworks.com/venice/kernel/linux-venice-6.6.8.tar.xz tar -C blobs/ --strip-components=2 -xvf linux-venice-6.6.8.tar.xz ./boot # gzip the kernel gzip -fk blobs/Image # creates blob/Image.gz
you can choose your own kernel/dtb/rootfs as long as they provide support for dm-crypt
Ramdisk Creation
We offer two pathways for obtaining a RAM disk tailored for our Full Disk Encryption (FDE) approach:
- Conveniently download a prebuilt ramdisk from our dev.gateworks website and inject our custom init script.
- Construct it through Buildroot, offering greater customization capabilities.
Both of these methods require having our custom init script and a key to be used for locking and unlocking the filesystem.
For our demonstration, we will just create some random data to be used as our filesystem encryption key via:
dd if=/dev/urandom of=fs.key bs=1 count=4096
The init script we will use can be created like this:
cat <<\EOF> init #!/bin/sh # This is to be used by a buildroot minimal rootfs on the target board # as an init script to: # - provision flash filesystem # - mount flash filesystem # # the flash storage (ie create disk partition, filesystems, with/without encryption, and populate them) # Some basic vars DEV=/dev/mmcblk2 # storage device to be used for the encrypted file system P1_OFFSETMB=64 PART=p1 NAME=rootfs # name for the encrypted file system (only used here) KEY=/fs.key NIC=eth0 INIT=/sbin/init # init of final OS # 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 error() { printf "\n\nErro: $@" while [ 1 ]; do sleep 1; done } user_data() { echo "Applying user changes..." # in any board specific changes to rootfs case "$(cat /proc/device-tree/board)" in GW74*) mkdir -p /mnt/etc/udev/rules.d echo 'SUBSYSTEM=="net", ACTION=="add", DEVPATH=="/devices/platform/soc@0/30800000.bus/30bf0000.ethernet/net/eth*", NAME="en0"' > /mnt/etc/udev/rules.d/70-persistent-net.rules echo 'SUBSYSTEM=="net", ACTION=="add", DEVPATH=="/devices/platform/soc@0/30800000.bus/30be0000.ethernet/net/eth*", NAME="en1"' >> /mnt/etc/udev/rules.d/70-persistent-net.rules sed -i 's/eth0/en0/' /mnt/etc/network/interfaces ;; esac } # example1: rootfs via a list of tarballs # - gateworks ubuntu tarbarll and linux kernel tarball ROOTFS_TARS="\ https://dev.gateworks.com/ubuntu/jammy/jammy-venice.tar.xz \ https://dev.gateworks.com/venice/kernel/linux-venice-6.6.8.tar.xz \ " rootfs1() { # Create an ext4 file system on the opened LUKS device echo "Creating filesystem..." mkfs.ext4 -q -F -L rootfs /dev/mapper/$NAME || error "mkfs.ext4 failed" # Mount the LUKS device to /mnt mount /dev/mapper/$NAME /mnt || error "mount failed" for url in $ROOTFS_TARS; do echo "Downloading $url..." wget -q --show-progress --no-check-certificate -O data $url || error "wget failed" echo "Extracting..." pv data | tar -C /mnt -mxJ --keep-directory-symlink || error "tar extract failed" done user_data # Unmount the LUKS device echo "Unmounting LUKS device..." umount /dev/mapper/$NAME || error "umount failed" } # example2: rootfs via compressed disk image rootfs2() { FILESYSTEM=https://dev.gateworks.com/venice/images/jammy-venice.img.gz # a compressed disk image contains a partition table as well as # partitions. Here we assume an MBR partition with the OS on P1. # we will extract the MBR, determine the start/end of P1 and copy # it to the LUKS device. echo "Downloading $FILESYSTEM..." wget -q --show-progress --no-check-certificate -O data $FILESYSTEM || exit 1 # Extract the MBR zcat data | dd of=mbr count=1 start=$(sfdisk -l mbr | tail -1 | tr -s ' ' | cut -d ' ' -f3) end=$(sfdisk -l mbr | tail -1 | tr -s ' ' | cut -d ' ' -f4) # copy the filesystem in P1 to the LUKS device echo "Copying..." pv data | zcat | dd of=/dev/mapper/$NAME skip=$start count=$((end-start)) # apply user data mount /dev/mapper/$NAME /mnt || error "mount failed" user_data umount /dev/mapper/$NAME || error "umount failed" } # example3: rootfs via existing filesystem rootfs3() { FILESYSTEM=https://dev.gateworks.com/buildroot/venice/minimal/rootfs.ext2.xz echo "Downloading $FILESYSTEM..." wget -q --show-progress --no-check-certificate -O data $FILESYSTEM || exit 1 # copy the filesystem in P1 to the LUKS device echo "Copying..." pv data | xzcat | dd of=/dev/mapper/$NAME bs=16M # apply user data mount /dev/mapper/$NAME /mnt || error "mount failed" user_data umount /dev/mapper/$NAME || error "umount failed" } provision() { rootfs=${1:-rootfs1} echo "Provision $DEV via $rootfs..." # board specific customizations case "$(cat /proc/device-tree/board)" in GW74*) NIC=eth1;; esac # Mount the /tmp directory as a tmpfs with 75% of the available memory mount -t tmpfs /tmp -o size=75% # Change the current working directory to /tmp cd /tmp # bring up network echo "Bringing up network $NIC..." udhcpc -i $NIC echo "Creating partition table..." # partition disk: # - you could use MBR or GPT based on your needs # - ensure that you do not start your partition data until after your boot firmware # Create MBR with a single partition taking up the entire device printf "$((P1_OFFSETMB*1024*1024/512)),,L,*" | sfdisk -u S $DEV || exit 1 DEV=${DEV}${PART} # Format the partition as a LUKS encrypted file system using the encryption key echo "Formating LUKS device..." echo "YES" | cryptsetup luksFormat $DEV $KEY - || exit 1 # Open the LUKS encrypted partition and map it to the specified name ($NAME) echo "Opening LUKS device..." cryptsetup luksOpen $DEV $NAME --key-file=$KEY || exit 1 # call rootfs option $rootfs # Close the LUKS device and remove the mapping echo "Closing LUKS device..." cryptsetup luksClose $NAME || exit 1 echo "Provisioning complete" # Wait forever #while [ 1 ]; do sleep 1; done # power cycle echo 2 > /sys/bus/i2c/devices/0-0020/powerdown } for x in $(cat /proc/cmdline); do case "$x" in bypass) echo "Booting straight to ramdisk..." umount /proc umount /sys umount /run if (exec 0</dev/console) 2>/dev/null; then exec 0</dev/console exec 1>/dev/console exec 2>/dev/console fi exec /sbin/init "$@" ;; provision=*) provision ${x//provision=};; esac done echo "Using $DEV..." # Wait for device to exist echo "Waiting for $DEV..." while [ ! -b "$DEV" ]; do sleep 1 echo -n . done # Open the LUKS encrypted partition and map it to the specified name ($NAME) DEV=${DEV}${PART} echo "Opening $DEV..." cryptsetup luksOpen $DEV $NAME --key-file=$KEY || exit 1 # Mount the LUKS device to /mnt echo "Mounting $DEV..." mount /dev/mapper/$NAME /mnt || exit 1 # Switch to the new root and execute init echo "Switching to new root and running $INIT $@..." cd /mnt [ -c dev/console ] || mknod -m 600 dev/console c 5 1 exec switch_root . "$INIT" "$@" # This will only be run if the above line failed echo "Failed to switch_root" EOF
This init script has the following features:
- demonstrates provisioning an encrypted filesystem via a kernel cmdline argument allowing three different provisioning options (described below)
- demonstrates mounting an encrypted filesystem
Note: If you wish to encrypt the default storage device using the init script, it is configured for the eMMC on Venice boards (/dev/mmcblk2) by default. You are free to modify this according to your requirements. Feel free to specify partitions, change the default storage device, or add any other customization you desire to the script.
Option1: Downloading a prebuilt ramdisk
To download the prebuilt root file system and incorporate your script manually, follow these steps:
# download the ramdisk wget https://dev.gateworks.com/buildroot/venice/minimal/rootfs.cpio.xz # extract files from an existing gzipped cpio: mkdir initrd (cd initrd; xz -cd ../rootfs.cpio.xz | fakeroot -s ../initrd.fakeroot cpio -idmv) # copy key and init to it cp fs.key init initrd/ # re-create initrd (cd initrd; find | fakeroot -i ../initrd.fakeroot cpio -o -H newc | xz --check=crc32 -c > ../rootfs.cpio.xz)
This will produce the desired rootfs.cpio.xz artifact which we will copy to our blobs directory for safe keeping:
cp rootfs.cpio.xz blobs/
Option2: Creating Buildroot Ramdisk
Typically you should choose this option over downloading the prebuilt Gateworks ramdisk only if you want to add additional functionality to the init script that your ramdisk uses, and that functionality is dependent on configuration options our ramdisk does not have enabled. To create the root file system manually, follow these steps:
- Clone the Buildroot repository:
git clone https://github.com/buildroot/buildroot.git cd buildroot
- Get a copy of Gateworks buildroot defconfigs
wget -O configs/venice_example_defconfig https://dev.gateworks.com/buildroot/venice/minimal/venice_example_defconfig wget https://dev.gateworks.com/buildroot/venice/minimal/venice_minimal_kernel_6.1_defconfig
- Make an overlay directory and get the custom init script as well as the filesystem key there
mkdir overlay cp init overlay/init cp fs.key overlay/
When specifying the overlay directory path in the Buildroot configuration (BR2_ROOTFS_OVERLAY), you simply provide the path to the directory you've chosen, regardless of its name. As long as the path is correctly specified, Buildroot will incorporate the contents of the overlay directory into the final root filesystem image during the build process.
- Buildroot's make command generates multiple artifacts, however, our focus is solely on the rootfs.cpio.xz file it produces
# make the custom filesystem make venice_example_defconfig make
This will produce output/images/rootfs.cpio.xz which we will copy to our blobs directory for safe keeping:
cp output/images/rootfs.cpio.xz blobs/
Using the Ramdisk and custom init script
Configuring Bootargs for Different init script use cases
Regardless of what option you choose for Secure System Setup in the next section, you will need to be familiar with how the example init script is intended to be used once you get your board to u-boot: this is a preliminary section describing how to use the script.
In order to utilize the custom init script properly, you can configure the bootargs variable at the U-Boot prompt to specify different provisioning options for setting up the Full Disk Encryption (FDE) and unlocking your FDE. Below are the available options:
- rootfs1 (provision option):
Use this option to provision the emmc by creating an ext4 filesystem on the LUKS device and downloading and extracting root file system components (kernel and rootfs) from specified URLs.
setenv bootargs provision=rootfs1
- rootfs2 (provision option):
This option provisions the emmc from a compressed disk image containing a partition with the root file system. The image is extracted, and the root file system is copied to the LUKS device.
setenv bootargs provision=rootfs2
- rootfs3 (provision option):
This option provisions the emmc from an existing compressed file system that can be downloaded and directly copied to the LUKS device.
setenv bootargs provision=rootfs3
- bypass (debugging):
Set bootargs to bypass to bypass normal provisioning/mounting and drop you into a bash shell with the buildroot root filesystem. This is useful for debugging and prototyping custom provisioning commands.
setenv bootargs bypass
- Unlocking the FDE (Default):
Leaving bootargs blank or not specifying any provisioning option will configure the system for regular use and unlocking the Full Disk Encryption. This is the default case where you have already provisioned your storage device and the FDE is switched to be used as the root file system.
Note: Provisioning options are intended to be used as a one-time process during system setup.
Confirming working operation of individual components
We strongly suggest to do a confirmation test that your kernel image, device tree, and ramdisk all work as intended before moving on.
At U-Boot, you have the option to choose the behavior:
- Using the ramdisk for encrypting a storage device (typically done only once)
setenv bootargs provision=rootfs1
- Using the ramdisk for unlocking a storage device (default)
setenv bootargs
Once you have your Kernel, ramdisk and device tree blob successfully copied over to a tftp server, we suggest confirming their working operation before continuing. You should be able to boot your ramdisk as follows
on host:
- copy the kernel Image, your dtb, and your rootfs.cpio.xz ramdisk to your tftp server venice directory
cp blobs/imx8mm-venice-gw73xx-0x.dtb blobs/Image blobs/rootfs.cpio.xz /tftpboot/venice/
- target; boot them via tftp
u-boot=> dhcp && setenv serverip <serverip> u-boot=> setenv bootargs bypass; tftpboot $kernel_addr_r venice/Image && tftpboot $fdt_addr_r venice/imx8mm-venice-gw73xx-0x.dtb && tftpboot $ramdisk_addr_r venice/rootfs.cpio.xz && booti $kernel_addr_r $ramdisk_addr_r:$filesize $fdt_addr_r
- it is important here that you tftp the ramdisk last as we use the filesize variable which is set by tftp to give booti the size of the ramdisk which is requires
Choosing the Boot Method for Secure System Setup
Now that we have our ramdisk, kernel, and device tree blob we have a few different options to continue on our path to having a secure system.
- Option 1: Creating a signed FIT image and transferring it over TFTP, and flashing the fit image to memory.
- Option 2: Incorporating the ramdisk, kernel, and device tree into the firmware image itself with HABv4 secure boot via u-boots fit image, and flashing the fit image to memory.
In comparing Option 1 and Option 2, both provide robust firmware security through code signing and verification. However, Option 2 goes a step further by integrating the verification into the hardware root-of-trust using platform security features like HABv4.
Option 1 provides more versatility in terms of being able to construct different FIT image payloads signed by the same key that U-Boot can universally verify and boot from. This modularity can be advantageous, especially for deployment scenarios that need frequent kernel/filesystem updates without modifying the secure bootloader components.
Option 1 - using an External FIT Image
This approach involves creating an external signed FIT (Flat Image Tree) image containing the ramdisk, kernel, and device tree which U-Boot can then verify (via the signature) before booting.
The benefit of this is:
- this is how its usually done
- this allows you to leave your boot firmware fairly small so that it still fits in the eMMC boot0 hardware partition
The downside is:
- it requires another key for the external FIT image vs using the SRK fuse settings and HABv4
FIT image background information
U-Boot supports signed FIT images which allow you to cryptographically validate various binaries and configurations within the FIT image.
The FIT format is already widely used in U-Boot. It is a flattened device tree (FDT) in a particular format, with images contained within. FIT images include hashes to verify images, so it is relatively straightforward to add signatures as well.
The public key can be stored in U-Boot's CONFIG_OF_CONTROL device tree in a standard place. Then when a FIT is loaded it can be verified using that public key. Multiple keys and multiple signatures are supported.
FIT supports hashing of images so that these hashes can be checked on loading. This protects against corruption of the image. However it does not prevent the substitution of one image for another.
The signature feature allows the hash to be signed with a private key such that it can be verified using a public key later. Provided that the private key is kept secret and the public key is stored in a non-volatile place, any image can be verified in this way.
While signing images is useful, it does not provide complete protection against several types of attack. For example, it is possible to create a FIT with the same signed images, but with the configuration changed such that a different one is selected (mix and match attack). It is also possible to substitute a signed image from an older FIT version into a newer FIT (roll-back attack).
To solve this problem, we support signed configurations. In this case it is the configurations that are signed, not the image. Each image has its own hash, and we include the hash in the configuration signature.
The procedure for signing is as follows:
- hash an image in the FIT
- sign the hash with a private key to produce a signature
- store the resulting signature in the FIT
The procedure for verification is:
- read the FIT
- obtain the public key
- extract the signature from the FIT
- hash the image from the FIT
- verify (with the public key) that the extracted signature matches the hash
The verification is done in U-Boot on the device using a /signature node in U-Boot’s control FDT. If U-Boot is building the DTB’s the signature node must be added to your specific board DTB by inserting it into the dts source files (which can easily be done by putting it in the root node of arch/arm/dts/imx8mm-u-boot.dtsi, arch/arm/dts/imx8mn-u-boot.dtsi or arch/arm/dts/imx8mp-u-boot.dtsi depending on your board's SoC).
For more info on verified boot and FIT images see:
- https://elixir.bootlin.com/u-boot/v2023.04/source/doc/uImage.FIT/verified-boot.txt
- https://elixir.bootlin.com/u-boot/v2023.04/source/doc/uImage.FIT/signature.txt
- https://elixir.bootlin.com/u-boot/v2023.04/source/doc/uImage.FIT/sign-configs.its
Generation of Signed FIT Image
We will need a key to used for image signing and verification.
To create a new public/private 2048 bit key pair:
# on host machine openssl genpkey -algorithm RSA -out fit.key -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537 # to create a certificate for this containing the public key: openssl req -batch -new -x509 -key fit.key -out fit.crt # you can see the public key: openssl rsa -in fit.key -pubout
Create the ITS template where each image is defined to have a hash and each configuration is signed:
cat << EOF > fit.its /dts-v1/; / { description = "Image for Linux Kernel"; #address-cells = <1>; images { kernel { description = "Linux Kernel"; data = /incbin/("blobs/Image.gz"); type = "kernel"; arch = "arm64"; os = "linux"; compression = "gzip"; load =<0x48200000> ; entry = <0x48200000>; hash-1 { algo = "sha256"; }; }; ramdisk { description = "ramdisk"; data = /incbin/("blobs/rootfs.cpio.xz"); type = "ramdisk"; arch = "arm64"; os = "linux"; compression = "none"; load = <0x50300000>; hash-1 { algo = "sha256"; }; }; fdt { description = "fdt"; data = /incbin/("blobs/imx8mm-venice-gw72xx-0x.dtb"); type = "flat_dt"; arch = "arm64"; compression = "none"; load = <0x50200000>; hash-1 { algo = "sha256"; }; }; }; configurations { default = "config-1"; config-1 { description = "Linux configuration"; kernel = "kernel"; ramdisk = "ramdisk"; fdt = "fdt"; signature-1 { algo = "sha1,rsa2048"; key-name-hint = "fit"; sign-images = "kernel", "fdt", "ramdisk"; }; }; }; }; EOF
At this point we have a key (fit.key, fit.crt) and a template to create the FIT image from (fit.its). Note that we have elected above to use a compressed kernel (why not?) so we need to make sure we gzip the kernel to match the 'gzip' property value specified in its 'compression' node above.
Now we can now use the U-Boot mkimage tool (from the u-boot-tools package or from the u-boot build dir) to build the Image. We are going to use the '-K <dtb>' option to add a signature for the key into an existing DTB but U-Boot builds its control DTB itself so we will create a dummy DTB to put the key in then decompile it:
# create a dummy its file cat <<EOF> blobs/signature.dts /dts-v1/; / { }; EOF # compile the dts to a dtb with device-tree compiler dtc -I dts -O dtb blobs/signature.dts > blobs/signature.dtb # create the fit image and add the signature to the dtb mkimage -f fit.its -k . -K blobs/signature.dtb -r fit.itb # decompile the dummy dtb dtc -I dtb -O dts blobs/signature.dtb > blobs/signature.dts # now we have a signed fit.itb you can put on your board or tftp cp fit.itb /tftpboot/venice
- Note: the -k parameter is the directory that the fit image key resides that is specified by key-name-hint. So for example with key-name-hint "fit" as above; fit.key will be in the directory specified by the -k parameter.
Before continuing with securing uboot, a quick check to make sure this fit image boots is a good idea. This step is strictly to help with debugging if there is an issue with your image blobs or an issue with u-boot's build configuration.
setenv bootargs bypass; tftpboot 0x40200000 venice/fit.itb && bootm 0x40200000
- Note: we are loading into 0x40200000 instead of $loadaddr because loadaddr is the same as kerneladdr and our fit image is loaded there and they conflict.
Getting FIT key into U-Boot proper and verifying signed images
In order for U-boot to validate a signed FIT image it must have access to the key used to sign that image. This is why we created a signature.dtb in the previous step by using ‘mkimage -K’ to ‘inject’ a node into our signature.dtb.
Now we need to modify U-Boot to allow it to verify FIT images against that key:
- copy the signature into the imx8mm-u-boot.dtsi, imx8mn-u-boot.dtsi or imx8mp-u-boot.dtsi file depending on your SoC.
- enable CONFIG_FIT_SIGNATURE and RSA_VERIFY
- disable CONFIG_LEGACY_IMAGE_FORMAT so that unsigned (or signed images that failed verification!) are not allowed to boot
Procedure:
- Checkout U-Boot (or use your existing U-Boot that you have been working with):
git clone https://github.com/Gateworks/uboot-venice.git cd uboot-venice # copy necessary artifacts from bsp cp $VENICE_BSP/u-boot/lpddr4*.bin . # DDR firmware cp $VENICE_BSP/atf/build/imx8mm/release/bl31.bin . # ATF
- Make sure your environment is configured for cross-compiling:
(cd $VENICE_BSP; . ./setup-environment) make imx8mm_venice_defconfig
- Add the signature node from ../blobs/signature.dts to 'arch/arm/dts/imx8mm-venice-u-boot.dtsi' within the root node. You can position it either directly above or below the 'wdt-reboot' node. The 'imx8mm-venice-u-boot.dtsi' is included with all imx8mm-venice boards, which ensures that the signature node appears in all device tree blobs (dtbs) listed in CONFIG_OF_LIST. Placing it in 'imx8mm-venice-u-boot.dtsi' simplifies the process as there's no need to consider which specific board the user has.
- Use 'make menuconfig' to make the following changes:
- CONFIG_FIT_SIGNATURE=y - enable signature verification of FIT uImages using a hash signed and verified using RSA.
- RSA=y
- CONFIG_LEGACY_IMAGE_FORMAT is undefined - otherwise this allows booting unsigned images or any image if you lack a signature node
make menuconfig # enable FIT_SIGNATURE/RSA, disable LEGACY_IMAGE_FORMAT make savedefconfig && cp defconfig configs/imx8mm_venice_defconfig
- Its a good idea for troubleshooting to enable DEBUG in common/spl/spl_fit.c by adding '#define DEBUG' to the top of that file
When complete your differences should look like this:
$ git diff diff --git a/arch/arm/dts/imx8mm-u-boot.dtsi b/arch/arm/dts/imx8mm-u-boot.dtsi index 04a7093a66..6d1f7e7907 100644 --- a/arch/arm/dts/imx8mm-u-boot.dtsi +++ b/arch/arm/dts/imx8mm-u-boot.dtsi @@ -4,6 +4,20 @@ */ / { + signature { + + key-fit { + required = "conf"; + algo = "sha1,rsa2048"; + rsa,r-squared = <0x5123aa0b 0x92792834 0x3f805a1b 0xa85c3894 0x817b72af 0xe5f737ef 0x9c97b0b0 0xe25dc8e5 0xfa 6a455f 0x18112fca 0xc5cfdd34 0xcc9e62ee 0x53401b1 0xceb41498 0xd3697f64 0x95a198c1 0x568b83b 0xb6cfedde 0x719783df 0x85724f45 0xc0c7b 83a 0x6d692e33 0x46a8e77e 0xf72fba00 0x5f294060 0xa3b8ab2f 0xcdc10f08 0xdec24553 0xccd09d3e 0xfed61495 0xb6f034bc 0x5d7a3e1e 0x5a8ee4 4c 0xc58fd62 0xee38127d 0x7be833c0 0xa43e308e 0xaf26959f 0x286f45ed 0xe0d95788 0x9818f079 0xaf4bdcf3 0x88ee0caf 0x7fdcd2b7 0x41f1217b 0xaf1ce320 0x1582997f 0x17232232 0x4794518d 0x79746211 0x2c0adb7f 0xc0288fc2 0xc4851840 0x85bf410a 0xc05fbb3f 0x8f38e96f 0x6d6b5b43 0xb195adc7 0xcb786525 0x1bb9f2b 0xff26d159 0xb2eee232 0xbea15340 0xff9c4086>; + rsa,modulus = <0xb1430c9e 0x101ef408 0x66e818f5 0xde892ee1 0x7ec0ed73 0xd5a899b7 0x2f5d9012 0x5cefdc25 0x9e3f 8697 0x5c06cc4e 0x368cb44a 0x97d1d99a 0x25c6bf39 0x2bbc1028 0x306cc237 0x74f9adfd 0xf9811fe8 0x3efa493c 0xcc5e97bb 0x3d55977d 0x1f918 32a 0xb6f300d0 0x4b00182a 0x9ee2e97f 0x89d2f8cd 0x95b3a15d 0xc0c28d86 0x14dfce86 0x42b16994 0xbbca5180 0x273cea3a 0x97a6ee90 0x26db41 38 0xf30ac22a 0xc34ce324 0xc02e160f 0xfbc14603 0xa7f95016 0xe73d7f3b 0x2e6f15b7 0x78e54e1 0xaa98b25a 0xdbc2ea6d 0x598b03a8 0x9a5f14e4 0xbface2c0 0xb1726a07 0xb2848286 0x3c071c5b 0xedc37018 0xb4f00631 0x30b23649 0xd03c0477 0x6573dbe7 0xe2617b39 0x1fa5407d 0xeac68d0f 0xc08f4fb2 0x28d36d15 0x1ce825bd 0xf792c214 0xb48f369 0x146c427e 0xa74f0177>; + rsa,exponent = <0x00 0x10001>; + rsa,n0-inverse = <0xa2fbd7b9>; + rsa,num-bits = <0x800>; + key-name-hint = "fit"; + }; + }; + binman: binman { multiple-images; }; diff --git a/common/spl/spl_fit.c b/common/spl/spl_fit.c index 730639f756..f6955eabbd 100644 --- a/common/spl/spl_fit.c +++ b/common/spl/spl_fit.c @@ -3,6 +3,7 @@ * Copyright (C) 2016 Google, Inc * Written by Simon Glass <sjg@chromium.org> */ +#define DEBUG #include <common.h> #include <errno.h> diff --git a/configs/imx8mm_venice_defconfig b/configs/imx8mm_venice_defconfig index d254e2a012..d9511538c0 100644 --- a/configs/imx8mm_venice_defconfig +++ b/configs/imx8mm_venice_defconfig @@ -18,6 +18,8 @@ CONFIG_SPL_DRIVERS_MISC=y CONFIG_SPL_STACK=0x920000 CONFIG_SPL=y CONFIG_ENV_OFFSET_REDUND=0x3f8000 +CONFIG_IMX_HAB=y +# CONFIG_CMD_DEKBLOB is not set CONFIG_SYS_LOAD_ADDR=0x48200000 CONFIG_SYS_MEMTEST_START=0x40000000 CONFIG_SYS_MEMTEST_END=0x80000000 @@ -25,7 +27,9 @@ CONFIG_LTO=y CONFIG_SYS_MONITOR_LEN=524288 CONFIG_FIT=y CONFIG_FIT_EXTERNAL_OFFSET=0x3000 +CONFIG_FIT_SIGNATURE=y CONFIG_SPL_LOAD_FIT=y +CONFIG_SPL_LOAD_FIT_ADDRESS=0x44000000 CONFIG_OF_BOARD_SETUP=y CONFIG_OF_SYSTEM_SETUP=y CONFIG_DISTRO_DEFAULTS=y
Now build it and update your board (by creating a JTAG image or using the 'update_firmware' script on the resulting flash.bin)
When uboot boots verify that the signature node is in the new controlling fdt:
u=boot-> fdt addr $fdtcontroladdr && fdt print /signature Working FDT set to fdef4680 signature { key-fit { required = "conf"; algo = "sha1,rsa2048"; rsa,r-squared = <0x10cb82b5 0xc0d123dd 0x369b77e4 0xfc5fe7b4 0x34e1b663 0xac7ccbd1 0x2089fcb8 0xd473ccdb 0x79e857 af 0x67c88056 0xb9cfc0bd 0x2f854d0c 0xc9908774 0x4690496a 0x24140c3f 0xb15f60cf 0x661576bb 0x788700b7 0x31e31f54 0xc929c98b 0xe75 472b5 0x1c83c812 0xbdc68033 0xb96ebbaf 0xc2c1160c 0xfd737350 0xbfbdafba 0xe4a8c600 0x5c7dcf9b 0x02090083 0x428b42f4 0xafe211c1 0x 1924ce2f 0xdcb959f3 0xce5edeb4 0x38864bfe 0xc1100fef 0x5e697704 0x89560798 0x15f4dd46 0xc72bf173 0xbf83f373 0x47960ba8 0x6e49beda 0x04fc6263 0x91fd2ac4 0x28975bbd 0x5e7cc6a9 0x857cb2db 0x3af856a7 0xc14bbde4 0x64710fbd 0xbc69c38a 0x03b65791 0x8aeac041 0x1e50b 0c5 0x9fb0c4f4 0x1fbe97e3 0xdaa7dcf7 0x80817ad7 0x5195514c 0x2a19ad58 0x66eb2870 0x05f78731>; rsa,modulus = <0xc95ff26c 0xad27a889 0x6bb89478 0x0e38cda9 0x29085777 0x1e896717 0x1e95d2ed 0x4cd49f17 0x63f49947 0x5d98b3d5 0xd5833340 0x18fbe768 0x45ae09ed 0x2f86b951 0xc41c067f 0xd85f8d1b 0x76e427a3 0xe25a9f1d 0x754a5242 0x4f26037a 0x5ecd2 3e2 0x4c9c629c 0x4f4db6b9 0x1c21bf95 0xa276f126 0x469f4989 0x35a7c3d8 0x7c89c0c4 0x26675817 0x352c1f61 0xe3a3e594 0xce04653a 0xa9 d1ece4 0xba3ed4ac 0x9b35d81f 0xb1a3371d 0xc473ca80 0x8ce9bc54 0x81109b91 0x320f0244 0x7144346c 0xebd97bed 0x57757d18 0x0a0dc99e 0 x1fbc8e4d 0x20f15eec 0x2e80cc41 0x0748270d 0x054e1f67 0xe4f21cdf 0x1a6e56fd 0x857d51f6 0x219c6494 0x7c8e8a36 0xf3ecda1c 0xad866a3 0 0xd1142431 0x3afab3a9 0x723891cc 0x9774b093 0x05051de2 0x6a7e4167 0xd3e2c5e5 0x27fcc949>; rsa,exponent = <0x00000000 0x00010001>; rsa,n0-inverse = <0xb3698707>; rsa,num-bits = <0x00000800>; key-name-hint = "fit"; }; };
Verify you can boot your signed image:
setenv bootargs bypass; tftpboot 0x40200000 venice/fit.itb && bootm 0x40200000
- Note: we are loading into 0x40200000 instead of $loadaddr because loadaddr is the same as kerneladdr and our fit image is loaded there and they conflict.
- Note U-boot will not verify signature if it can not find the signature node but will continue to load the image if CONFIG_LEGACY_IMAGE_FORMAT is enabled; in order to secure U-Boot you want to disable that.
At this point, you will need to decide where to store the signed FIT image on flash. You can either hard-code it to an offset outside the partitioned space (e.g., between the partition table and the first partition), or place it in an unencrypted partition that U-Boot can load from.
For the emmc on venice boards, the partition table starts at 0x0 and we like to leave 1MiB of space for ample room for the partition table. The custom init script we are using to provision the eMMC that creates the partition table and the partitions is configured to start P1's data at 64MiB offset so we can essentially use the eMMC user hardware partition from 1MB to 64MB allowing for 63MB (64-1) of loading space for the fit image (if this is not large enough for your FIT image, just modify the start of the P1 data offset in the provisioning step).
U-Boot's mmc commands use 512byte blocks in hex notation so we figure out what 63MB is for a blk count:
printf "0x%x\n" $((63*1024*1024/512)) # hex 512byte blocks 0x1f800
Now to tftp transfer and flash the fit image to memory
u=boot-> tftpboot $loadaddr venice/fit.itb && setexpr blkcnt $filesize + 0x1ff && setexpr blkcnt $blkcnt / 0x200 && mmc dev 2 0 && mmc write $loadaddr 0x800 $blkcnt
- note we use setexpr to round up to the next block size to make sure we get all the data
You can now boot with this:
u=boot-> mmc dev 2 0 && mmc read 0x40200000 0x800 0x1f800 && bootm 0x40200000
Now that your secure ramdisk is written to memory: you are at the point of wanting to lock down your u-boot environment
Option 2 - Embed the kernel/dtb/ramdisk in the U-Boot ITB
For IMX you can utilize the fact that the SPL is using HABv4 and the SRK keys to verify the entire U-Boot FIT image (u-boot.itb) which already contains the ARM Trusted Firmware, U-Boot propper, and the U-Boot control DTB's. This approach involves integrating the ramdisk, kernel, and device tree into U-Boot's FIT image (u-boot.itb) and taking advantage of the High Assurance Boot (HABv4) feature to authenticate the entire firmware image before execution. The u-boot.itb is the u-boot FIT image which is built by binman at the end of the U-Boot make target and described in the binman/itb/fit node. This option provides robust security by leveraging hardware-based security features and extending the trusted boot chain.
The benefits of this method:
- no additional key needed
The downsides of this method:
- bloats your boot firmware to where you need to put it on the eMMC user hardware partition which is an extra complication
- requires you to modify where you store U-Boot env on the eMMC (or disable it)
Procedure:
- Checkout U-Boot (or use your existing U-Boot that you have been working with):
git clone https://github.com/Gateworks/uboot-venice.git cd uboot-venice # copy necessary artifacts from bsp cp $VENICE_BSP/u-boot/lpddr4*.bin . # DDR firmware cp $VENICE_BSP/atf/build/imx8mm/release/bl31.bin . # ATF
- Make sure your environment is configured for cross-compiling:
(cd $VENICE_BSP; . ./setup-environment) make imx8mm_venice_defconfig
- Use 'make menuconfig' to make the following changes:
- CONFIG_LEGACY_IMAGE_FORMAT=y to allow packaging the ramdisk with a header.
- CONFIG_ENV_IS_IN_MMC=n to prevent U-Boot environment storage conflicts.
make menuconfig # enable LEGACY_IMAGE_FORMAT, enable CONFIG_ENV_IS_NOWHERE disable CONFIG_ENV_IS_IN_MMC make savedefconfig && cp defconfig configs/imx8mm_venice_defconfig
- Its a good idea for troubleshooting to enable DEBUG in common/spl/spl_fit.c by adding '#define DEBUG' to the top of that file
- Create a uramdisk image via mkimage
U-Boot needs a special format for its ramdisk image to ensure proper booting and functionality of the system, and the mkimage command is used to create such images according to U-Boot's requirements.
mkimage -A arm64 -T ramdisk -C none -d blobs/rootfs.cpio.xz blobs/uramdisk
- Modify the U-Boot device tree source per SoC (ie arch/arm/dts/imx8mm-u-boot.dtsi) and:
- add the following in the itb/fit/images node:
os_kernel { arch = "arm64"; compression = "none"; description = "Kernel image"; load = <0x48200000>; type = "kernel"; uboot-blob { filename = "blobs/Image"; type = "blob-ext"; }; }; os_fdt { arch = "arm64"; compression = "none"; description = "flat_dt"; load = <0x50200000>; type = "firmware"; uboot-blob { filename = "blobs/imx8mm-venice-gw72xx-0x.dtb"; type = "blob-ext"; }; }; os_ramdisk { arch = "arm64"; compression = "none"; description = "ramdisk"; load = <0x50300000>; type = "firmware"; uboot-blob { filename = "blobs/uramdisk"; type = "blob-ext"; }; };
- update the loadables to include the 'os_kernel', 'os_fdt', 'os_ramdisk' you added
- add the following in the itb/fit/images node:
- U-Boot needs a special format for its ramdisk image to ensure proper booting and functionality of the system, and the mkimage command is used to create such images according to U-Boot's requirements.
mkimage -A arm64 -T ramdisk -C none -d ../blobs/rootfs.cpio.xz ../blobs/uramdisk
When complete your differences should look like this:
$ git diff diff --git a/arch/arm/dts/imx8mm-u-boot.dtsi b/arch/arm/dts/imx8mm-u-boot.dtsi index 04a7093a6694..0563bfa23ed2 100644 --- a/arch/arm/dts/imx8mm-u-boot.dtsi +++ b/arch/arm/dts/imx8mm-u-boot.dtsi @@ -155,6 +155,45 @@ type = "firmware"; }; + os_kernel { + arch = "arm64"; + compression = "none"; + description = "Kernel image"; + load = <0x48200000>; + type = "kernel"; + + uboot-blob { + filename = "../blobs/Image"; + type = "blob-ext"; + }; + }; + + os_fdt { + arch = "arm64"; + compression = "none"; + description = "flat_dt"; + load = <0x50200000>; + type = "firmware"; + + uboot-blob { + filename = "../blobs/imx8mm-venice-gw73xx-0x.dtb"; + type = "blob-ext"; + }; + }; + + os_ramdisk { + arch = "arm64"; + compression = "none"; + description = "ramdisk"; + load = <0x50300000>; + type = "firmware"; + + uboot-blob { + filename = "../blobs/uramdisk"; + type = "blob-ext"; + }; + }; + @fdt-SEQ { compression = "none"; description = "NAME"; @@ -176,9 +215,9 @@ firmware = "uboot"; #ifndef CONFIG_ARMV8_PSCI #ifdef CONFIG_OPTEE_LOAD_ADDRESS - loadables = "atf", "tee"; + loadables = "atf", "tee", "os_kernel", "os_fdt", "os_ramdisk"; #else - loadables = "atf"; + loadables = "atf", "os_kernel", "os_fdt", "os_ramdisk"; #endif #endif }; diff --git a/common/spl/spl_fit.c b/common/spl/spl_fit.c index 730639f7562c..deb77356c940 100644 --- a/common/spl/spl_fit.c +++ b/common/spl/spl_fit.c @@ -3,7 +3,7 @@ * Copyright (C) 2016 Google, Inc * Written by Simon Glass <sjg@chromium.org> */ - +#define DEBUG #include <common.h> #include <errno.h> #include <fpga.h> diff --git a/configs/imx8mm_venice_defconfig b/configs/imx8mm_venice_defconfig index dc391e47dac1..241ecc99992f 100644 --- a/configs/imx8mm_venice_defconfig +++ b/configs/imx8mm_venice_defconfig @@ -6,7 +6,6 @@ CONFIG_SPL_GPIO=y CONFIG_SPL_LIBCOMMON_SUPPORT=y CONFIG_SPL_LIBGENERIC_SUPPORT=y CONFIG_ENV_SIZE=0x8000 -CONFIG_ENV_OFFSET=0x3f0000 CONFIG_DM_GPIO=y CONFIG_DEFAULT_DEVICE_TREE="imx8mm-venice" CONFIG_SPL_TEXT_BASE=0x7E1000 @@ -17,7 +16,6 @@ CONFIG_SPL_SERIAL=y CONFIG_SPL_DRIVERS_MISC=y CONFIG_SPL_STACK=0x920000 CONFIG_SPL=y -CONFIG_ENV_OFFSET_REDUND=0x3f8000 CONFIG_SYS_LOAD_ADDR=0x48200000 CONFIG_SYS_MEMTEST_START=0x40000000 CONFIG_SYS_MEMTEST_END=0x80000000 @@ -77,9 +75,7 @@ CONFIG_CMD_EXT4_WRITE=y CONFIG_OF_CONTROL=y CONFIG_SPL_OF_CONTROL=y CONFIG_OF_LIST="imx8mm-venice imx8mm-venice-gw71xx-0x imx8mm-venice-gw72xx-0x imx8mm-venice-gw73xx-0x imx8mm-venice-gw7901 imx8mm-ve nice-gw7902 imx8mm-venice-gw7903 imx8mm-venice-gw7904 imx8mm-venice-gw75xx-0x" -CONFIG_ENV_IS_IN_MMC=y CONFIG_SYS_REDUNDAND_ENVIRONMENT=y -CONFIG_SYS_MMC_ENV_DEV=2 CONFIG_ENV_VARS_UBOOT_RUNTIME_CONFIG=y CONFIG_USE_ETHPRIME=y CONFIG_ETHPRIME="eth0"
Now build it and update your board (by creating a JTAG image or using the 'update_firmware' script on the resulting flash.bin)
Important: Avoid directly copying your device tree blob (DTB) to U-Boot's directory. Doing so may unexpectedly corrupt your flash.bin file. Instead, adopt the approach we use here by placing the DTB in a subdirectory, like 'blobs'.
To flash this onto the eMMC we need to change the eMMC to boot to the user hardware partition and place it there as it no longer will fit in the 4MB boot0 hardware partition:
- for imx8mm:
u-boot=> tftpboot $loadaddr venice/flash.bin && setexpr blkcnt $filesize + 0x1ff && setexpr blkcnt $blkcnt / 0x200 && mmc dev 2 0 && mmc write $loadaddr 0x42 $blkcnt && mmc partconf 2 1 7 0
- for imx8mn/imx8mp (different bootblk)
u-boot=> tftpboot $loadaddr venice/flash.bin && setexpr blkcnt $filesize + 0x1ff && setexpr blkcnt $blkcnt / 0x200 && mmc dev 2 0 && mmc write $loadaddr 0x40 $blkcnt && mmc partconf 2 1 7 0
Now when booting the SPL loads the kernel, ramdisk, fdt to the addresses specified in the binman nodes above and you can boot it with booti:
u-boot=> booti 0x48200000 0x50300000 0x50200000
Now that your secure ramdisk is written to memory: you are at the point of wanting to lock down your u-boot environment
Locking down U-Boot environment and shell
There isn't much point in securing U-Boot if you leave the U-Boot shell enabled. Additionally if you leave the U-Boot env stored in flash that introduces another attack vector so you might as well disable env storage completely and hard-code the bootcmd:
- enable CONFIG_ENV_IS_NOWHERE=y to disable env load/save
- disable CONFIG_ENV_IS_IN_MMC
- change CONFIG_BOOTCOMMAND to what you need to directly boot (depending on the options above)
HABv4 encrypted boot architecture
The IMX HABv4 also provides an extra optional security operation by using cryptography (AES-CCM) to obscure the boot image so it can not be seen or used by unauthorized users.
Encrypted boot adds an extra layer of security to the boot sequence using cryptographic techniques to obscure the bootloader data (which can be extended to the entire firmware image)so that it can not be seen or used by unauthorized users. This mechanism protects and conceals the bootloader code residing in flash.
The Data Encryption Key (DEK) is an AES key used to encrypt the boot image (via the Code Signing Tool) and decrypt the boot image (using the DEK blob appended to the image). The DEK blob is used as a security layer to wrap and store the DEK off-chip which is unique to the chip that generated the blob.
Generation of the DEK blob that gets appended to your image must be done on the IMX via the U-Boot dek_blob command which is enabled with CONFIG_CMD_DEKBLOB=y.
References:
- https://elixir.bootlin.com/u-boot/latest/source/doc/imx/habv4/introduction_habv4.txt
- https://elixir.bootlin.com/u-boot/v2021.07/source/doc/imx/habv4/guides/encrypted_boot.txt
- NXP AN12056 - Encrypted Boot on HABv4 and CAAM Enabled devices