Securing the boot process from the ROM code to the user-space is more than ever a requirement for embedded system projects. This post outlines the implementation of a trust boot chain (secure boot, integrity and encryption) on i.MX8 using Yocto and the NXP BSP layer. It assumes familiarity with the security concepts discussed.

Secure Boot

On i.MX, the Root of Trust (ROT) is the boot ROM and its sub-component High Assurance Boot (HAB). HAB is responsible for verifying signatures when the processor is configured as a secure device. HAB can be invoked at any time to verify a signature. For instance, the bootloader can authenticate the kernel image signature via HAB without embedding the kernel’s public key in the device tree.

The signature format is specific to NXP, and binaries must be signed using the NXP Code Signing Tool (CST) for HAB verification. For an overview of HAB and binary signatures with CST, refer to High Assurance Boot (HAB) for dummies.

PKI tree

The PKI tree can be generated using the CST tool package.

Here are the steps to quickly generate a PKI tree from the extracted CST package:

  1. Set an OpenSSL initial certificate serial number in the serial file in the /keys directory. For example:
    ~/cst/keys$ echo "12345678" > serial
  2. Set a passphrase to protect the HAB code signing private keys in the key_pass.txt in the /keys directory. For example:
    ~/cst/keys$ echo -e "test\ntest" > key_pass.txt
  3. Generate the PKI tree in CLI mode:
    ~/cst/keys$ ./hab4_pki_tree.sh \
        -existing-ca n -kt rsa -kl 2048 \
        -duration 5 -num-srk 4 -srk-ca y
  4. Generate the SRK tables and fuse binaries:
    ~/cst$ ./linux64/bin/srktool -h 4 \
        -t ./crts/SRK_1_2_3_4_table.bin \
        -e ./crts/SRK_1_2_3_4_fuse.bin \
        -f 1 -d sha256 \
        -c ./crts/SRK1_sha256_2048_65537_v3_ca_crt.pem,\
        ./crts/SRK2_sha256_2048_65537_v3_ca_crt.pem,\
        ./crts/SRK3_sha256_2048_65537_v3_ca_crt.pem,\
        ./crts/SRK4_sha256_2048_65537_v3_ca_crt.pem

For detailed keys and certification generation, please refer to the user guide UG10106 in the docs directory of the CST package.

Signing

The meta-nxp-security-reference-design layer handles the CST signatures using the secure-boot-image bbclass.

Before building, add the following lines to the local.conf to enable secure boot:

CST_PATH = "<absolute path to cst package>"
IMAGE_CLASSES:append = " secure-boot-image"

The imx-boot-signature and linux-imx-signature recipes sign the imx-boot binary and the Linux image, respectively.

Signed fitImage

Currently, the NXP layer does not support signing the fitImage format. Only Image and zImage are supported.

Two solutions exist to sign a fitImage:

  1. Since any file can be signed with HAB, extend the NXP recipe to support signing the fitImage format. This also requires patching U-Boot to authenticate the fitImage format (see Patch for u-boot-imx: Using FIT and HAB in bootm command).
  2. Use RSA keys to sign the fitImage instead of the CST-generated keys. This is supported by Yocto using the UBOOT_SIGN_ENABLE variable and U-Boot FIT Signature Verification. However, this requires:
    • Patching U-Boot to disable the authentication with HAB (and use RSA verification instead),
    • Managing a second key pair,
    • Embeddeding the fitImage’s public key in the bootloader’s device tree (update the bootloader if the fitImage signature changes).

Provisioning the keys

Provisioning the keys — i.e. flashing the public key to the OTP (fuses) — is tipically done during the installation process from U-Boot or Linux user-space. Both methods requires the SRK_1_2_3_4_fuse.bin file generated in the /crts directory of the CST.

The SRK OTP bank is 6 and 7. Refer to the Security Reference Manual IMX8MPSRM.

Flashing the keys from U-Boot

U-Boot provides a fuse command. First, prints the fuses values from the SRK fuse binary with their correct endianness using the following command:

~/cst/crts/$ hexdump -e '/4 "0x"' -e '/4 "%X""n"' < SRK_1_2_3_4_fuse.bin
0xValue1
0xValue2
0xValue3
0xValue4
0xValue5
0xValue6
0xValue7
0xValue8

Next, from U-Boot, fuse the keys in the proper order using the fuse command:

=> fuse prog -y 6 0 0xValue1
=> fuse prog -y 6 1 0xValue2
=> fuse prog -y 6 2 0xValue3
=> fuse prog -y 6 3 0xValue4
=> fuse prog -y 7 0 0xValue5
=> fuse prog -y 7 1 0xValue6
=> fuse prog -y 7 2 0xValue7
=> fuse prog -y 7 3 0xValue8

Note: Flashing the OTP is irreversible. Replace the 0xValueN with the correct values.

Flashing the keys from Linux user-space

Flashing the fuses from U-Boot is inefficient and difficult to automate in a factory installation process. Fortunately, this can be done easily from Linux user-space using the NVMe sysfs.

Here is an example of read and write operations on the SRK bank in Bash:

SRK_FILE="SRK_1_2_3_4_fuse.bin"

# Bank 6
OCOTP_SRK_BANK="6"
OCOTP_SRK_INDEX="$((OCOTP_SRK_BANK * 4))"
OCOTP_SRK_SIZE="8"

# Get the OCOTP sysfs file
ocotp_path=$(find /sys/bus/ -name "imx-ocotp0")
ocotp_file=${ocotp_path}/nvmem

# Read the bank
dd if="${ocotp_file}" bs=4 count="${OCOTP_SRK_SIZE}" skip="${OCOTP_SRK_INDEX}" 2>/dev/null | xxd -p

# Write the keys
dd if="${SRK_FILE}" of="${ocotp_file}" bs=4 seek="${OCOTP_SRK_INDEX}" 2>/dev/null

I recommend implementing some check operations, such as verifying if the SRK bank is empty (i.e. not already provisioned) and comparing the SRK to ensure the flashed keys are correct before closing the device.

Closing the device

Closing the device will put the board into secure mode. At the next boot:

  • The bootloader signature is verified by HAB,
  • The CAAM (see the encryption section) operates in secure mode.

Like flashing the fuses, closing the device is done by writing to OTP bank 1, word 3.

  • From U-Boot:
    => fuse prog 1 3 0x02000000
  • From Linux userspace:
    OCOTP_BOOT_CFG_BANK="1"
    OCOTP_BOOT_CFG_WORD="3"
    OCOTP_BOOT_CFG_INDEX="$((OCOTP_BOOT_CFG_BANK * 4 + OCOTP_BOOT_CFG_WORD))"
    
    echo -en "\x0\x0\x0\x2" > /tmp/config
    dd if="/tmp/config" of="${ocotp_file}" bs=4 seek="${OCOTP_BOOT_CFG_INDEX}" 2>/dev/null

Disable the JTAG

Disabling the Secure JTAG Controller (SJC) should be done on the same word.

Use 0x02200000 for U-Boot, or "\x0\x0\x20\x2" for Linux to disable the SJC and closing the device in one operation.

Disk integrity (dm-verity)

Authenticating the boot process is ineffective without user-space protection. Using dm-verity on a read-only rootfs is often the recommended solution.

In Yocto, the meta-security layer supports dm-verity integration.

Generation

The verity Merkle tree (vhash), root hash, and salt are generated at build time using the dm-verity-img bbclass. The class is well documented; refer to it for further information.

Before building, add the following line to the local.conf:

DM_VERITY_IMAGE = "core-image-minimal"
DM_VERITY_IMAGE_TYPE = "ext4"
IMAGE_CLASSES:append = " dm-verity-img"
IMAGE_FEATURES:append = " read-only-rootfs"

By default, the vhash is appended to the rootfs, forming a .verity file. The vhash can be generated separately with DM_VERITY_SEPARATE_HASH = "1".

Once the image built, the generated verity files are:

  • Merkle tree: in the deploy directory, within the rootfs .verity file or seperately as a .vhash file,
  • dm-verity variables (salt and root hash): in the work-shared directory, .verity.env file.

Integration

The verity (rootfs+vhash) or the vhash files can be direclty installed within the WIC image. The layer provides some examples.

For an installation process, the file can be directly flashed on the disk partition with dd.

Runtime

At every boot, the verity device mapper should be created before mounting the rootfs. This step is usually done during the initramfs boot process.

As example, the meta-security layer provides an initramfs dm-verity module. This module is used with the dm-verity-image-initramfs recipe, which includes the generated .verity.env file, required by the module.

Feel free to extend these recipes to match your project needs. For instance, the --restart-on-corruption is not enabled by the provided dm-verity script module. Furthermore, the script is compatible only if the rootfs partition is plain; for encrypted partition, a /dev/mapper path should be used instead of /dev/disk/by-partuuid.

Disk encryption (dm-crypt)

If the rootfs contains sensitive information, disk encryption with dm-crypt should be configured in addition to integrity.

Encryption on the i.MX8MP is enabled using the CAAM and Linux keyctl utilities. For further details, refer to NXP’s AN12714 and this article: Encrypted storage on i.MX by Marcus Folkesson.

The meta-imx layer provides the keyctl-caam package. Linux keyctl binary can be installed with the keyutils package from meta-openembedded layer.

Note: As explained in the secure boot section, the board should be closed; otherwise, the CAAM module will not use the unique hardware encryption keys. In an installation process, the board should reboot after closing the device before continuing the installation.

Encryption process

The encryption can be done during the installation process. The following commands create a unique key with the CAAM and load it with keyctl:

# Load the driver
modprobe dm-crypt
# Create a new keyctl session
keyctl new_session my_session
# Create a unique key with CAAM
caam-keygen create randomkey ecb -s 16
# Load the generated key in keyctl
cat /data/caam/randomkey | keyctl padd logon logkey: @s

From the loaded key, use dm-setup to create an encrypted device mapper for the rootfs partition:

dmsetup -v create rootfs --noudevsync --table "0 $(blockdev --getsz $DEVICE) crypt capi:tk(cbc(aes))-plain :36:logon:logkey: 0 $DEVICE 0 1 sector_size:512"

The rootfs ext4 or the verity (rootfs+vhash) files can then be flashed on the created device mapper.

The generated black blob of the random key should be copied to the plain (non-encrypted) partition, such as the boot partition.

Decryption Process

At every boot, before the verity part, the rootfs partition must be opened with dmsetup. First, imports the black blob from the plain partition to retrieve the generated key from CAAM:

# Load the driver
modprobe dm-crypt
# Import the key from the black blob
caam-keygen import /mnt/randomkey.bb importKey
# Load the key in keyctl
cat /data/caam/importKey | keyctl padd logon logkey: @us

The, create the device mapper (like previous section):

dmsetup -v create rootfs --noudevsync --table "0 $(blockdev --getsz $DEVICE) crypt capi:tk(cbc(aes))-plain :36:logon:logkey: 0 $DEVICE 0 1 sector_size:512"

Note: You may need to edit the dm-verity initramfs script to create the verity device mapper on top of the encrypted device mapper.