wiki:venice/secure_boot

Version 22 (modified by Samuel Lee, 8 months ago) ( diff )

--

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

See also:

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
    1. Retrieve the NXP Code Signing Tool (CST): https://www.nxp.com/webapp/Download?colCode=IMX_CST_TOOL_NEW (Account required on NXP site)
    2. Unpack the CST :
      tar xvf cst-3.4.0.tgz
      cd cst-3.4.0/keys
      
    3. 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
      
    4.  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
      
    5. 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'
    6. 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
    7. 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
  2. Build U-boot with HABv4 enabled and a single DTB:
    • Pre-requisite: Build Venice BSP using this link here: venice/bsp
    • Export VENICE_BSP variable to your directory: export VENICE_BSP=/path/to/your/bsp/directory
      # checkout a fresh u-boot
      git clone https://github.com/Gateworks/uboot-venice.git
      cd u-boot
      # setup cross toolchain environment (ie source setup-environment in Venice BSP dir)
      export PATH=$VENICE_BSP/buildroot/output/host/bin:$PATH
      export CROSS_COMPILE="aarch64-linux-"
      export ARCH=arm64
      # copy necessary artifacts from bsp
      cp $VENICE_BSP/u-boot/lpddr4*.bin . # DDR firmware
      cp $VENICE_BSP/atf/build/imx8mm/release/bl31.bin . # ATF
      # configure for venice board
      make imx8mm_venice_defconfig
      make menuconfig # select CONFIG_IMX_HAB=y and CONFIG_SPL_LOAD_FIT_ADDRESS=0x44000000
      make flash.bin
      
    • for clarity here are the differences in defconfig:
      $ make savedefconfig && diff defconfig configs/imx8mm_venice_defconfig
      scripts/kconfig/conf  --savedefconfig=defconfig Kconfig
      21,22d20
      < CONFIG_IMX_HAB=y
      < # CONFIG_CMD_DEKBLOB is not set
      31d28
      < CONFIG_SPL_LOAD_FIT_ADDRESS=0x44000000
      
  3. 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
    
  4. Program signed firmware image:
    jtag_usbv4 -p signed_u-boot_spl-imx8mm.bin
    
    • Booting this would look something like the following:
      U-Boot SPL 2023.04-00034-g1f567dfbe119 (Jun 23 2023 - 15:53:20 -0700)
      GSCv3   : v61 0x1d6f RST:VIN Thermal protection:disabled 
      RTC     : 1970-01-01   0:00:31 UTC
      Model   : GW7200-01-B1F
      Serial  : 935180
      MFGDate : 04-05-2023
      PMIC    : MP5416 (IMX8MM)
      DRAM    : LPDDR4 4 GiB 3000MT/s 1500MHz
      Failed to initialize caam_jr: -19
      WDT:   Started watchdog@30280000 with servicing every 1000ms (60s timeout)
      Trying to boot from eMMC
      hab fuse not enabled
      
      Authenticate image from DDR location 0x44000000...
      DTB     : imx8mm-venice-gw72xx-0x
      NOTICE:  BL31: v2.4(release):f884ad7b0ba2
      NOTICE:  BL31: Built : 13:06:09, Oct 20 2021
      
      
      U-Boot 2023.04-00034-g1f567dfbe119 (Jun 23 2023 - 15:53:20 -0700)
      
    • Note the 'hab fuse not enabled' message which means the SEC_CONFIG[1] fuse is not blown and the device is not locked
    • Note the 'Authenticate image from DDR location' messages which shows that image authentication is able to be used
  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
      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:
    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
    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:

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:

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:

OP-TEE on venice

In general the following things need to be done to use OP-TEE in the 'SPL -> ATF -> OP-TEE -> U-Boot -> Linux' boot flow:

  • build ATF (bl31.bin) with OP-TEE support:
    • Add 'SPD=opteed' to the env to use a Secure Payload Dispatcher (SPD)
    • Add 'BL32_BASE=' to the env to tell the ATF (BL31) where TEE is in memory (BL32)
  • build TEE (tee.bin):
    • PLATFORM=imx
    • PLATFORM_FLAVOR=mx8mmevk|mx8mnevk|mv8mpevk - Use one of these depending on which SOC (imx8mm/imx8mn/imx8mp) you are using. These define CFG_UART_BASE and some memory config which we will override to suit our needs
    • CFG_DDR_SIZE= - Specify the DRAM size of the board you are building for. For example 1GiB=0x40000000, 2GiB=0x80000000, 4GiB=0x10000000
    • CFG_CORE_LARGE_PHYS_ADDR= - n for 3GiB or less DRAM, and y for larger than 3GiB DRAM
    • CFG_CORE_ARM64_PA_BITS= - 32 for 3GiB or less DRAM, and 36 for larger than 3GiB DRAM
    • CFG_TZDRAM_START= - the (link/load) address where TEE should run at
  • build u-Boot with the following configuration:
    • CONFIG_IMX_HAB=y
    • CONFIG_CMD_DEKBLOB=y
    • CONFIG_SPL_LOAD_FIT_ADDRESS=0x48000000
    • CONFIG_OPTEE=y
    • CONFIG_OPTEE_LOAD_ADDRESS= the (link/load) address for TEE that should match BL32_BASE for ATF and CFG_TZDRAM_START for OPTEE

Note that you have to tailor the firmware specifically for the DRAM size of your board and that it is very important to ensure that BL32_BASE (used when building ATF), CFG_TZDRAM_START (used when building OPTEE), and CONFIG_OPTEE_LOAD_ADDRESS (U-Boot's .config file) all match. This address should be the top 32MiB of DRAM but because U-Boot is loading this from its FIT image into DRAM it needs to be a 32bit address and should be adjusted down if you are on a board with 4GiB:

  • 1GiB DRAM use 0x7e000000
  • 2GiB DRAM use 0xbe000000
  • 3GiB or larger use 0xfe000000

Here are the detailed steps to build secure boot firmware for imx8mm:

  1. setup cross compile environment
    # setup cross compile environment - here we will use the venice bsp toolchain/config
    cd venice/bsp
    . ./setup-environment
    
  2. setup additional SOC and Board specific environment:
    • for imx8mm:
      export PLATFORM_FLAVOR=mx8mmevk # used for OPTEE build
      export PLAT=imx8mm # used for ATF build
      
    • for imx8mp:
      export PLATFORM_FLAVOR=mx8mpevk # used for OPTEE build
      export PLAT=imx8mp # used for ATF build
      
    • for 1GiB DRAM boards:
      export CFG_DDR_SIZE=0x40000000
      export CFG_CORE_LARGE_PHYS_ADDR=n
      export CFG_CORE_ARM64_PA_BITS=32
      export CFG_TZDRAM_START=0x7f000000
      
    • for 2GiB DRAM boards:
      export CFG_DDR_SIZE=0x80000000
      export CFG_CORE_LARGE_PHYS_ADDR=n
      export CFG_CORE_ARM64_PA_BITS=32
      export CFG_TZDRAM_START=0xbf000000
      
    • for 4GiB DRAM boards:
      export CFG_DDR_SIZE=0x100000000
      export CFG_CORE_LARGE_PHYS_ADDR=y
      export CFG_CORE_ARM64_PA_BITS=36
      export CFG_TZDRAM_START=0xfe000000
      
  3. 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
    
  4. 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 .
    
  5. 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
    
  6. Build ATF:
    make -j8 -C atf SPD=opteed PLAT=$PLAT BL32_BASE=$CFG_TZDRAM_START && \
    cp atf/build/$PLAT/release/bl31.bin ./bl31.bin
    
  7. Build U-Boot:
    # start with standard venice defconfig
    make imx8mm_venice_defconfig # choose imx8mm/imx8mn/imx8mp depending on board SOC
    # enable IMX_HAB/OPTEE
    make menuconfig # search for (with /) and set CONFIG_IMX_HAB=y CONFIG_CMD_DEKBLOB=y CONFIG_SPL_LOAD_FIT_ADDRESS=0x48000000 CONFIG_OPTEE=y and CONFIG_OPTEE_LOAD_ADDRESS= set to the value of CFG_TZDRAM_START
    make -j8 flash.bin
    
  8. 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

Kernel and DTB

We are going to need a kernel and DTB for your board. For this demonstration we will use the kernel and DTB's from the pre-built Gateworks Linux tarball:

mkdir blobs
# grab the prebuilt gateworks kernel
wget https://dev.gateworks.com/venice/kernel/linux-venice-6.6.8.tar.xz
tar -C blobs/ --strip-components=2 -xvf linux-venice-6.6.8.tar.xz ./boot
# gzip the kernel
gzip -fk blobs/Image # creates blob/Image.gz

you can choose your own kernel/dtb/rootfs as long as they provide support for dm-crypt

Ramdisk Creation

We offer two pathways for obtaining a RAM disk tailored for our Full Disk Encryption (FDE) approach:

  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:

dd if=/dev/urandom of=fs.key bs=1 count=4096

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
    
    # 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=<YOUR_SERVER_IP> # 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
                    echo "Your Keyphrase (up to 256BYTE)" > $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/console) 2>/dev/null; then
        exec 0</dev/console
        exec 1>/dev/console
        exec 2>/dev/console
    fi
    exec /sbin/init "$@"
                            ;;
                    provision=*) provision ${x//provision=};;
            esac
    done
    
    echo "Using $DEV..."
    
    # Wait for device to exist
    echo "Waiting for $DEV..."
    while [ ! -b "$DEV" ]; do
            sleep 1
            echo -n .
    done
    
    # Open the LUKS encrypted partition and map it to the specified name ($NAME)
    [ "$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.

#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.

#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:
    git clone https://github.com/buildroot/buildroot.git
    cd buildroot
    
  • Get a copy of Gateworks buildroot defconfigs
    wget -O configs/venice_example_defconfig https://dev.gateworks.com/buildroot/venice/minimal/venice_example_defconfig
    wget https://dev.gateworks.com/buildroot/venice/minimal/venice_minimal_kernel_6.1_defconfig
    
  • Make an overlay directory and get the custom init script as well as the filesystem key there
    mkdir overlay
    cp init overlay/init
    cp fs.key overlay/
    

When specifying the overlay directory path in the Buildroot configuration (BR2_ROOTFS_OVERLAY), you simply provide the path to the directory you've chosen, regardless of its name. As long as the path is correctly specified, Buildroot will incorporate the contents of the overlay directory into the final root filesystem image during the build process.

  • Buildroot's make command generates multiple artifacts, however, our focus is solely on the rootfs.cpio.xz file it produces
    # make the custom filesystem
    make venice_example_defconfig
    make
    

This will produce output/images/rootfs.cpio.xz which we will copy to our blobs directory for safe keeping:

cp output/images/rootfs.cpio.xz blobs/

Using the Ramdisk and custom init script

Configuring Bootargs for Different init script use cases

Regardless of what option you choose for Secure System Setup in the next section, you will need to be familiar with how the example init script is intended to be used once you get your board to u-boot: this is a preliminary section describing how to use the script.

In order to utilize the custom init script properly, you can configure the bootargs variable at the U-Boot prompt to specify different provisioning options for setting up the Full Disk Encryption (FDE) and unlocking your FDE. Below are the available options:

  • rootfs1 (provision option):

Use this option to provision the emmc by creating an ext4 filesystem on the LUKS device and downloading and extracting root file system components (kernel and rootfs) from specified URLs.

setenv bootargs provision=rootfs1
  • rootfs2 (provision option):

This option provisions the emmc from a compressed disk image containing a partition with the root file system. The image is extracted, and the root file system is copied to the LUKS device.

setenv bootargs provision=rootfs2
  • rootfs3 (provision option):

This option provisions the emmc from an existing compressed file system that can be downloaded and directly copied to the LUKS device.

setenv bootargs provision=rootfs3
  • bypass (debugging):

Set bootargs to bypass to bypass normal provisioning/mounting and drop you into a bash shell with the buildroot root filesystem. This is useful for debugging and prototyping custom provisioning commands.

setenv bootargs bypass
  • Unlocking the FDE (Default):

Leaving bootargs blank or not specifying any provisioning option will configure the system for regular use and unlocking the Full Disk Encryption. This is the default case where you have already provisioned your storage device and the FDE is switched to be used as the root file system.

Note: Provisioning options are intended to be used as a one-time process during system setup.

Confirming working operation of individual components

We strongly suggest to do a confirmation test that your kernel image, device tree, and ramdisk all work as intended before moving on.

At U-Boot, you have the option to choose the behavior:

  1. Using the ramdisk for encrypting a storage device (typically done only once)
    setenv bootargs provision=rootfs1
    
  2. Using the ramdisk for unlocking a storage device (default)
    setenv bootargs
    

Once you have your Kernel, ramdisk and device tree blob successfully copied over to a tftp server, we suggest confirming their working operation before continuing. You should be able to boot your ramdisk as follows

on host:

  • copy the kernel Image, your dtb, and your rootfs.cpio.xz ramdisk to your tftp server venice directory
    cp blobs/imx8mm-venice-gw73xx-0x.dtb blobs/Image blobs/rootfs.cpio.xz /tftpboot/venice/
    
  • target; boot them via tftp
    u-boot=> dhcp && setenv serverip <serverip>
    u-boot=> setenv bootargs bypass; tftpboot $kernel_addr_r venice/Image && tftpboot $fdt_addr_r venice/imx8mm-venice-gw73xx-0x.dtb && tftpboot $ramdisk_addr_r venice/rootfs.cpio.xz && booti $kernel_addr_r $ramdisk_addr_r:$filesize $fdt_addr_r
    
  • it is important here that you tftp the ramdisk last as we use the filesize variable which is set by tftp to give booti the size of the ramdisk which is requires

Choosing the Boot Method for Secure System Setup

Now that we have our ramdisk, kernel, and device tree blob we have a few different options to continue on our path to having a secure system.

  • Option 1: Creating a signed FIT image and transferring it over TFTP, and flashing the fit image to memory.
  • Option 2: Incorporating the ramdisk, kernel, and device tree into the firmware image itself with HABv4 secure boot via u-boots fit image, and flashing the fit image to memory.

In comparing Option 1 and Option 2, both provide robust firmware security through code signing and verification. However, Option 2 goes a step further by integrating the verification into the hardware root-of-trust using platform security features like HABv4.

Option 1 provides more versatility in terms of being able to construct different FIT image payloads signed by the same key that U-Boot can universally verify and boot from. This modularity can be advantageous, especially for deployment scenarios that need frequent kernel/filesystem updates without modifying the secure bootloader components. 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:

Generation of Signed FIT Image

We will need a key to used for image signing and verification.

To create a new public/private 2048 bit key pair:

# on host machine
openssl genpkey -algorithm RSA -out fit.key -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537
# to create a certificate for this containing the public key:
openssl req -batch -new -x509 -key fit.key -out fit.crt
# you can see the public key:
openssl rsa -in fit.key -pubout

Create the ITS template where each image is defined to have a hash and each configuration is signed:

cat << EOF > fit.its
/dts-v1/;

/ {
        description = "Image for Linux Kernel";
        #address-cells = <1>;

        images {
                kernel {
                        description = "Linux Kernel";
                        data = /incbin/("blobs/Image.gz");
                        type = "kernel";
                        arch = "arm64";
                        os = "linux";
                        compression = "gzip";
                        load =<0x48200000> ;
                        entry = <0x48200000>;
                        hash-1 {
                                algo = "sha256";
                        };
                };
                ramdisk {
                        description = "ramdisk";
                        data = /incbin/("blobs/rootfs.cpio.xz");
                        type = "ramdisk";
                        arch = "arm64";
                        os = "linux";
                        compression = "none";
                        load = <0x50300000>;
                        hash-1 {
                                algo = "sha256";
                        };
                };
                fdt {
                        description = "fdt";
                        data = /incbin/("blobs/imx8mm-venice-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 <dtb>' option to add a signature for the key into an existing DTB but U-Boot builds its control DTB itself so we will create a dummy DTB to put the key in then decompile it:

# create a dummy its file
cat <<EOF> blobs/signature.dts
 /dts-v1/;

/ {
};
EOF
# compile the dts to a dtb with device-tree compiler
dtc -I dts -O dtb blobs/signature.dts > blobs/signature.dtb
# create the fit image and add the signature to the dtb
mkimage -f fit.its -k . -K blobs/signature.dtb -r fit.itb
# decompile the dummy dtb
dtc -I dtb -O dts blobs/signature.dtb > blobs/signature.dts
# now we have a signed fit.itb you can put on your board or tftp
cp fit.itb /tftpboot/venice
  • Note: the -k parameter is the directory that the fit image key resides that is specified by key-name-hint. So for example with key-name-hint "fit" as above; fit.key will be in the directory specified by the -k parameter.

Before continuing with securing uboot, a quick check to make sure this fit image boots is a good idea. This step is strictly to help with debugging if there is an issue with your image blobs or an issue with u-boot's build configuration.

setenv bootargs bypass; tftpboot 0x40200000 venice/fit.itb && bootm 0x40200000
  • Note: we are loading into 0x40200000 instead of $loadaddr because loadaddr is the same as kerneladdr and our fit image is loaded there and they conflict.

Getting FIT key into U-Boot proper and verifying signed images

In order for U-boot to validate a signed FIT image it must have access to the key used to sign that image. This is why we created a signature.dtb in the previous step by using ‘mkimage -K’ to ‘inject’ a node into our signature.dtb.

Now we need to modify U-Boot to allow it to verify FIT images against that key:

  • copy the signature into the imx8mm-u-boot.dtsi, imx8mn-u-boot.dtsi or imx8mp-u-boot.dtsi file depending on your SoC.
  • enable CONFIG_FIT_SIGNATURE and RSA_VERIFY
  • disable CONFIG_LEGACY_IMAGE_FORMAT so that unsigned (or signed images that failed verification!) are not allowed to boot

Procedure:

  1. 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
    
  2. Make sure your environment is configured for cross-compiling:
    (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
    make menuconfig # enable FIT_SIGNATURE/RSA, disable LEGACY_IMAGE_FORMAT
    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

When complete your differences should look like this:

$ git diff
diff --git a/arch/arm/dts/imx8mm-u-boot.dtsi b/arch/arm/dts/imx8mm-u-boot.dtsi
index 04a7093a66..6d1f7e7907 100644
--- a/arch/arm/dts/imx8mm-u-boot.dtsi
+++ b/arch/arm/dts/imx8mm-u-boot.dtsi
@@ -4,6 +4,20 @@
  */
 
 / {
+       signature {
+
+               key-fit {
+                       required = "conf";
+                       algo = "sha1,rsa2048";
+                       rsa,r-squared = <0x5123aa0b 0x92792834 0x3f805a1b 0xa85c3894 0x817b72af 0xe5f737ef 0x9c97b0b0 0xe25dc8e5 0xfa
6a455f 0x18112fca 0xc5cfdd34 0xcc9e62ee 0x53401b1 0xceb41498 0xd3697f64 0x95a198c1 0x568b83b 0xb6cfedde 0x719783df 0x85724f45 0xc0c7b
83a 0x6d692e33 0x46a8e77e 0xf72fba00 0x5f294060 0xa3b8ab2f 0xcdc10f08 0xdec24553 0xccd09d3e 0xfed61495 0xb6f034bc 0x5d7a3e1e 0x5a8ee4
4c 0xc58fd62 0xee38127d 0x7be833c0 0xa43e308e 0xaf26959f 0x286f45ed 0xe0d95788 0x9818f079 0xaf4bdcf3 0x88ee0caf 0x7fdcd2b7 0x41f1217b
 0xaf1ce320 0x1582997f 0x17232232 0x4794518d 0x79746211 0x2c0adb7f 0xc0288fc2 0xc4851840 0x85bf410a 0xc05fbb3f 0x8f38e96f 0x6d6b5b43 
0xb195adc7 0xcb786525 0x1bb9f2b 0xff26d159 0xb2eee232 0xbea15340 0xff9c4086>;
+                       rsa,modulus = <0xb1430c9e 0x101ef408 0x66e818f5 0xde892ee1 0x7ec0ed73 0xd5a899b7 0x2f5d9012 0x5cefdc25 0x9e3f
8697 0x5c06cc4e 0x368cb44a 0x97d1d99a 0x25c6bf39 0x2bbc1028 0x306cc237 0x74f9adfd 0xf9811fe8 0x3efa493c 0xcc5e97bb 0x3d55977d 0x1f918
32a 0xb6f300d0 0x4b00182a 0x9ee2e97f 0x89d2f8cd 0x95b3a15d 0xc0c28d86 0x14dfce86 0x42b16994 0xbbca5180 0x273cea3a 0x97a6ee90 0x26db41
38 0xf30ac22a 0xc34ce324 0xc02e160f 0xfbc14603 0xa7f95016 0xe73d7f3b 0x2e6f15b7 0x78e54e1 0xaa98b25a 0xdbc2ea6d 0x598b03a8 0x9a5f14e4
 0xbface2c0 0xb1726a07 0xb2848286 0x3c071c5b 0xedc37018 0xb4f00631 0x30b23649 0xd03c0477 0x6573dbe7 0xe2617b39 0x1fa5407d 0xeac68d0f 
0xc08f4fb2 0x28d36d15 0x1ce825bd 0xf792c214 0xb48f369 0x146c427e 0xa74f0177>;
+                       rsa,exponent = <0x00 0x10001>;
+                       rsa,n0-inverse = <0xa2fbd7b9>;
+                       rsa,num-bits = <0x800>;
+                       key-name-hint = "fit";
+               };
+       };
+
        binman: binman {
                multiple-images;
        };
diff --git a/common/spl/spl_fit.c b/common/spl/spl_fit.c
index 730639f756..f6955eabbd 100644
--- a/common/spl/spl_fit.c
+++ b/common/spl/spl_fit.c
@@ -3,6 +3,7 @@
  * Copyright (C) 2016 Google, Inc
  * Written by Simon Glass <sjg@chromium.org>
  */
+#define DEBUG
 
 #include <common.h>
 #include <errno.h>
diff --git a/configs/imx8mm_venice_defconfig b/configs/imx8mm_venice_defconfig
index d254e2a012..d9511538c0 100644
--- a/configs/imx8mm_venice_defconfig
+++ b/configs/imx8mm_venice_defconfig
@@ -18,6 +18,8 @@ CONFIG_SPL_DRIVERS_MISC=y
 CONFIG_SPL_STACK=0x920000
 CONFIG_SPL=y
 CONFIG_ENV_OFFSET_REDUND=0x3f8000
+CONFIG_IMX_HAB=y
+# CONFIG_CMD_DEKBLOB is not set
 CONFIG_SYS_LOAD_ADDR=0x48200000
 CONFIG_SYS_MEMTEST_START=0x40000000
 CONFIG_SYS_MEMTEST_END=0x80000000
@@ -25,7 +27,9 @@ CONFIG_LTO=y
 CONFIG_SYS_MONITOR_LEN=524288
 CONFIG_FIT=y
 CONFIG_FIT_EXTERNAL_OFFSET=0x3000
+CONFIG_FIT_SIGNATURE=y
 CONFIG_SPL_LOAD_FIT=y
+CONFIG_SPL_LOAD_FIT_ADDRESS=0x44000000
 CONFIG_OF_BOARD_SETUP=y
 CONFIG_OF_SYSTEM_SETUP=y
 CONFIG_DISTRO_DEFAULTS=y

Now build it and update your board (by creating a JTAG image or using the 'update_firmware' script on the resulting flash.bin)

When uboot boots verify that the signature node is in the new controlling fdt:

u=boot-> fdt addr $fdtcontroladdr && fdt print /signature
Working FDT set to fdef4680
signature {
        key-fit {
                required = "conf";
                algo = "sha1,rsa2048";
                rsa,r-squared = <0x10cb82b5 0xc0d123dd 0x369b77e4 0xfc5fe7b4 0x34e1b663 0xac7ccbd1 0x2089fcb8 0xd473ccdb 0x79e857
af 0x67c88056 0xb9cfc0bd 0x2f854d0c 0xc9908774 0x4690496a 0x24140c3f 0xb15f60cf 0x661576bb 0x788700b7 0x31e31f54 0xc929c98b 0xe75
472b5 0x1c83c812 0xbdc68033 0xb96ebbaf 0xc2c1160c 0xfd737350 0xbfbdafba 0xe4a8c600 0x5c7dcf9b 0x02090083 0x428b42f4 0xafe211c1 0x
1924ce2f 0xdcb959f3 0xce5edeb4 0x38864bfe 0xc1100fef 0x5e697704 0x89560798 0x15f4dd46 0xc72bf173 0xbf83f373 0x47960ba8 0x6e49beda
 0x04fc6263 0x91fd2ac4 0x28975bbd 0x5e7cc6a9 0x857cb2db 0x3af856a7 0xc14bbde4 0x64710fbd 0xbc69c38a 0x03b65791 0x8aeac041 0x1e50b
0c5 0x9fb0c4f4 0x1fbe97e3 0xdaa7dcf7 0x80817ad7 0x5195514c 0x2a19ad58 0x66eb2870 0x05f78731>;
                rsa,modulus = <0xc95ff26c 0xad27a889 0x6bb89478 0x0e38cda9 0x29085777 0x1e896717 0x1e95d2ed 0x4cd49f17 0x63f49947
 0x5d98b3d5 0xd5833340 0x18fbe768 0x45ae09ed 0x2f86b951 0xc41c067f 0xd85f8d1b 0x76e427a3 0xe25a9f1d 0x754a5242 0x4f26037a 0x5ecd2
3e2 0x4c9c629c 0x4f4db6b9 0x1c21bf95 0xa276f126 0x469f4989 0x35a7c3d8 0x7c89c0c4 0x26675817 0x352c1f61 0xe3a3e594 0xce04653a 0xa9
d1ece4 0xba3ed4ac 0x9b35d81f 0xb1a3371d 0xc473ca80 0x8ce9bc54 0x81109b91 0x320f0244 0x7144346c 0xebd97bed 0x57757d18 0x0a0dc99e 0
x1fbc8e4d 0x20f15eec 0x2e80cc41 0x0748270d 0x054e1f67 0xe4f21cdf 0x1a6e56fd 0x857d51f6 0x219c6494 0x7c8e8a36 0xf3ecda1c 0xad866a3
0 0xd1142431 0x3afab3a9 0x723891cc 0x9774b093 0x05051de2 0x6a7e4167 0xd3e2c5e5 0x27fcc949>;
                rsa,exponent = <0x00000000 0x00010001>;
                rsa,n0-inverse = <0xb3698707>;
                rsa,num-bits = <0x00000800>;
                key-name-hint = "fit";
        };
};

Verify you can boot your signed image:

setenv bootargs bypass; tftpboot 0x40200000 venice/fit.itb && bootm 0x40200000
  • Note: we are loading into 0x40200000 instead of $loadaddr because loadaddr is the same as kerneladdr and our fit image is loaded there and they conflict.
  • Note U-boot will not verify signature if it can not find the signature node but will continue to load the image if CONFIG_LEGACY_IMAGE_FORMAT is enabled; in order to secure U-Boot you want to disable that.

At this point, you will need to decide where to store the signed FIT image on flash. You can either hard-code it to an offset outside the partitioned space (e.g., between the partition table and the first partition), or place it in an unencrypted partition that U-Boot can load from.

For the emmc on venice boards, the partition table starts at 0x0 and we like to leave 1MiB of space for ample room for the partition table. The custom init script we are using to provision the eMMC that creates the partition table and the partitions is configured to start P1's data at 64MiB offset so we can essentially use the eMMC user hardware partition from 1MB to 64MB allowing for 63MB (64-1) of loading space for the fit image (if this is not large enough for your FIT image, just modify the start of the P1 data offset in the provisioning step).

U-Boot's mmc commands use 512byte blocks in hex notation so we figure out what 63MB is for a blk count:

printf "0x%x\n" $((63*1024*1024/512)) # hex 512byte blocks
0x1f800

Now to tftp transfer and flash the fit image to memory

u=boot-> tftpboot $loadaddr venice/fit.itb && setexpr blkcnt $filesize + 0x1ff && setexpr blkcnt $blkcnt / 0x200 && mmc dev 2 0 && mmc write $loadaddr 0x800 $blkcnt
  • note we use setexpr to round up to the next block size to make sure we get all the data

You can now boot with this:

u=boot-> mmc dev 2 0 && mmc read 0x40200000 0x800 0x1f800 && bootm 0x40200000

Now that your secure ramdisk is written to memory: you are at the point of wanting to lock down your u-boot environment

Option 2 - Embed the kernel/dtb/ramdisk in the U-Boot ITB

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):
    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:
    (cd $VENICE_BSP; . ./setup-environment)
    make imx8mm_venice_defconfig
    
  3. 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
      
  4. 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
  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:
                                      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
  2. 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:

  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.

#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:

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
  • 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):
    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:
    (cd $VENICE_BSP; . ./setup-environment)
    make imx8mm_venice_defconfig
    
  3. Use 'make menuconfig' to make the changes above
    make menuconfig # enable CONFIG options described above
    make savedefconfig && cp defconfig configs/imx8mm_venice_defconfig
    
  4. 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
    
  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:

$ 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:

Note: See TracWiki for help on using the wiki.