Recently, we’ve had an opportunity to examine the STSAFE-A110, a secure element produced by ST Microelectronics. Secure elements are specially hardened processors offering security services, uniquely safeguarded through their physical and logical separation from potentially harmful software running on the main CPU.
A secure element might provide services such as:
- Establishing a root of trust
- Storing and verifying sensitive data, including cryptographic keys and certificates
- Securely generating cryptographic material
- Authenticating messages or peripherals
Nowadays, secure elements and secure co-processors can often be found integrated into the main CPU’s die. While this enhances security, it also makes experimentation more challenging. Fortunately, the STSAFE-A110 module is a physically separate package with an external I2C interface, which greatly simplifies the requirements for intercepting and analyzing communication.
As expected, the firmware for STSAFE-A110 is not publicly accessible due to the high level of protection typically associated with secure elements.
In this post, we will not attempt to access this firmware.
Instead, we’ll utilize the source code of drivers and a sample program provided by ST Microelectronics.
However, due to licensing limitations, the included code snippets in this blogpost are IDA-generated pseudocode, taken against a binary-only version of the
all_use_cases sample application.
Because of this there might be superficial differences between the code in this post and the original source code.
all_use_cases sample program allows us to interact with the STSAFE-A110 and generate a new asymmetric key pair.
To further enhance our exploration, we’ll also use a debugger and a logic analyzer to monitor the communication between the CPU and the STSAFE-A110.
This post won’t cover all the features of STSAFE-A110; instead, we’ll focus on a selection of the more intriguing features. Our goal is to better understand the communication protocol and commands supported by the secure element.
To communicate with the secure element, a main MCU and some peripherals are required. For this purpose, we’ve selected the B-L475E-IOT01A devkit, which utilizes the STM32L4S5VIT6 MCU - when we refer to “local host” or “host”, this is the chip we’re talking about. We also have our secure element sitting on a X-NUCLEO-SAFEA1 expansion board. Finally, we have our trusty Saleae Logic 8 logic analyser.
The STM32L4 family of chips is a good starting point for our investigation for the following reasons:
- It is widely available
- It is easy to prototype using its devkit
- Development is quick due to its IDE with a built-in
- A full stack of easily accessible CMSIS, drivers, middleware, and sample applications
- There is good support for the STSAFE-A110 in the form of drivers, middleware, and sample projects, which double as reference implementation for developers
B-L475E-IOT01A development kit has all the peripherals we need for our experiments, including an STSAFE-A110 chip already on the board.
However, just to make our life easier we used a X-NUCLEO-SAFEA1 expansion board.
This is going to be useful mainly because it’s Arduino-compatible, meaning all the pins from the secure element are exposed on the board via female headers.
We will make good use of this to sniff the I2C communication using a logic analyser.
In order to get a taste of the full STM experience, we opted to use the STM32 toolchain. It consists of 4 unique software components:
- STM32CubeMX - a GUI tool to automatically generate the initialisation code for STM32 microcontrollers using a wizard. This is used when developing STM32 LL/HAL applications from scratch. We’re not going to need this one right now because we’re using the sample projects, which bundle the initialisation code for our boards.
- STM32CubeProg - a GUI and CLI programmer with support to read/write device memory via JTAG, SWD as well as bootloader interfaces.
- STM32CubeMonitor - a family of GUI tools to monitor variables in real-time on the device for application profiling.
- STM32CubeIDE - a custom Eclipse IDE, with full support for C, a builtin GDB debugger that works seamlessly with the ST-LINK/V2-1 debugging chip on the board.
Because of its ease of use and good integration to the ecosystem, including support for the on-board debug port using
gdb, we are using this IDE for the post.
To configure our demo app we did the following:
- Unpack the
- Select the root directory of the
- Import a sample project, such as the
General->Existing Projects into Workspace
- Build the project:
- Run the build with a debugger (gdb):
- Note: In
Project Explorerthe cursor should be on the
.elfbinary otherwise an error pops up
- Note: In
The sample program we just flashed onto the board using these steps implement all the main flows the STSAFE-A110 supports:
- Extracting and verifying a X509 certificate
- Peripheral authentication
- Key generation and verification - this is the flow we’re going to focus on in this post
- Generate shared secret using ephemeral keys
- Wrapping and unwrapping local envelopes
All these are sample implementations where not all the security features are used. However, this is useful for us because it makes the process of our understanding the flows easier.
To intercept the I2C communication between the secure element and the MCU, we used a Saleae Logic 8 logic analyser. Setting this up is simple thanks to the Arduino-compatible STSAFE-A110 extension board. We just need to connect our SDA, SDC, and a GND wire from our Saleae to the corresponding headers on the board similarly to the pictures below:
Commands are the APIs that the STSAFE-A110 exposes to the main CPU. These commands are the building blocks for the flows that are implemented by the sample program we’re using, explained above. Each command has a code and an expected data structure which needs to be sent by the CPU. Some command codes support flags that can be set by the CPU - in effect by XORing the command code with the appropriate bitmask - which enable certain features, such as message encryption or message signing.
The following idapython script can be used to pull out the various
STSAFEA_CMD_*` constants from firmware or the generated ELF binary:
The result of the script will look like the following, where the hex values correspond to the command codes being sent on the I2C bus:
Next we will have a look at an intercepted stream of messages exchanged during the key generation on the secure element, in order to understand how the communication between the MCU and the secure element works in practice.
In the sample applications, communication with the local host (the main CPU) is unencrypted due to a lack of configured host keys.
Host keys are used for multiple purposes, for example when verifying message authentication signatures, and encrypting the I2C messages.
They are stored in a one-time programmable area on the secure element, meaning once they’re set, they cannot be recovered or replaced.
Additionally, according to ST, if the wrong C-MAC (command authentication signature sent by the CPU) is sent 50 times, the keys become blocked, bricking the STSAFE.
Similarly, if the host fails to store these keys in its own flash somewhere, the secure element will have to be replaced.
In order to be able to intercept I2C messages, and in case we lose the host keys during the many iterations of flashing and executing programs on the CPU, we opted not to set the host keys while preparing this post.
However, in a real-world use case past the prototyping phase, host keys should always be set during the first time the device is programmed (using the
STSAFEA_CMD_PUT_ATTRIBUTE command) in order to ensure that communication between the secure element and the CPU is encrypted and authenticated.
We included comments in the data stream to map the fields of the source code’s
StSafeA_Handle_t handler object to the I2C messages.
StSafeA_Handle_t structure looks like this in decompiled code:
This intercepted stream of I2C data is a result of a - mostly (more on this later) - unmodified run of the
key_pair_generation() function of the
all_use_cases sample application from the
First, the MCU queries the secure element about the host key in slot
0x17, as well as the
HostCMacCounter (more on this later) as part of the
StSafeA_GetHostMacSequenceCounter() STSAFE middleware call using the
This is an important step in terms of security, which we’ll discuss later.
The secure element responds with the
HostKeyPresenceFlag, which is
0 in this fresh development kit, meaning there is no key in this slot.
We could set up host keys by generating the required pair of MAC and cipher keys, and storing them in the one-time writeable sections inside STSAFE (as well as in the flash for the MCU) using the
The amount of bytes returned from the secure element are enough to hold the value of the
HostCMacSequenceCounter should the
HostKeyPresenceFlag not be zero.
Next, the MCU issues the
STSAFEA_CMD_GENERATE_KEY command with the corresponding data structure, as per the notations in the message dump below.
For those of you following along at home, note that for multibyte fields, endianness is swapped for the I2C protocol, in case you want to check these values using a debugger as well:
At this point we should note that the reference code uses a form of message authentication feature, whereby the command code is bit masked with the
However, we have found that if this feature is used as-is, the secure element errors out and does not generate the expected keys.
So for the sake of this walkthrough we have disabled the host CMAC, and we are just sending the command code without masking, which results in keys being generated as expected.
In response to this command, the secure co-processor will generate two new RSA keys and return the public keys:
By default the STSAFE-A110 will include a CRC-16 checksum at the end of messages on the I2C channel (weirdly, the MCU will not).
This will be verified by the MCU in the
StSafeA_MAC_SHA_PrePostProcess() function call:
However, as we know, a CRC code is not meant to protect against malicious modification, as an attacker can just as easily calculate a valid CRC and include it in the message.
For exactly this purpose, the STSAFE-A110 can also include an AEC C-MAC (or R-MAC to authenticate responses from the STSAFE) in order to protect the data sent to and from the STSAFE on the I2C channel.
For this feature to work, the secure element needs to be paired with the local host using the following flow, using the
For the sake of brevity, we are going to skip over this process on a source code level.
Interestingly, the C-MAC will only modify the command code by bitwise OR’ing with the shared MAC and the
STSAFEA_CMD_HEADER_MAC_MSK constant, as it can be seen in the code snippet below:
This means that all other fields of the command data structure will remain unsigned.
Also, as part of the MAC calculations, the local host and the secure element will keep track of the communication flow by a Host MAC sequence counter, in order to protect against commands injected into the I2C channel by malicious third-parties:
If a MAC is used, the same process is performed in the reverse when the local host receives a message from the secure element, in the post-processing step:
Local host pairing (I2C bus encryption)
Some commands, such as the
STSAFEA_CMD_GENERATE_SIGNATURE - among others - also support encryption for the full data stream on the I2C bus.
This encryption scheme is granular, where developers can choose to encrypt only the response, the command, both, or neither.
The encryption scheme is annoyingly strong, using an AES-CBC cipher, where even the IV is generated according to cryptographic best practices by encrypting a counter - our old friend, the
This is an important step because the AES-CBC key will be reused between commands:
The full list of commands that support command/response encryption can be found in the STSAFE Middleware API,
STSAFE has some interesting optimisation options, which could have an effect on the security of the device.
One such optimisation is the use of the
It is explained in the documentation as follows:
Set to 1 to optimize RAM usage. If set to 1 the
StSafeA_Handle_t.InOutBufferused through the Middleware APIs is shared with the application between each commad & response. It means that everytime the MW API returns a
TLVBufferpointer, it returns in fact a pointer to the shared
StSafeA_Handle_t.InOutBuffer. As consequence the user shall copy data from given pointer into variable defined by himself in case data need to be stored. If set to 0 the user must specifically allocate (statically or dynamically) a right sized buffer to be passed as parameter to the Middleware command API.
If this flag is set to 1, such that RAM usage is optimised, it could lead to memory management vulnerabilities such as:
- Time-of-check-to-time-of-use (TOCTTOU) - if the contents of the
TLVBufferare not correctly verified between commands (or responses) and the application (or STSAFE) uses the data from it when another command could’ve overwritten all or parts of it. Of course verifying this is easier on the local host, when a response is handled, because how the STSAFE handles the
TLVBufferis a black box.
- Buffer overflows - again, if the data in the reused
TLVBufferis not handled correctly, for example by verifying the length field against the actual length of the data pointer, during memory copy operations, this could lead to buffer overflow vulnerabilities.
We hope this quick runthrough of the key pair generation flow has helped the reader to understand how a secure element - and specifically the STSAFE-A110 - works under the hood. We are also releasing a Logic analyser plugin to help understand the I2C messages, which can be found here. At the moment it is only able to decode the flow described in this blog post, but pull requests with additional features are welcome.