Version 37 (modified by 5 months ago) ( diff ) | ,
---|
-
Venice Secure Boot
- General Notes
- Chain of Trust
- i.MX8M High Assurance Boot (HAB)
- i.MX secure boot
- HABv4 encrypted boot architecture
- Trusted Execution Environment (TEE)
- Full Disk Encryption (FDE)
- Locking down U-Boot environment and shell
- TPMv2.0
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:
- U-Boot Versions: It's crucial to note that this guide was written for U-Boot version v2023.04-venice (or a compatible version), as different U-Boot branches may necessitate distinct configurations.
- Board Versions: Many of the steps here must be customized for your particular board, specifically the device-tree of the board. You will see many instructions here assuming 'imx8mm-venice-gw73xx-0x.dtb' - adjust as necessary.
- 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.
- Memory Addresses: There are many memory addresses used here which correspond to where various objects get loaded into memory. See venice/bootloader/ for some recomendations
- 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. Also note that the different SoC's use different FLASH layouts:
soc | boot device | SPL offset | defconfig | binman |
---|---|---|---|---|
imx8mm | emmc user | 33K | imx8mm_venice_defconfig | imx8mm-u-boot.dtsi |
emmc boot0/boot1 | 33K | |||
imx8mn | emmc user | 32K | imx8mn_venice_defconfig | imx8mn-u-boot.dtsi |
emmc boot0/boot1 | 0K | |||
imx8mp | emmc user | 32K | imx8mp_venice_defconfig | imx8mp-u-boot.dtsi |
emmc boot0/boot1 | 0K |
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.
The Super Root Key (SRK) is an RSA key pair which forms the start of the boot-time authentication chain. The hash of the SRK public key is embedded in the processor using OTP hardware. The SRK private key is held by the CA. The SRK in this document (unless noted) refers to the public key only.
The authentication begins with establishing a root of trust with the SRK. HAB does this by computing a cryptographic hash of the SRK table and comparing the result with a pre-computed hash that is provisioned in OTP fuses.
The SRK_HASH fuses for the IMX6/IMX7/IMX8 are defined by a bank and a word:
- SRK_HASH[31:00] bank 6 word 0
- SRK_HASH[63:32] bank 6 word 1
- SRK_HASH[95:64] bank 6 word 2
- SRK_HASH[127:96] bank 6 word 3
- SRK_HASH[159:128] bank 7 word 0
- SRK_HASH]191:160] bank 7 word 1
- SRK_HASH[223:192] bank 7 word 2
- SRK_HASH[255:224] bank 7 word 3
See also:
- U-Boot mx8m_spl_secure_boot.txt
- U-Boot introduction_habv4.txt
- AN12056 Encrypted Boot on HABv4 and CAAM Enabled Devices
- AN4581 Secure Boot on i.MX50, i.MX53, and i.MX 6 Series using HABv4
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) which you can install via:
- apt package via 'sudo apt install imx-code-signing-tool'
- download from NXP at 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.4.0.tgz cd cst-3.4.0/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 uboot-venice # 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 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,22d21 < CONFIG_IMX_HAB=y < # CONFIG_CMD_DEKBLOB is not set
- note that the above is for imx8mm (you need to copy the ATF from the imx8mp directory and use imx8mp_venice_defconfig if you are using imx8mp for example)
- create a signed_flash.bin
# setup env to point to the CST export CST_DIR=/usr/src/nxp/cst-3.4.0 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) ... Hit any key to stop autoboot: 0 u-boot=> hab_status Secure boot disabled HAB Configuration: 0xf0, HAB State: 0x66 No HAB Events Found!
- 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
- via U-Boot:
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
- via Linux:
# read OTP fuse # $1 bank # $2 word # OTP is arranged as banks of 32bit words so we access with bs=4 and skip to the word we want OCOTP_DEV=/sys/devices/platform/soc@0/30000000.bus/30350000.efuse/imx-ocotp0/nvmem fuse_read() { local bank=$1 local word=$2 local offset=$(($((bank*4))+((word%4)))) # and can use hexdump to display as a 32bit int dd if=$OCOTP_DEV bs=4 count=1 skip=$offset 2>/dev/null | hexdump -v -e '1/4 "0x%08x"' } # blow OTP fuse # $1 bank # $2 word # $3 value fuse_blow() { local bank=$1 local word=$2 local val=$3 local offset=$(($((bank*4))+((word%4)))) echo "fuse_blow: bank$bank word$word offset=$offset val=$val" # output binary 32bit int local b0=$(( $((val>>24)) & 0xff)) local b1=$(( $((val>>16)) & 0xff)) local b2=$(( $((val>>8)) & 0xff)) local b3=$(( $((val>>0)) & 0xff)) printf "\\x$(printf "%x" $b3)\\x$(printf "%x" $b2)\\x$(printf "%x" $b1)\\x$(printf "%x" $b0)" | dd of=$OCOTP_DEV bs=4 seek=$offset 2>/dev/null } echo "Current SRK_FUSES:" echo $(fuse_read 6 0) echo $(fuse_read 6 1) echo $(fuse_read 6 2) echo $(fuse_read 6 3) echo $(fuse_read 7 0) echo $(fuse_read 7 1) echo $(fuse_read 7 2) echo $(fuse_read 7 3) echo $(fuse_read 1 3) echo "Blowing fuses from $SRK_HASH..." bank=6 word=0 for i in $(hexdump -e '/4 "0x"' -e '/4 "%X""\n"' $SRK_HASH); do fuse_blow $bank $word $i word=$((word+1)) [ $word -eq 4 ] && { word=0 bank=$((bank+1)) } done
- 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
- via U-Boot:
u-boot=> fuse prog -y 1 3 0x2000000
- via Linux (see above):
fuse blow 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
- via U-Boot:
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
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
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=0x44000000
- 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=0x7e000000
- 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=0xbe000000
- 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 (This must be rebuilt every time you 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 (This must be rebuilt every time you build OP-TEE):
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=0x44000000 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.4.0 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
We will be using many files here in the process which we will store in a 'blobs' directory we will refer to with an env variable called BLOBS:
export BLOBS=$PWD/blobs mkdir $BLOBS
- make sure this is a full path as above to avoid potential problems where we use it
Kernel and DTB
We are going to need a kernel Image, DTB for your board, and kernel modules (the modules are only needed if you are using something requiring modules like the TPM). For this demonstration we will use the the pre-built Gateworks Linux tarball and we will untar the contents of the boot dir (where the kernel Image and DTB's are) and the modules dir where the kernel modules are. We will place these in the $BLOBS directory for use later:
# grab the prebuilt gateworks kernel wget https://dev.gateworks.com/venice/kernel/linux-venice-6.6.8.tar.xz tar -C $BLOBS -xvf linux-venice-6.6.8.tar.xz ./boot ./lib/modules # gzip the kernel gzip -fk $BLOBS/boot/Image # creates 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=128
In this example, we're using a 128-byte key to ensure compatibility with both TPM-enabled and non-TPM systems. According to the TPM documentation, the 'tpm2_create' command allows for the creation of a sealing object with a maximum size of 128 bytes, enabling the sealing of user data to the TPM. However, it's worth noting that the 'cryptsetup luksFormat' command supports keyfiles of up to 8192 KiB. If you're operating without a TPM, opting for a longer key generally enhances security.
The init script we will use can be created like this:
- You need to set SERVERIP variable to your tftp IP address
cat <<\EOF> init #!/bin/sh set -x # script debugging; show commands set -e # exit on error # 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 DEVICE=mmcblk2 # storage device to be used for the encrypted file system DEV=/dev/$DEVICE 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 SERVERIP=172.24.21.238 # Boot firmware support BOOT_FIRMWARE=http://$SERVERIP/tftpboot/venice/signed-flash.bin FIT_IMAGE=http://$SERVERIP/tftpboot/venice/fit.itb # TPM support KEY_HANDLE=0x81234567 #TPM2 Owner Persistent Handle Range: 0x81000000 - 0x81800000 # 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 # Check if "tpm" is present in bootargs for arg in $(cat /proc/cmdline); do [ "$arg" == "tpm" ] && { insmod /virt-dma.ko insmod /imx-sdma.ko insmod /spi-imx.ko insmod /tpm_tis_spi.ko [ -c /dev/tpm0 ] && { TPM=/dev/tpm0 echo "Using TPM: $TPM" } } 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/e th*", 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/e th*", 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 # Mount the LUKS device to /mnt mount /dev/mapper/$NAME /mnt for url in $ROOTFS_TARS; do echo "Downloading $url..." wget -q --show-progress --no-check-certificate -O data $url echo "Extracting..." pv data | tar -C /mnt -mxJ --keep-directory-symlink done user_data # Unmount the LUKS device echo "Unmounting LUKS device..." umount /dev/mapper/$NAME } # 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 # 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 user_data umount /dev/mapper/$NAME } # example3: rootfs via existing filesystem rootfs3() { FILESYSTEM=https://dev.gateworks.com/buildroot/venice/minimal/rootfs.ext2.xz #FILESYSTEM=https://$SERVERIP/tftpboot/venice/rootfs.ext2.xz echo "Downloading $FILESYSTEM..." wget -q --show-progress --no-check-certificate -O data $FILESYSTEM # 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 user_data umount /dev/mapper/$NAME } # provision the flash with secure boot firmware and encrypted rootfs provision() { rootfs=${1:-rootfs1} echo "Provision $DEV via $rootfs..." # board specific customizations case "$(cat /proc/device-tree/board)" in GW74*) NIC=eth1;; esac # soc specific customizations case "$(cat /proc/device-tree/soc@0/compatible | tr \0 \n)" in *imx8mm*) boot0_offset_kb=33 user_offset_kb=33 ;; *imx8mn*|*imx8mp*) boot0_offset_kb=0 user_offset_kb=32 ;; 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 [ "$BOOT_FIRMWARE" ] && { echo "Writing locked down U-Boot to memory..." wget -q --show-progress --no-check-certificate -O data $BOOT_FIRMWARE # disable boot0 readonly echo 0 > /sys/class/block/${DEVICE}boot0/force_ro # write boot firmware to proper offset dd if=data of=${DEV}boot0 bs=1K seek=$boot0_offset_kb oflag=sync } [ "$FIT_IMAGE" ] && { echo "Flashing FIT Image to memory..." wget -q --show-progress --no-check-certificate -O data $FIT_IMAGE # sanity check its not too big for P1_OFFSETMB # - we don't want to automatically choose P1_OFFSETMB as you may want to support # firmware updates and you should allow it to grow and thus choose your slack space FIT_SIZEMB=$(($(stat -c '%s' data) / 1024 / 1024)) [ $FIT_SIZEMB -lt $(($P1_OFFSETMB + 1)) ] || { echo "Error: $FIT_IMAGE is too large; adjust P1_OFFSETMB"; exit 1; } # Write fit Image to memory dd if=data of=$DEV bs=1M seek=1 oflag=sync } 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 # force re-read of partition table now partprobe $DEV [ "$TPM" ] && { # Get inital PCR measurment values echo "Retrieving initial PCR measurement values..." # PCR0: boot-firmware dd if=${DEV}boot0 of=data bs=1M count=4 tpm2 pcrextend 0:sha1=$(sha1sum data | cut -d" " -f1) # PCR8: partition table and FIT image (kernel, dtb, ramdisk) dd if=$DEV of=data bs=1M count=$P1_OFFSETMB tpm2 pcrextend 8:sha1=$(sha1sum data | cut -d" " -f1) # show PCR's tpm2 pcrread echo "Creating Key from TPM2.0..." hexdump -C $KEY # if debugging # delete any existing key tpm2 evictcontrol -C o -c $KEY_HANDLE # create a policy that depends on PCR0 and PCR8 tpm2 createpolicy --policy-pcr -l sha1:0,8 -L policy.digest tpm2 createprimary -g sha256 -G rsa -c primary.context # create an object containing the key tpm2 create -g sha256 -u obj.pub -r obj.priv -C primary.context -L policy.digest -a "noda|adminwithpolicy|fixedparent |fixedtpm" -i $KEY # load it tpm2 load -C primary.context -u obj.pub -r obj.priv -c load.context # save it to TPM handle tpm2 evictcontrol -C o -c load.context $KEY_HANDLE rm -f policy.digest primary.context obj.pub obj.priv load.context } # Format the partition as a LUKS encrypted file system using the encryption key echo "Formating LUKS device..." cryptsetup --batch-mode luksFormat ${DEV}${PART} --key-file=$KEY # Open the LUKS encrypted partition and map it to the specified name ($NAME) echo "Opening LUKS device..." cryptsetup luksOpen ${DEV}${PART} $NAME --key-file=$KEY # call rootfs option $rootfs # Close the LUKS device and remove the mapping echo "Closing LUKS device..." cryptsetup luksClose $NAME echo "Provisioning complete" sleep 2 # Wait forever #while [ 1 ]; do sleep 1; done # power cycle echo 2 > /sys/bus/i2c/devices/0-0020/powerdown } # boot to encrypted fs boot() { 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) [ "$TPM" ] && { echo "Fetching Key from TPM..." KEY=fs.keyphrase_decrypted # PCR0: boot-firmware dd if=${DEV}boot0 of=data bs=1M count=4 tpm2 pcrextend 0:sha1=$(sha1sum data | cut -d" " -f1) # PCR8: partition table and FIT image (kernel, dtb, ramdisk) dd if=$DEV of=data bs=1M count=$P1_OFFSETMB tpm2 pcrextend 8:sha1=$(sha1sum data | cut -d" " -f1) # show PCR's tpm2 pcrread # unseal our key tpm2 unseal -c $KEY_HANDLE -p pcr:sha1:0,8 -o $KEY hexdump -C $KEY # if debugging } DEV=${DEV}${PART} echo "Opening $DEV..." cryptsetup luksOpen $DEV $NAME --key-file=$KEY [ "$TPM" ] && { # reseal the key by extending the PCR's rm -f $KEY tpm2 pcrextend 0:sha1=0000000000000000000000000000000000000000 tpm2 pcrextend 8:sha1=0000000000000000000000000000000000000000 } # Mount the LUKS device to /mnt echo "Mounting $DEV..." mount /dev/mapper/$NAME /mnt # 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" } # boot straight to ramdisk (no disk encryption) 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 "$@" } for x in $(cat /proc/cmdline); do case "$x" in bypass) bypass;; provision=*) provision ${x//provision=};; esac done boot EOF
- Make sure you set BOOT_FIRMWARE and FIT_IMAGE URL's at the beginning of the script to where your board can get them on your network
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 and modifying
To download the prebuilt root file system and incorporate the needed extra files manually, follow these steps:
Note: For Gateworks's standard BSP 6.6 Kernel to work seamlessly with the initialization scripts and enable TPM functionality, it's imperative to include the following kernel modules:
- spi-imx.ko
- tpm_tis_spi.ko
- virt-dma.ko
- imx-sdma.ko
We will get these modules from the pre-built kernel that you downloaded and extracted to the 'blobs' directory. If you are using a different kernel set the KERNEL directory differently - just understand that the kernel Image version/signature used in the FIT image must match the kernel modules placed in the ramdisk.
Additionally, ensure that the firmware file sdma-imx7d.bin is located in the directory lib/firmware/imx/sdma/. These modules are crucial for proper TPM integration and operation within our secure setup.
# download prebuilt 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 the filesystem key and custom init script cp fs.key init initrd/ # copy any required kernel modules matching the kernel you are using (ie modules needed for TPM usage) from the $BLOBS dir we discussed earlier: for i in virt-dma imx-sdma spi-imx tpm_tis_spi; do file=$(find $BLOBS -name $i.ko); [ -r "$file" ] && cp $file initrd && echo $file; done # copy any required firmware files needed (ie SDMA used for SPI) mkdir -p initrd/lib/firmware/imx/sdma/ wget https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/plain/imx/sdma/sdma-imx7d.bin -O initrd/lib/firmware/imx/sdma/sdma-imx7d.bin # re-create initrd (cd initrd; find | fakeroot -i ../initrd.fakeroot cpio -o -H newc | xz --check=crc32 -c > ../rootfs.cpio.xz) # copy it to your blobs directory for use when creating the FIT image 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
# beging with the example venice defconfig make venice_example_defconfig make # copy the cpio image to your blobs directory for use when creating the FIT image 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/boot/imx8mm-venice-gw73xx-0x.dtb $BLOBS/boot/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. This is also appears to be the standard when implementing a secure boot process. We strongly recommend using this option.
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
Important notes for external FIT images:
- Signature Node: The process of signing an external 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.
- 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.
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 create fit.key (or copy one here) openssl genpkey -algorithm RSA -out fit.key -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537 # create a certificate fit.crt from fit.key 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/boot/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/boot/imx8mm-venice-gw73xx-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 to do two things:
- create a signed FIT image (fit.itb)
- store the signature in a 'signature.dtb' file (the '-K <dtb>' parameter) which we can decompile to obtain the signature node we need to put in our secure boot loader
Procedure:
# create a signature.dtb for 'mkimage -K <dtb>' to modify printf "/dts-v1/;\n/ {\n};\n" > signature.dts && dtc -I dts -O dtb signature.dts > signature.dtb # create the fit image and add the signature to the dtb mkimage -f fit.its -k . -K signature.dtb -r fit.itb # now we have a signed fit.itb you can put on your board or tftp cp fit.itb /tftpboot/venice # decompile the signature.dtb to signature.dts dtc -I dtb -O dts signature.dtb > signature.dts # then strip the first five lines and the last line out to just obtain the signature node cat signature.dts | tail -n +5 | head -n -1 > $BLOBS/signature.dts
- Note: the -k parameter provides the directory to look for the key file 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.dts in the previous step by using ‘mkimage -K’ to ‘inject’ a node into our signature.dtb then decompiling it to a signature.dts.
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: (Note that we require some files from the Venice BSP with an env directory VENICE_BSP pointing to it)
- 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
- Note that if you needed to modify the ATF in some way, for example adding OPTEE, copy it from there instead
- Make sure your environment is configured for cross-compiling:
pushd .; cd $VENICE_BSP; . ./setup-environment; popd # do not do this in a subshell or the env vars will not be retained 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.
- CONFIG_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:
$ diff configs/imx8mm_venice_defconfig configs/imx8mm_venice_signed_fit_defconfig diff --git a/arch/arm/dts/imx8mm-venice-u-boot.dtsi b/arch/arm/dts/imx8mm-venice-u-boot.dtsi index 6f786b9467..1dd642284f 100644 --- a/arch/arm/dts/imx8mm-venice-u-boot.dtsi +++ b/arch/arm/dts/imx8mm-venice-u-boot.dtsi @@ -6,6 +6,20 @@ #include "imx8mm-u-boot.dtsi" / { + signature { + + key-fit { + required = "conf"; + algo = "sha1,rsa2048"; + rsa,r-squared = <0x932f55f8 0xac6cc41d 0xcf065c47 0x4d71b5fb 0x50f4ea66 0x9ed1dbf2 0xe1778994 0x52028220 0x4a472f2 0x83378884 0xf55d497c 0x13f87ec5 0x6fc3c00 0x975f757 0x627cdc82 0xd8fd168e 0xc751f39b 0x912a51ad 0x2363156a 0x40a949fd 0x889ffcb8 0xeb19c201 0xfabebd4c 0x1e2f4e8e 0xefd589a5 0x7d86d40b 0x803dad0c 0x1dc81f72 0xd8e796db 0xf5a92d2b 0x3a72c1c 0x887c466 0x72bb9f5a 0xa52240c9 0x11f7668e 0xa2c3f421 0x4a587e34 0x98094d7a 0x87c1ba58 0xae9590b6 0x86a5a319 0x50fec828 0x952ae60e 0x65ece7ef 0x4f73ea76 0x8ebd25c1 0x4ad18681 0x185c6ae 0x68eed273 0x10953c67 0x9a9b4863 0x65d93360 0xfb8d5204 0xf1f0b9fe 0x6702d693 0x2993ba4f 0xc09caa24 0x5e49a12c 0x5054b937 0xccf85ea3 0x4d3282a1 0xb835aa12 0x8cf64429 0x2e7c6af1>; + rsa,modulus = <0x9cb7dcc9 0xcb925802 0xeb36ab79 0xae8012d9 0x33efe64f 0x863bc674 0xbaefabb5 0xa8396439 0x7dc00cbb 0x8c3f83b2 0x8e1792b 0x7d73c2e0 0xc272e34d 0x8c0739a0 0x196e7e17 0xbde3c1a8 0x7125e08b 0x7b3f9b22 0xc2fffdd8 0x43152100 0xb2a2031d 0x36fe08a0 0xc6fd2416 0x9bec5502 0x6b640c44 0x70bb1efb 0xf4fa1bfe 0x1bed6c17 0x66fdf8a8 0xd5c6676d 0x8088f45e 0xd1e3787f 0x53abaf0b 0xa61c6498 0x7f04c69 0x6e0c868b 0xcb66131f 0x3f9edf95 0xfb4f0111 0xd183ae2c 0x9522acf4 0xe7b282f9 0xeece7364 0xe2fa025c 0xffaa236b 0xbd9fe80b 0xb1803b74 0xc5e584f3 0x3f48d40b 0x60881e13 0x5f30454b 0xd3a4efde 0x1d794825 0x5ff4c37b 0xc8d78a2b 0x3063a4ef 0x655b1927 0xe1631379 0x60cdb1a2 0xbcfd3259 0xe286d61a 0xefe95dcd 0xe91de838 0x4ba6887f>; + rsa,exponent = <0x00 0x10001>; + rsa,n0-inverse = <0xf08ec881>; + rsa,num-bits = <0x800>; + key-name-hint = "fit"; + }; + }; + wdt-reboot { compatible = "wdt-reboot"; wdt = <&wdog1>; diff --git a/configs/imx8mm_venice_defconfig b/configs/imx8mm_venice_defconfig index dc391e47da..eb05389259 100644 --- a/configs/imx8mm_venice_defconfig +++ b/configs/imx8mm_venice_defconfig @@ -25,6 +25,7 @@ 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
- note you won't see CONFIG_RSA=y and CONFIG_LEGACY_IMAGE_FORMAT undefined in your diff because those options are now the default with the above change
Now build it and update the resulting flash.bin to your board (by creating a JTAG image or using the 'update_firmware' script on the resulting flash.bin):
make && cp flash.bin /tftpboot/venice/venice-imx8mm-flash.bin
When you boot your updated firmware 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
We Strongly recommend using option 1, we keep this option documented here in the off chance someone would want to implement this edge case for whatever reason 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/boot/Image"; type = "blob-ext"; }; }; os_fdt { arch = "arm64"; compression = "none"; description = "flat_dt"; load = <0x50200000>; type = "firmware"; uboot-blob { filename = "$BLOBS/boot/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"; }; };
- 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)
TPMv2.0
Now that we've implemented encrypted storage, providing a solid layer of security for data-at-rest protection, we can consider an optional step to further enhance our system's security. This step involves integrating TPM 2.0 technology, which offers features like measured boot and secure key storage. By opting for TPM 2.0 integration, we can extend the chain of trust and manage encryption keys with enhanced security. Next, we'll outline the steps to integrate TPM 2.0 into our secure setup for those who wish to implement this additional security measure. For this example we are building of the base blocks of "Option 1 - using an External FIT Image" above.
TPMv2.0 (Trusted Platform Module) is a hardware-based security solution designed to provide secure storage and processing of cryptographic keys, digital certificates, and other security-related data on computing platforms. It is an international standard developed by the Trusted Computing Group (TCG) and serves as an evolution of the original TPM specification (TPMv1.2). Platform Configuration Registers (PCRs) are specialized registers within the TPM that store cryptographic hashes, also known as digests or integrity measurements, of the software components running on the system. These digests are generated using secure hashing algorithms, such as SHA-256, and represent the measured state of the code running on your computer at various stages of the boot process.
The process of extending a PCR involves updating its current value by combining it with a new digest using a one-way operation. The integrity measurement is hashed together with the existing PCR value, and the result is stored back in the PCR. This process ensures that any change to the system's software state, including the order in which components are loaded, will alter the final PCR values, enabling detection of unauthorized modifications or tampering.
The TPM contains a small amount of Non-Volatile RAM (NV RAM) that can be used to store persistent data and configurations. The NV RAM is divided into several areas, each with its own access policies and permissions.
One of the main features of the TPM is the ability to perform a "measured boot" process. During boot, it is intended that the system firmware (BIOS/UEFI) measures and records the hash of each component involved in the boot process, such as the bootloader, kernel, drivers, and just about any other software blob running on your system that can be hashed. These measurements are stored in the TPM's PCRs.
By comparing the PCR values against known good values, the system can detect if any unauthorized modifications have been made to the boot components. This allows for a secure boot process and provides a chain of trust for the entire system.
In our example, the PCRs and NV RAM are leveraged together to implement measured boot and secure storage of data. Sensitive data can be "sealed" to specific PCR values, meaning it is encrypted by the TPM and can only be decrypted if the PCR values match the expected values (i.e., the system is in a known good state).
You can read more about the TPMv2.0 spec here: https://trustedcomputinggroup.org/wp-content/uploads/TCG_ServerManagementDomainFirmwareProfile_v1p00_11aug2020.pdf
Although we intend to adhere to the Trusted Computing Group's guidelines for utilizing TPMv2.0 as outlined in the specification, the support for TPMv2.0 in U-Boot is still somewhat limited due to its relatively newer adoption. Consequently, we will primarily rely on our IMX High Assurance Boot (IMX.HAB) feature, which is part of the on-chip BOOT ROM, to serve as our Code Root of Trust for Measurement (CRTM). Once the boot process reaches U-Boot, we will extend the PCRs with SHA-1 digests of our firmware components and of our FIT Image Blob.
In this example we are using PCR 0 to measure the entire boot firmware including the SPL, ATF, U-Boot DTB's and the u-boot environment. Additionally, we are dedicating PCR 8 specifically for measuring the contents of the fit.itb, which includes the device tree blob (.dtb), kernel Image, and ramdisk as those are part of the OS. Technically you can consider things like bootargs and ramdisk as OS configuration (PCR9) but we don't care to make that distinction here.
According to the TCG specification's Table 1 PCR Usage, PCR 0 is designated for the SRTM (Static Root of Trust for Measurement) and Boot Loader, which aligns with our decision to use it for measuring the entire boot firmware. PCR 8, on the other hand, is defined for use by the Management Domain OS, making it a suitable choice for measuring the contents of our fit.itb, which will be essential for the operating system's boot process.
This mechanism is used in the provided script to protect the encryption key for the root filesystem. The key is sealed to specific PCR values representing the boot firmware and FIT Image (kernel & device tree blob & initrd) measurements. During the boot process, the script unseals the key from the TPM using the current PCR values, allowing the encrypted root filesystem to be unlocked and mounted.
We'll securely store the key necessary for unlocking our encrypted storage device and filesystem within the TPM. Access to this key will be restricted to the ramdisk only if the PCRs have been extended with precise data from our firmware components and FIT Image Blob. Once this key is utilized to unlock the encrypted disk device, we'll update the PCRs again. Any subsequent attempts to access the key post-separation event will be unsuccessful since the PCRs will no longer align with the sealing policy.
In order to successfully integrate the TPM into our secure setup, these components and configurations are necessary:
- A Gateworks board that has a tpm (from the following list) with a regular Gateworks u-boot enviroment:
- GW71x1-E
- GW72x1-F+
- GW73x1-F+
- GW74x1-B+
- GW830x
- GW890x
- A TFTP server with the following files:
- The final FIT Image Tree Blob (fit.itb) you want to use for flashing
- The final flash.bin from U-Boot with appropriate tpm, lockdown, and boot command configurations you want to use for flashing
We are choosing the following PCR conventions:
- PCR0 - SRTM & Boot Loader:
- boot firmware (emmc boot0 hardware partition)
- PCR8 - Management Domain OS
- OS (partition table, FIT image) (not root filesystem as we assume that is read/write and encrypted in this example)
We are choosing to store our fs key in the TPM NVRAM (0x81000000 - 0x81800000) in a somewhat random location:
- 0x81234567
Using the TPM with the init script
If you want to utilize the discrete TPM on your Gateworks board, simply add the 'tpm' argument at the end of the bootargs in U-Boot.
#How to use the tpm with provisioning setenv bootargs provision=rootfs1 tpm #How to use the TPM for unlocking setenv bootargs tpm
Final u-boot flash.bin and FIT Image
This section serves as a summary step for this wiki article.
Similarly to the meticulous steps outlined throughout this wiki article, it is essential to confirm the inclusion of the final desired blobs in the FIT Image, while also configuring our end u-boot environment with the specific settings we desire:
Once you created a fit.itb comprised of these individual blobs by following the steps outlined in the "Generation of Signed FIT Image" section you just need to copy that FIT image to the tftp server you intend to use (same server ip that you define inside the init script).
Now we need to modify U-Boot to enable which security features you wish to leverage and change the boot command: Note: we are not including op-tee with this as it breaks the setup and since we are using a secure kernel we are not sure how much value op-tee really adds.
- i.MX HAB CONFIG OPTIONS
- CONFIG_IMX_HAB=y #'hab_auto_img', 'hab_status' and 'hab_version' commands
- CONFIG_SPL_LOAD_FIT_ADDRESS=0x44000000
- OP-TEE CONFIG OPTIONS
- CONFIG_IMX_HAB=y
- CONFIG_CMD_DEKBLOB=y
- CONFIG_SPL_LOAD_FIT_ADDRESS=0x44000000
- 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 OPTEEld ATF (bl31.bin) with OP-TEE support:
- 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
- SIGNED FIT IMAGE CONFIG OPTIONS
- CONFIG_FIT_SIGNATURE=y #enable signature verification of FIT uImages using a hash signed and verified using RSA.
- RSA=y # enable RSA library
- CONFIG_LEGACY_IMAGE_FORMAT=n #undefined - otherwise this allows booting unsigned images or any image if you lack a signature node
- Include signature Node in u-boot build (See "Getting FIT key into U-Boot proper and verifying signed images" section)
- LOCKED DOWN U-BOOT CONFIG OPTIONS
- CONFIG_BOOTDELAY=-1 #disables use of u-boot prompt
- CONFIG_ENV_IS_NOWHERE=y #disable env load/save
- CONFIG_ENV_IS_IN_MMC=n
- Choose BOOTCOMMAND CONFIG OPTION depending on if you want to use a TPM or not
- CONFIG_BOOTCOMMAND="setenv bootargs tpm && mmc dev 2 0 && mmc read 0x40200000 0x800 0x1f800 && bootm 0x40200000" # If you are using a TPM
- CONFIG_BOOTCOMMAND="setenv bootargs; mmc dev 2 0 && mmc read 0x40200000 0x800 0x1f800 && bootm 0x40200000" # If you are not using the TPM
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 changes above
make menuconfig # enable CONFIG options described above make savedefconfig && cp defconfig configs/imx8mm_venice_defconfig
- Make the flash.bin and sign it and tftp it over
make flash.bin # sign flash.bin; see "i.MX secure boot" section /bin/sh doc/imx/habv4/csf_examples/mx8m/csf.sh
- 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/configs/imx8mm_venice_defconfig b/configs/imx8mm_venice_defconfig index dc391e47da..556052a7ce 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,8 @@ CONFIG_SPL_SERIAL=y 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,11 +25,14 @@ 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 +CONFIG_BOOTDELAY=1 +CONFIG_BOOTCOMMAND="tpm2 init && tpm2 startup TPM2_SU_CLEAR && tpm2 self_test full && tpm2 self_test continue && mmc dev 2 1 && mmc read $loadaddr 0 0x2000 && hash sha1 $loadaddr 0x400000 *0x40200000 && md.b 0x40200000 0x20 && tpm2 pcr_extend 0 0x40200000 sha1 && tpm2 pcr_read 0 0x40200000 sha1 && mmc dev 2 0 && mmc read $loadaddr 0x0 0x20000 && hash sha1 $loadaddr 0x4000000 *0x40200000 && md.b 0x40200000 0x20 && tpm2 pcr_extend 8 0x40200000 sha1 && tpm2 pcr_read 8 0x40200000 sha1 && setenv bootargs tpm && mmc dev 2 0 && mmc read 0x40200000 0x800 0x1f800 && bootm 0x40200000" CONFIG_USE_PREBOOT=y CONFIG_PREBOOT="gsc wd-disable" CONFIG_BOARD_LATE_INIT=y @@ -77,9 +80,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-venice-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"
Provisioning
Now that we have all the necessary files in our tftp server, instantiating the provisioning of a FDE on a Gateworks board will look something like this:
u-boot=> dhcp && setenv serverip <YOUR_SERVER_IP> && setenv bootargs provision=rootfs1 && tftpboot 0x40200000 venice/fit.itb && bootm 0x40200000
If you opt to use the TPM for enhanced security it will look like this instead:
u-boot=> dhcp && setenv serverip <YOUR_SERVER_IP> && setenv bootargs provision=rootfs1 tpm && tftpboot 0x40200000 venice/fit.itb && bootm 0x40200000
Once the board completes provisioning its storage device, the secure system setup for your Venice board is complete.
Further TPM Reading/Sources
For more info on TPM see:
- https://trustedcomputinggroup.org/wp-content/uploads/TCG_ServerManagementDomainFirmwareProfile_v1p00_11aug2020.pdf
- https://community.infineon.com/t5/Blogs/Sealing-and-unsealing-data-in-TPM/ba-p/465547
- https://community.infineon.com/t5/Blogs/Storing-and-reporting-system-measurements-with-TPM/ba-p/443590
- https://trustedcomputinggroup.org/resource/tpm-library-specification/
- https://tpm2-tools.readthedocs.io/en/latest/