For both privacy and security reasons it is important for modern devices to ensure that the software running on the device hasn't been tampered with. In particular any tampering with software early in the boot sequence will be hard to detect later while having a big amount of control over the system. To solve this issues various vendors and consortiums have created technologies to combat this, known under names as “secure boot”, “highly assured boot” (NXP), “verified boot” (Google Android/ChromeOS).

While the scope and implementation details of these technologies differs the approach to provide a trusted boot chain tends to be similar between all of them. This document discusses how that aspect of the various technologies works on a high-level and how this can be introduced into Apertis.

# Boot sequence

To understand how secure boot works first one has to understand how booting works. From a high-level perspective a CPU is a very simple beast, it needs to be pointed at a stream of instructions (code) which it will then be able to execute. Without instructions a CPU cannot do anything. The instructions also need to be in a region of memory which the CPU can access. However when a device is powered on the code that is meant to be run on it (e.g. Linux) will not be in memory yet. To make matters worse on power on main memory (Dynamic RAM) will not even be accessible by the CPU yet! To solve this problem some bootstrapping is required, typically referred to as booting the system.

The very first step in the boot process after power on is to get the CPU to start executing some instructions. As the CPU cannot load instructions without running instructions these first instructions are hardwired into the SoC directly with the CPU is hardwired to start executing those when powers comes on. This hardwired piece of code is often referred to as the ROM or romcode.

The job of the romcode is to do very basic SoC setup and load further code to execute. To allow the romcode to do its job, it will have access to a small amount of static RAM (SRAM, typically 64 to 128 kilobyte). The locations from where the ROM code can load is system specific. On most modern ARM-based systems this will include at least (SPI-connected) flash (NAND/NOR), eMMC cards, SD cards, serial ports etc. Most systems can only have code loaded over USB initially while some can even load code directly over the network via bootp!. The details of the format the code needs to be in (e.g. specific headers), how the code is presented (e.g. specific offsets on the eMMC) is very system specific. Once romcode managed to load the code from one of its supported location into SRAM execution of that code will start, which will the first time user supplied code is actually ran on the device.

This next step is known under various different names such as Boot Loader stage 1(BL1), Secondary Program Loader(SPL), Tertiary Program Loader(TPL), etc. The code for this stage must be quite small as only SRAM is available at this stage. The goal for this step is normally to initialize Dynamic RAM (e.g. run DDR memory training) followed by loading the next step into DRAM and executing it (which can be far bigger now that DRAM is available). Depending on the system this stage may also provide initial user feedback that the system is booting (e.g. display a first splash image, turning an LED on etc), but that purely depends on the overall system design and available space.

What the next step of executed code is more system specific. In some cases it can directly be Linux, in some cases it will be a bootloader with more functionality (as all of main memory is now available) and in some cases it will be multiple loader steps. As an example of the last case for devices using ARM Trusted Firmware there will typically be follow-on steps to load the secure firmware (such as OP-TEE) followed by a non-secure world bootloader which loads Linux. For those interested the various images used in an ATF setup can be found here.

Linux starting up typically is the last phase of the boot process. For Linux to start the previous stage will have loaded a kernel image, optionally a initramfs and optionally a devicetree into main memory. The combination of these will load the root filesystem at which point userspace (e.g. applications) will start running.

Note that while the above is a simple view on the basic boot process, the overall flow will be the same on all systems (both ARM and non-ARM devices). For the above we also implicitly assumed that only one CPU is booted, for some more complex systems multiple CPUs (e.g. main application processors and various co-processors) might be booted. It may even be the case that all the early stages are done by a co-processor which takes care of loading the first code and starting the main processor. The overall description is also valid for system with hypervisors, essentially the hypervisor is just another stage in the boot sequence and will load/start the code for each of the cells it runs.

For this document we'll only look at securing the booting of the main (Linux running) processor without a hypervisor.

# Secure boot sequence

The main objective for a secure boot process is to ensure all code that gets executed by the processor is trusted. As each of the stages described in the previous section is responsible for loading the code for the next stage the solution for that is relatively straight-forward. Apart from loading the next stage of code, each stage also needs to verify the code it has loaded. Typically this is done by some signature verification mechanism.

The ROM step is normally assumed to be fully trusted as it's hard-wired into the SoC and cannot be replaced. How the ROM is configured and how it validates the next stage is highly device specific. Later steps can do the verification either by calling back into ROM code (thus re-using the same mechanisms as the ROM) or by pure software implementation (making it more consistent between different devices).

In all cases to support this, apart from device specific configuration, all boot stages need to be appropriately signed. Luckily this is typically based on standard mechanisms such as RSA keys and X.509 Certificates.

Once Linux starts the approach has to be different as it's not feasible in most systems to fully verify all of the root filesystem at boot time as this simply would take far too long. As such the form of protection described thus far only gets applied up to the point the Linux kernel starts loading.

# Threat models

To understand what a secure boot system really secures it's important to look at the related threat models. As a first step we can distinguish between offline (device is turned off) and online attacks (device powered on).

For these considerations the assumption is made all boot steps work as intended. As with any software security vulnerabilities can invalidate the protection given. While in most cases these can be patches as issues become known, for ROM code this is impossible without a hardware change.

## offline attacks

• Attack: Replace any of the boot stages on device storage (physical access required)

• Impact: Depending on the boot stage the attacker can get full control of the device for each following boot.

• Mitigation: Assuming each stage correctly validates the next boot stage, any tampering with loaded code will be detected and prevented (e.g. device fails to boot).

• Attack: Trigger the device to load software from external means (e.g. USB or serial) under the attackers control.

• Impact: Depending on the boot stage the attacker can get full control of the device.

• Mitigation: The ROM or any stage that loads from an external source should use the same verification as for any on device stages. However for production use, if possible, loading software from external source should be disabled.

• Attack: Replace or add binaries on the systems root filesystem

• Impact: Full control of the device as far as the kernel allows.

• Mitigation: No protection from the above mechanisms.

## online attacks

• Attack: Gain enough access to replace any of the boot stages on device storage

• Impact: Depending on the boot stage the attacker can get full control of the device for each following boot.

• Mitigation: Assuming each stage correctly validates the next boot stage, any tampering with loaded code will be detected and prevented (e.g. device fails to boot).

• Attack: Replace or add binaries on the systems root filesystem

• Impact: Full control of the device as far as the kernel allows.

• Mitigation: No protection from the above mechanisms.

# Signing and signing infrastructure

To securely boot a device it is assumed all the various boot stages have some kind of signature which can be validate by previous stages. Which by extension also means the protection is only as strong as the signature; if an attacker can sign code under their control with a signature that is valid (or seen as valid) for the verifying step all protection is lost. This means that special care has to be taken with respect to key handling to ensure signing keys are kept with the right amount of security depending on their intended use.

For development usage and devices a low amount of security is ok in most cases, the intention in the development stage is for developers to be easily able to run their own code and by extension should be able to sign their own builds with minimal effort.

For production devices however the requirements should be much more strict as unauthorized of control of a signing key can allow attackers to defeat the intended protection by secure boot. Furthermore production devices should typically not be allowed to run development builds as those tend to enable extra access for debugging and development reasons which tend to be a great attack vector.

For these reason it's recommendable to have at least two different sets of signing keys, one for development usage and one for production use. Development keys can be kept with low security or even be publicly available, while production keys should only be used to sign final production images and managed by a hardware security module (HSM) for secure storage. To allow the usage of a commercially available HSMs it's recommended for the signing process to be able to support the PKCS#11 standard.

Note that in case security keys do get lost/stolen/etc it is possible for some devices to revoke or update the valid set of keys. However this can be quite limited e.g. on i.MX6 device one can one-time program up to four acceptable keys and each of those can be flagged as revoked, but it's impossible to add more or replace any keys.

# Apertis secure boot integration

Integrating secure boot into Apertis really exists out of two parts. The first part is to ensure all boot stages have the ability to verify. The second part is to be able to sign all the boot stages as part of the Apertis image building process. While the actual implementation details of both will be system/hardware/SoC specific the impact of this is generic for all.

As Apertis images are composed out of pre-build binary packages the package delivering the implementation for the various boot stages should either provide a build which will always enforce signature verification or the implementation should detect if the device is configured for secure boot and only enforce in that situation. Enforcing on demand has the benefit that it makes it easier to test the same builds on non-secure devices (though care must be taken that secure boot status cannot be faked).

For the signing of the various stages this needs to be done at image build time such that the signing key can be chosen based on the target. For example whether it's a final production build or a development build or even a production build to test on development devices. This in turn means that the signing tools and implementation need to support signing outside the build process which is normally supported.

# Apertis secure boot implementation steps

As the whole process is somewhat device specific implementation of a secure boot flow for Apertis should be done on a device per device basis. The best starting point is is most likely the NXP i.MX6 sabrelite reference board as the secure boot process (Highly Assured Boot in NXP terms) is both well-known and well supported by upstream components. Furthermore an initial PoC for the early boot stages was already done for the NXP Sabre Auto boards which are based on the same SoC.

# SabreLite secure boot preparation

The good introduction into HAB (High Assurance Boot) is prepared by Boundary Devices, also there are some documentation and examples in U-Boot source tree.

The NXP Code Signing Tool is needed to create keys, certificates and SRK hashes used during the signing process – please refer to section 3.1.3 of CST User's Guide. Apertis reference images use the public git repository with all secrets available, so it could be used for signing binaries during development in case if board has been fused with Apertis SRK hash (irreversible operation!!!).

Caution: the SabreLite board can be fused with the SRK (Super Root Key) hash only once!

To fuse the Apertis SRK hash we have to have the hexadecimal dump of the hash of the key. Command below will produce the output with commands for Apertis SRK hash fusing:

## Signing the FIT image

Now it is time to sign the produced image. The procedure is similar to signing U-Boot with additional step – we need to add the IVT (Image Vector Table) for the kernel image. We skip this step for U-Boot since it is prepared automatically during the build of the bootloader.

The IVT is needed for the HAB ROM and must be the part of the binary, it should be aligned to 0x1000 boundary. For instance, if the produced binary is:

 1 2  stat -c "%s" vmlinuz.itb 25555173  we need to pad the file to nearest aligned value, which is 25559040:  1   objcopy -I binary -O binary --pad-to=25559040 --gap-fill=0x00 vmlinuz.itb vmlinuz-pad.itb 

The next step is IVT generation for the FIT image and the easiest method is to use the genIVT script provided by Boundary Devices with adaptation for padded FIT image:

• Jump Location – 0x12000000 Here we expect the image will be loaded by U-Boot
• Self Pointer – 0x13860000 (Jump Location + size of padded image) Pointer to the IVT table itself, which will place after padded image
• CSF Pointer – 0x13860020 (Jump Location + size of padded image + size of IVT) Pointer to signature data, which we will add after IVT

So, the IVT generation is pretty simple:

 1  $perl genIVT  it will generate the binary named ivt.bin to be added to the image:  1  $ cat vmlinuz-pad.itb ivt.bin > vmlinuz-pad-ivt.itb 

We need to prepare the config file for signing the padded FIT image with IVT. This step is absolutely the same as for U-Boot signing.

Configuration file for FIT image is created from template csf_uboot.txt, and values in [Authenticate Data] section must be the same as used for IVT calculation – Jump Location and the size of generated file:

 1 2 3 4  [Authenticate Data] Verification index = 2 # Authenticate Start Address, Offset, Length and file Blocks = 0x12000000 0x00000000 0x1860020 "vmlinuz-pad-ivt.itb" 

At last we are able to sign the prepared FIT image:

## FIT image creation and signing

The FIT image is more complex. So for Apertis we use 2 scripts:

The integration with the build pipeline happens after the kernel is installed by the OSTree commit recipe by adding the step below:

 1 2 3   - action: run description: Generate FIT image script: scripts/generate_fit_image.sh 

NB: this action must be done prior to ostree commit action to add the signed FIT kernel into OSTree repository for OTA upgrades.

# As next steps the following could be undertaken:

• Integration of PCKS#11 support in the signing process to support HSM devices
• Automated testing of secure boot if possible