[[PageOutline]] = 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 v2024.03-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 [wiki:venice/bootloader/#memmap] 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 [http://trac.gateworks.com/wiki/secure_boot 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)  1. Creation of Code Signing Key: '''This is an example - read the CST documentation and tailor to your needs'''   a. Retrieve the NXP Code Signing Tool (CST): https://www.nxp.com/webapp/Download?colCode=IMX_CST_TOOL_NEW (Account required on NXP site) b. Unpack the CST : {{{#!bash tar xvf cst-3.4.0.tgz cd cst-3.4.0/keys }}}   c. Create a text file named "serial", which contains 8 digits. OpenSSL uses the contents of this file for the 'certificate serial numbers'. {{{#!bash echo "12345678" > serial }}}   d.  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: {{{#!bash PASS=mypassphrase printf "$PASS\n$PASS" > key_pass.txt }}}   e. 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) {{{#!bash ./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'  f. 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: {{{#!bash 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   g. Use hexdump to obtain the fuse table (8x 32bit fuse values) in the correct endianness for programming with u-boot 'fuse prog': {{{#!bash $ 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 2. Build U-boot with HABv4 enabled and a single DTB: * Pre-requisite: Build Venice BSP using this link here: [wiki:venice/bsp] * Export VENICE_BSP variable to your directory: {{{export VENICE_BSP=/path/to/your/bsp/directory}}} {{{#!bash # 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: {{{#!bash $ 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 }}}  3. create a signed_flash.bin {{{#!bash # 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 }}} 4. Program signed firmware image: {{{#!bash jtag_usbv4 -p signed_u-boot_spl-imx8mm.bin }}} * Booting this would look something like the following: {{{#!bash 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  5. 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** {{{#!bash 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 }}} 6. Boot it again and verify no HAB events: {{{#!bash u-boot=> hab_status Secure boot disabled HAB Configuration: 0xf0, HAB State: 0x66 No HAB Events Found! }}}  7. Close the device (lock it down!) - this step is irreversible, make sure there are no HAB events from the prior step {{{#!bash 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 == 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 [=#tee] == Trusted Execution Environment (TEE) The Trusted Execution Environment (TEE) is a set of specifications published by the [http://www.globalplatform.org 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 [=#optee] === 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 [https://optee.readthedocs.io/en/latest/architecture/globalplatform_api.html#tee-internal-core-api TEE Internal Core API v1.1.x] which is the API exposed to Trusted Applications and the [https://optee.readthedocs.io/en/latest/architecture/globalplatform_api.html#tee-client-api TEE Client API v1.0], which is the API describing how to communicate with a TEE. Those APIs are defined in the [https://optee.readthedocs.io/en/latest/architecture/globalplatform_api.html#globalplatform-api 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: 1. setup cross compile environment {{{#!bash # setup cross compile environment - here we will use the venice bsp toolchain/config cd venice/bsp . ./setup-environment }}} 1. setup additional SOC and Board specific environment: - for imx8mm: {{{#!bash export PLATFORM_FLAVOR=mx8mmevk # used for OPTEE build export PLAT=imx8mm # used for ATF build }}} - for imx8mp: {{{#!bash export PLATFORM_FLAVOR=mx8mpevk # used for OPTEE build export PLAT=imx8mp # used for ATF build }}} - for 1GiB DRAM boards: {{{#!bash 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: {{{#!bash 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: {{{#!bash export CFG_DDR_SIZE=0x100000000 export CFG_CORE_LARGE_PHYS_ADDR=y export CFG_CORE_ARM64_PA_BITS=36 export CFG_TZDRAM_START=0xfe000000 }}} 1. Create directories: We will create a 'secure-boot' directory containing u-boot, atf, and optee: {{{#!bash 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 }}} 1. Get IMX DDR training firmware: {{{#!bash 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 . }}} 1. Build OP-TEE (**This must be rebuilt every time you build OP-TEE): {{{#!bash 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 }}} 1. Build ATF (**This must be rebuilt every time you build OP-TEE): {{{#!bash make -j8 -C atf SPD=opteed PLAT=$PLAT BL32_BASE=$CFG_TZDRAM_START && \ cp atf/build/$PLAT/release/bl31.bin ./bl31.bin }}} 1. Build U-Boot: {{{#!bash # 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 }}} 1. Sign it: {{{#!bash # 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 }}} [=#fde] == 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: {{{#!bash 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: 1. Conveniently download a prebuilt ramdisk from our dev.gateworks website and inject our custom init script. 2. 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: {{{#!bash 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''''' {{{#!bash 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 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= # serverip for tftp server # Boot firmware support BOOT_FIRMWARE=http://$SERVERIP/tftpboot/venice/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 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 [ "$BOOT_FIRMWARE" ] && { echo "Writing locked down U-Boot to memory..." wget -q --show-progress --no-check-certificate -O data $BOOT_FIRMWARE || exit 1 # create 4MB boot0 image and flash it truncate -s 4M boot-firmware dd if=data of=boot-firmware bs=1K seek=33 conv=notrunc # note this seek=0 for imx8mm/imx8mp for boot0/boot1 # BOOT partitions by default are read-only as they are typically used for sensitive boot firmware. To write to them you must disable force_ro in sysfs echo 0 > /sys/class/block/${DEVICE}boot0/force_ro # write the flash.bin with proper offset to unlocked BOOTpart dd if=boot-firmware of=/dev/${DEVICE}boot0 } [ "$FIT_IMAGE" ] && { echo "Flashing FIT Image to memory..." wget -q --show-progress --no-check-certificate -O data $FIT_IMAGE || exit 1 # 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 } 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 [ "$TPM" ] && { # Get inital PCR measurment values echo "Retrieving initial PCR measurement values..." # PCR0: boot-firmware tpm2 pcrextend 0:sha1=$(sha1sum boot-firmware | 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) echo "Creating Key from TPM2.0..." tpm2_evictcontrol -C o -c $KEY_HANDLE # delete any existing key tpm2_createpolicy --policy-pcr -l sha1:0,8 -L policy.digest #only sha1 works for our current TPM's PCRs tpm2_createprimary -g sha256 -G rsa -c primary.context tpm2_create -g sha256 -u obj.pub -r obj.priv -C primary.context -L policy.digest -a "noda|adminwithpolicy|fixedparent|fixedtpm" -i $KEY tpm2_load -C primary.context -u obj.pub -r obj.priv -c load.context tpm2_evictcontrol -C o -c load.context $KEY_HANDLE # save key to TPM handle rm -rf 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..." echo "YES" | cryptsetup luksFormat ${DEV}${PART} $KEY - || exit 1 # 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 || 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/null; then exec 0/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) [ "$TPM" ] && { echo "Fetching Key from TPM..." tpm2_unseal -c $KEY_HANDLE -p pcr:sha1:0,8 -o fs.keyphrase_decrypted KEY=fs.keyphrase_decrypted } DEV=${DEV}${PART} echo "Opening $DEV..." cryptsetup luksOpen $DEV $NAME --key-file=$KEY || exit 1 [ "$TPM" ] && { # reseal the key by extending the PCR's rm -rf fs.keyphrase_decrypted tpm2_pcrextend 0:sha1=0000000000000000000000000000000000000000 tpm2_pcrextend 8:sha1=0000000000000000000000000000000000000000 } # 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 the needed overlay 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 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. {{{#!bash #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) #get all the custom files needed in one spot mkdir overlay cp fs.key init overlay/ #utilize a bsp to copy in required kernel modules to be used for tpm in the init script mkdir -p overlay/lib/firmware/imx/sdma/ export VENICE_BSP=/path/to/venice_bsp for i in virt-dma imx-sdma spi-imx tpm_tis_spi; do file=$(find $VENICE_BSP/linux/ -name $i.ko); [ -r "$file" ] && cp $file overlay && echo $file; done wget https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/plain/imx/sdma/sdma-imx7d.bin -O overlay/lib/firmware/imx/sdma/sdma-imx7d.bin # copy needed files to initrd cp -r overlay/* 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. {{{#!bash #copy it over to your tftp server (under the venice sub-dir) cp rootfs.cpio.xz /tftpserver/venice }}} ==== 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: {{{#!bash git clone https://github.com/buildroot/buildroot.git cd buildroot }}} - Get a copy of Gateworks buildroot defconfigs {{{#!bash 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 {{{#!bash 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 {{{#!bash # 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: {{{#!bash 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. {{{#!bash 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. {{{#!bash 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. {{{#!bash 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. {{{#!bash 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: 1. Using the ramdisk for encrypting a storage device (typically done only once) {{{#!bash setenv bootargs provision=rootfs1 }}} 2. Using the ramdisk for unlocking a storage device (default) {{{#!bash 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 {{{#!bash cp blobs/imx8mm-venice-gw73xx-0x.dtb blobs/Image blobs/rootfs.cpio.xz /tftpboot/venice/ }}} - target; boot them via tftp {{{#!bash u-boot=> dhcp && setenv 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. [=#external-fit] ==== 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: {{{#!bash # 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: {{{#!bash 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-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. We are going to use the '-K ' 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: {{{#!bash # create a dummy its file cat < 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. {{{#!bash 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: 1. Checkout U-Boot (or use your existing U-Boot that you have been working with): {{{#!bash 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 }}} 2. Make sure your environment is configured for cross-compiling: {{{#!bash (cd $VENICE_BSP; . ./setup-environment) make imx8mm_venice_defconfig }}} 3. 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. 4. 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 {{{#!bash make menuconfig # enable FIT_SIGNATURE/RSA, disable LEGACY_IMAGE_FORMAT make savedefconfig && cp defconfig configs/imx8mm_venice_defconfig }}} 5. 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: {{{#!bash $ git diff 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 }}} 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: {{{#!bash 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: {{{#!bash 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: {{{#!bash printf "0x%x\n" $((63*1024*1024/512)) # hex 512byte blocks 0x1f800 }}} Now to tftp transfer and flash the fit image to memory {{{#!bash 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: {{{#!bash 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 [=#embedded-fit] ==== 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: 1. Checkout U-Boot (or use your existing U-Boot that you have been working with): {{{#!bash 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 }}} 1. Make sure your environment is configured for cross-compiling: {{{#!bash (cd $VENICE_BSP; . ./setup-environment) make imx8mm_venice_defconfig }}} 1. 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. {{{#!bash 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 }}} 1. 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. {{{#!bash mkimage -A arm64 -T ramdisk -C none -d blobs/rootfs.cpio.xz blobs/uramdisk }}} 1. 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: {{{#!bash 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"; }; }; }}} - update the loadables to include the 'os_kernel', 'os_fdt', 'os_ramdisk' you added 1. 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. {{{#!bash mkimage -A arm64 -T ramdisk -C none -d ../blobs/rootfs.cpio.xz ../blobs/uramdisk }}} When complete your differences should look like this: {{{#!bash $ 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 */ - +#define DEBUG #include #include #include 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: {{{#!bash 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) {{{#!bash 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: {{{#!bash 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 [=#env] == 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: 1. 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 2. 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. {{{#!bash #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: For the FIT Image, you must use this rootfs.cpio.xz that we just created above, a standard (built from our BSP) or custom device tree blob specific to the board you are using, and you can use our Gateworks Kernel (built from our BSP) or other custom kernel: {{{#!bash mkdir blobs cp $VENICE_BSP/linux/arch/arm64/boot/Image blobs/ cp $VENICE_BSP/linux/arch/arm64/boot/dts/freescale/imx8mm-venice-gw72xx-0x.dtb blobs/ cp rootfs.cpio.xz blobs/ # gzip the kernel gzip -fk blobs/Image }}} 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="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" #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: 1. Checkout U-Boot (or use your existing U-Boot that you have been working with): {{{#!bash 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 }}} 1. Make sure your environment is configured for cross-compiling: {{{#!bash (cd $VENICE_BSP; . ./setup-environment) make imx8mm_venice_defconfig }}} 1. Use 'make menuconfig' to make the changes above {{{#!bash make menuconfig # enable CONFIG options described above make savedefconfig && cp defconfig configs/imx8mm_venice_defconfig }}} 1. Make the flash.bin and sign it and tftp it over {{{#!bash make flash.bin # sign flash.bin; see "i.MX secure boot" section /bin/sh doc/imx/habv4/csf_examples/mx8m/csf.sh }}} 1. 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: {{{#!bash $ 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: {{{#!bash u-boot=> dhcp && setenv serverip && 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: {{{#!bash u-boot=> dhcp && setenv serverip && 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/