
In today's increasingly online world, comprehensive system security is more important than ever. With access to millions of unsecured devices, such as cameras and set-top boxes, cybercriminals have exploited and regularly used a vast array of them worldwide. With the increasing use of internet-connected industrial and medical controls that require connectivity for basic operation, this level of unauthorized access could have critical or even life-threatening consequences.
When all software running on a device is fully validated via digital signatures, risks from exploits that target software on these sorts of embedded devices are significantly mitigated.
Defining the Chain of Trust
In cybersecurity, a threat model is a set of attack types that can reasonably be expected to occur. Secure boot and root filesystem integrity aim to address threats where attackers must execute code that was not intended during the device's development lifecycle. Using a chain of trust mitigates this threat by preventing the execution of all untrusted code, starting with the very first code that runs on the device.
At the root of the chain, the processor/SoC is implicitly trusted. In turn, it validates the digital signature of the boot package it loads and refuses to boot it if the signature is invalid. Each subsequent stage of the chain past the boot package is fully defined in software, and it can ensure that the previous stage validated it successfully.
Then, the next stage that runs is validated and executed. This is repeated down to the application level, guaranteeing that all software derives its validity from a digital signature.
Using Hash Trees for Block Device Integrity
Hash trees are a natural application of the chain-of-trust concept. In a hash tree, or Merkle tree, each node contains a cryptographic hash value, defined to be equal to the hash of all the nodes’ direct children or stored data values. The recursive structure ensures that if any node in a subtree is modified by even a single byte, the root of the tree becomes invalid.
┌────────────Node 0─────────────┐
│ hash = 27c62bb9639db5... │
│ children = {1, 2, 3, 4} │
│ data = nil │
└───────────────────────────────┘
…
┌────────────Node 1─────────────┐ ┌────────────Node 2─────────────┐
│ hash = 4275e8701916f6... │ │ hash = c5ceb77a505b40... │
│ children = {5, 6, 7, 8} │ │ children = {9, A, B, C} │
│ data = nil │ │ data = nil │
└───────────────────────────────┘ └───────────────────────────────┘
┌────────────Node 3─────────────┐ ┌────────────Node 4─────────────┐
│ hash = 66a074305cd554... │ │ hash = 6b584794b7b853... │
│ children = {D, E, F, 10} │ │ children = {11, 12, 13, 14} │
│ data = nil │ │ data = nil │
└───────────────────────────────┘ └───────────────────────────────┘
… … … …
By having the lowest-level blocks in the hierarchy "map" a specific range of bytes, the hash tree effectively serves as a storage system for hierarchically verifying the integrity of any data it contains.
To also guarantee authenticity, the root hash can be digitally signed. Once the root hash is verified against the digital signature, repeating the hash verification against each node guarantees that the contents of the entire tree have been authenticated by the same digital signature. We’ll take a deeper look at signature verification of the root hash later in this article.
Linux and the dm-verity Driver
Because each stage in the chain of trust depends on the signed subsequent stage, it makes logical sense to work backwards and generate the last stage to be verified first. In a Linux environment, this is the filesystem level.
The Linux kernel incorporates dm-verity, a virtual block device, as part of the Device Mapper infrastructure. When being set up, it takes a raw block device and a pre-generated hash tree, and uses the hash tree to verify (and even perform some error correction) reads from the raw block device, or return errors if the raw data couldn't be verified.
You can attach dm-verity to a filesystem image in any format with the following steps:
- Generate the filesystem image (e.g., rootfs.ext4).
- Using veritysetup, generate the hash tree into a separate block device image and fetch the root node's hash.
- Write the rootfs.ext4 image and hash tree into separate logical partitions on device storage.
- Store this root hash in an initramfs script. In the same script, mount the verified block device using veritysetup with the root hash. If successful, the filesystem image inside the verified block device can be mounted and root changed into it.
Chaining Trust from the Bootloader
To ensure the bootloader recognizes the kernel, initramfs, and device tree as valid, it is common practice to use a binary FIT (Flattened uImage Tree) image file, which serves as a simple container for these files with their appended signatures. U-Boot, has support for booting FIT images using the bootm command. When secure boot is enabled, it will validate the signature of the contained resources before launching.
The Yocto u-boot recipes contain u-boot-sign.bbclass, which automates the process of signing FIT images. Additionally, ensure KERNEL_IMAGETYPES contains fitImage.
The steps in the previous section suggest storing the root hash for dm-verity directly into a script in the initramfs. Signing the entire initramfs binary transitively attaches a signature value to the root hash stored within, so the filesystem is now verified with the bootloader signing key. The kernel binary is signed (as is the device tree binary, if present), enabling the bootloader to fully verify subsequent execution.
Chaining Trust from the SoC
Establishing an SoC to be a trust root is slightly more complicated and vendor-specific. In general, a "root" keypair and a code bundle that the processor loads on startup to sign are the only mandatory prerequisites. The process is generally straightforward. Here is an example that uses i.MX8 AHAB (Advanced High Assurance Boot), but the steps are similar for other vendors.
- Generate the root keys. First, follow the setup instructions to create a serial file and password, then run the ahab_pki_tree.sh script provided by NXP.
- Generate the root key hash to fuse. This is done using srktool, which calculates the Super Root Key values.
- Add the outputs of the previous stage to the device's Yocto build system to sign the boot package. Layers like meta-variscite-hab or Vigiles streamline the process.
- Install the signed boot package and program the root key hash into the SoC by blowing fuses.
If done correctly, the SoC will now verify the boot package, the bootloader will verify the kernel, the kernel will verify the filesystem, and the filesystem's integrity ensures that only the intended application code runs on the device.
Secure Trust from Silicon to Software
Many real-world threats to embedded Linux devices exist, meaning the security of your embedded Linux environment can’t be an afterthought. Building a reliable chain of trust from the SoC to the root filesystem ensures that every stage of your system boots with verified, authenticated software.
Ultimately, validating each stage of your software stack is how you turn “secure by design” from a catchphrase into a reality.
Want to learn how Lynx can help you implement a verified chain of trust for your embedded systems? Explore our security solutions or contact us to discuss how to build a secure foundation tailored to your device.