Firmware is made up of many layers. These are obvious: a bootloader, an RTOS, your application(s), etc. At startup you want to be able to guarantee the integrity of all that code. As well, the environment that is executing the code needs to match security expectations. Deep embedded systems, like typical IoT devices, make this an easier problem to solve. These systems are simple compared to Windows or a UNIX server.
Hardware Helps, Sort Of
There are many secure processors on the market today. These often see use in payment processing applications. Payment processing is where people are willing to invest security effort, since the cost of theft is so high. But, features of these devices are starting to show up in less sensitive environments. As of Skylake, Intel supports EFI image authentication in the U-series SKUs (called Platform Trust Technology). This is, of course, at the vendor's discretion if they're going to use it. Most choose to, though. The vendor can blow some fuses in the chip in the factory that represent a hash of their public key. More on this in a bit, though.
The intention these features is to create a root of trust. This process usually involves two steps:
- Verify the integrity of the environment. Lock down features that might compromise the guarantees about the code that is to run.
- Load the 'next stage' code, check its signature. Ensure public key hash matches the hash known to the hardware.
A special-purpose bootloader usually does this work. For some environments, this lives in ROM on the device. In other cases, like the case with Intel, special microcode verifies the integrity of the firmware while loading it.
By first verifying the integrity of the environment, we are able to prevent compromise before the firmware even loads. Usually this process is a kind of reset. Any system component state is reset to known values. This can also disable JTAG debugging and other invasive features that could enable code tampering.
Next, we want to verify the code you intend to run is the code you loaded. Public-key crypto makes this easy. Let's look at the way many vendors do this.
Known Public Keys and Verifying Signatures
Many such devices, from vendors like Broadcom, Atmel, Maxim Integrated and even Intel, have a one-time programmable memory (OTP). This memory is large enough to store a SHA-2 hash - usually SHA-256, so 32 bytes. By design, this memory stores a hash of your firmware signing public key. When the device powers on, and its internal bootloader or microcode takes over, it will read this memory. As a device vendor, you program this with a hash of your public key per the ASIC vendor's spec.
The root of trust in a device is that key hash. So when you program the key hash, you need to ensure it isn't tampered with. This is where a trusted manufacturing facility comes in. Programming this hash in a trusted environment decreases the likelihood of someone swapping the key out for one of their own.
Your complete firmware image is then signed with the private half of that key. Your firmware image needs a structure known to the ROM or device for loading and verification. You need to provide 3 key pieces of information:
- your public key (the one you programmed the hash of),
- your firmware image and
- the signature for that firmware image.
The device will read your public key, hash it, then compare the hash to the known key hash in the OTP memory. If this matches, it will proceed to load the firmware image. Finally, after hashing the firmware image, it will check that the hash encrypted in the signature matches the hash it calculated, using the known public key.
If the hash in the signature matches, then the ROM can transfer control of the device over to your firmware image.
If your device doesn't need to load any more code, the trust stops here. Assuming your software is perfect, you can make guarantees that only the code you desire runs on the device. We know that's not the case though. You should follow some of our recommendations to at least make it harder for untrusted code to run.
But, if this stage of the firmware loads code, you need to perform the exact same checks the bootloader did. Every chunk of code that is to run with any degree of privilege needs to have its origin verified. You can trust that the current firmware load has not been tampered with, including data in the load. This means you can use different keys from that programmed into the device. This could maybe represent different trusted code providers.
The Chain of Trust
A chain of trust is the chain of attestation that the software running is what the vendor intended it be. A transient property follows. When you verify code against a set of trusted keys, you know it can be at least as trusted as the module that loaded it. Generally, as you load code further down the chain of trust, you want that newly loaded code to have less and less privilege. The chain of trust guarantees that the device and privilege setup has been performed correctly. Authenticating and validating the code loaded at each link in the chain is crucial.
For example, verifying the trustworthiness of the kernel is the bootloader's job. The bootloader will verify the kernel's signature, load, then run it. The kernel's job is to set up and attest for the environment that processes run in. These processes will run at much lower privilege level than the kernel. But, the kernel may run at the same level of trust as the boot loader.
This is hardly a perfect solution. Anywhere a user can control inputs, there is risk. Bugs can crop up anywhere, enabling an attacker to break a chain of trust. Verification of software is crucial at this stage. But building a chain of trust at least lets you make guarantees that software further up the chain did its job. So when the kernel tells a task it is secure and locked down, it can believe it, and operate under that assumption.