The ubiquitous i.MX6 Ultra Lite is high performance processor that offers a wide range of hardware security features such as ARM TrustZone, High Assurance Boot (HAB) and a Cryptographic Acceleration and Assurance Module (CAAM). The exact features and details depend on the specific part and are documented in detail in the Security Reference Manual (locked behind a license agreement and subject to FAE approval). In this post we’ll be looking at a feature known as the Bus Encryption Engine (BEE) and how we can make use of it from Linux.
The Bus Encryption Engine (BEE), as described in the Technical Reference Manual, allows you to perform on-the-fly encryption/decryption of data being written to, or read from one of it’s memory windows. For example, if you write to one of it’s windows, the BEE module will encrypt the data and then write it out to a pre-programmed memory region. The typical use-case is to encrypt RAM and in doing so make it much more difficult for an attacker with physical access to the device to read or tamper with the data.
The IMX version of U-Boot contains a ‘bee’ command that can be used to configure and test bee – as these sources are public we can gain a little insight into the BEE module, its capabilities and limitations just by examining the code. The ‘bee’ command can be enabled via the ‘CONFIG_CMD_BEE’ config option. Let’s first see what we can do with the ‘bee’ command.
=> bee BEE disabed in fuse!
The BEE functionality is gated behind a fuse – if the fuse is burnt (set) then the feature is unavailable. Depending on the specific i.MX6-UL part you have, this specific fuse may have already been burnt by NXP before reaching you. Let’s try again on hardware with BEE support:
=> bee bee - BEE function test Usage: bee init [key] [mode] [start] [size] - BEE block initial key: 0 | 1, 0 means software key, 1 means SNVS random key mode: 0 | 1, 0 means ECB mode, 1 means CTR mode start: start address that you want to protect size: The size of the area that you want to protect start and end(start + size) addr both should be 64KB aligned. After initialization, the mapping: 1. [0x10000000 - (0x10000000 + size - 1)] <---> [start - (start + size - 1)] Here [start - (start + size -1)] is fixed mapping to [0x10000000 - (0x10000000 + size - 1)], whatever start is. 2. [0x30000000 - (0x30000000 + IRAM_SIZE - 1)] <---> [IRAM_BASE_ADDR - (IRAM_BASE_ADDR + IRAM_SIZE - 1)] Note: Here we only use AES region 0 to protect the DRAM area that you specified, max size SZ_512M. AES region 1 is used to protect IRAM area. Example: 1. bee init 1 1 0xa0000000 0x10000 Access 0x10000000 - 0x10010000 to protect 0xa0000000 - 0xa0010000 2. bee init 1 1 0x80000000 0x20000 Access 0x10000000 - 0x10020000 to protect 0x80000000 - 0x80020000 Default configuration if only `bee init` without any args: 1. software key 2. ECB mode 3. Address protected: Remapped Region0: PHYS_SDRAM - PHYS_SDRAM + SZ_512M Remapped Region1: IRAM_BASE_ADDR - IRAM_BASE_ADDR + IRAM_SIZE 4. Default Mapping for 6UL: [0x10000000 - 0x2FFFFFFF] <-> [0x80000000 - 0x9FFFFFFF] [0x30000000 - 0x3001FFFF] <-> [0x00900000 - 0x0091FFFF] bee test [region] - BEE function test region: 0 | 1, 0 means region0, 1 means regions1
As you can see from the command output, we can use the command to initialise BEE via the ‘bee init’ command, and we can test BEE with via ‘bee test’ command. The ‘bee init’ command gives us options to specify the type of key (random or specified), as well as the counter mode – we can also specify the size and address of the area we want BEE to read/write data from/to. Let’s give this a go:
=> bee init 1 1 0x87800000 0x1000 BEE is settings as: CTR mode, SNVS HW 256 key Access Region 0x10000000 - 0x10000fff to protect 0x87800000 - 0x87800fff Do not directly access 0x87800000 - 0x87800fff Access Region 0x30000000 - 0x3001ffff to protect 0x900000 - 0x91ffff Do not directly access 0x900000 - 0x91ffff
We’ve asked BEE to create a mapping from the fixed window at 0x10000000 such that reads/writes are directed to somewhere in RAM (0x87800000). Incidentally the command also creates a mapping for the IRAM, but we’re going to ignore that.
We can use the memory utility commands to verify this mapping. We’ll start by writing a random (secret) byte value (0xaa) to our window (0x10000000) – we can verify that after writing to the window the value at address 0x87800000 (RAM) changes. This value is the 0xaa secret byte value encrypted and stored in RAM.
=> md.b 0x87800000 1 # Read what's already in the RAM window 87800000: b8 . => md.b 0x10000000 1 # Read what's already in the BEE window 10000000: ef . => mw.b 0x10000000 0xaa # Write some data to the BEE window => dcache flush => md.b 0x87800000 1 # Read the RAM window (it's changed) 87800000: fd . => md.b 0x10000000 1 # Read the BEE window (the value we wrote) 10000000: aa .
As the processors view of address space is through the cache, we need to flush the cache after writing to the BEE window in order to observe changes made to the RAM region. This is because the cache doesn’t have any knowledge that our RAM region is an alias or is related to the BEE window.
We can also verify BEE by changing the encrypted RAM region to see the effect on the BEE window, let’s write a value in RAM:
=> mw.b 0x87800000 0x11 # Write to the encrypted RAM window => dcache flush => md.b 0x87800000 1 # Read it back (it's as expected) 87800000: 11 . => md.b 0x10000000 1 # Read the BEE window (it's changed) 10000000: 46 .
This time, we can see that the BEE window value updates – thus further demonstrating the relationship between the RAM and BEE windows with an encryption function in between.
This ‘bee’ command provides a great way to store sensitive data in RAM such that it is transparently encrypted – but how can be make use of this in Linux? An obvious way would be to run Linux from an area of RAM that is encrypted by BEE: By hacking the kernel I’ve reached a point where it will attempt to mount the root file-system, however there are numerous challenges with this approach thus making it impractical.
However, what is possible is to write a driver that gives user-space access to encrypted RAM. Achieving this is trivial and simply involves porting the U-Boot ‘bee’ command to a miscellaneous driver in Linux. The driver exposes a mmap interface that gives user-space access to the BEE’s window. The BEE peripheral is configured at start of day to map to an area of RAM that Linux won’t use.
With the approach presented here it becomes possible for a user application to make use of an area of RAM that is encrypted by BEE.