A side-channel analysis of the Arm CryptoCell-310 AES

This blog post presents an end-to-end side-channel analysis of the AES encryption performed by the Arm CryptoCell-310 (Arm CC310), the cryptographic coprocessor embedded in the Nordic nRF52840.
I do not claim to have broken anything. Neither Nordic nor the Arm CC310 documentation claims any side-channel resistance for this IP. Still, it is somewhat surprising that there is, to my knowledge, no public work describing a side-channel attack against the Arm CC310.
This description shows that recovering the full key requires a few non-trivial side-channel techniques. So this should be read as an example of side-channel methodology rather than an exploit. All findings were shared with the Nordic team and this blogpost was reviewed before this publication.
This analysis was carried out with esDynamic, eShard’s commercial platform.
1. Target
1.1 Hardware
The target is a Nordic nRF52840 development kit. I first tried to collect power fluctuations from the on-board current-sensing pins, but the signal was very noisy. So I switched to electromagnetic (EM) measurements, using a Langer RF-B 3-2 probe coupled with a PA303 amplifier.
The probe was positioned by feel, without a spatial cartography step. The results presented in the next sections could therefore likely be improved by mapping the surface of the chip to maximize the leakage, but this first placement was good enough for my purpose and I did not push further.
The acquisitions were performed with our “old” LeCroy WaveRunner 640Zi, a high-end 8-bit oscilloscope. The sampling rate was set to 1 GS/s, with the oscilloscope’s built-in 200 MHz low-pass filter enabled. A set of about 8 million EM traces was acquired. In the attacks below I used “only” 5 million of them, which was enough to illustrate the method while keeping the computations faster.

1.2 Software
As a lazy expert I asked Claude to write a small firmware that performs AES encryptions using the Arm CryptoCell-310 implementation. The key is hard-coded in the firmware, and a simple command requests the encryption of one or several plaintext blocks.
The encryption is offloaded to the dedicated Arm CC310 hardware engine. The firmware sets up the context, loads the key (this happens outside the measured window), then starts the hardware encryption and waits for it in a polling loop until completion. A GPIO (P0.28) is raised when the encryption starts and lowered when it ends. This GPIO signal is used as the oscilloscope trigger, so the captured window brackets only the hardware encryption itself, not the key setup.
#include "sns_silib.h" // SaSi_LibInit()
#include "ssi_aes.h" // SaSi_Aes*
// --- bring up the CC310 ---
NRF_CRYPTOCELL->ENABLE = 1; // power up the accelerator
NVIC_EnableIRQ(CRYPTOCELL_IRQn); // AesFinish waits on this IRQ
SaSi_LibInit(); // check return code
// --- encrypt one block (AES-128-ECB) ---
SaSiAesUserContext_t ctx;
SaSiAesUserKeyData_t kd = { .pKey = key, .keySize = 16 };
SaSi_AesInit(&ctx, SASI_AES_ENCRYPT, SASI_AES_MODE_ECB, SASI_AES_PADDING_NONE);
SaSi_AesSetKey(&ctx, SASI_AES_USER_KEY, &kd, sizeof(kd)); // key schedule (outside trigger)
size_t out = 16;
TRIGGER_HIGH(); // P0.28 high
SaSi_AesFinish(&ctx, 16, plain, 16, cipher, &out); // the hardware AES runs here
TRIGGER_LOW(); // P0.28 low
SaSi_AesFree(&ctx);To confirm that the encryption really goes through the hardware engine and not a software fallback, the firmware image was inspected: the data path writes the Arm CC310 core registers (in the 0x5002B000 to 0x5002BFFF range, reached through the NRF_CRYPTOCELL peripheral at 0x5002A000).
2. Synchronization
As shown in Figure 2, there is jitter in the clock. The signal is well aligned around the trigger, then drifts more and more out of sync as the execution progresses.

Because the Arm CC310 AES runs on its dedicated engine rather than as a visible instruction stream on the main CPU, there is no guarantee that the large, clearly visible peaks in the EM signal are synchronous with the AES rounds. I have nothing better to align on, though. It is worth noting that Nordic does not publish a separate operating frequency for the Arm CC310: the block diagram places it on the 64 MHz AHB bus, the same clock domain as the Cortex-M4F CPU, with internally managed per-engine clock domains. A spectrum of the raw signal does show a strong component around 64 MHz, which is at least consistent with that. I therefore decided to synchronize on those large, visible peaks.
The synchronization is then fairly simple, in two steps. First, the large peaks are detected. Second, around each peak a window is extracted, sized to about 0.6 times the spacing to the neighboring peaks, which keeps a small margin on each side.

After applying this method, the signal is now well aligned on these large peaks, as shown in Figure 4.

3. Characterization and reverse
The first step is to look for plaintext and ciphertext leakage with the aim to bound the AES execution in time. For this, classical parameters are used: a Pearson correlation distinguisher with a Hamming-weight model. The plaintext leaks earlier in the trace and the ciphertext later, which locates the encryption between the two and confirms its direction.

These results let me restrict the analysis to a reduced frame (samples 10,500 to 12,500), which mainly serves to speed up the subsequent computations.
I then spent a few hours looking for leakage on all the intermediate steps of the AES, a known-key reverse and characterization phase. This included Hamming-distance models as well, between different bytes of the same state and between different states (for example before and after SubBytes).
After this first analysis, the main leakage appears at the output of MixColumns. There is also some leakage after AddRoundKey, but it is mixed with plaintext leakage, which makes it difficult to exploit (I tried the attack). I also tried an attack on the SubBytes output, just to be sure not to miss anything obvious, without success.
Note that I focus here on the leakage during the first round. This work is a preliminary step toward attacking a firmware decryption that uses AES in counter mode, so with unknown outputs.
The MixColumns-output leakage is not the most convenient to attack. It leaves two options. The first is a guess on 32 bits, which, even well optimized, remains a fairly long attack. The second is to use acquisitions with fixed plaintext bytes, an approach described by Vasselle and Wurcker (see references). I already had a dataset and did not want to acquire a new one, and a chosen-input acquisition does not really match my final use case anyway. So I explored a third way.
4. Collision-correlation (CoCo for short)
I explored an attack that exploits any first-round leakage occurring before MixColumns: collision-correlation, as introduced by Moradi, Mischke and Eisenbarth (see references). This attack does not recover the key directly, but it recovers the deltas (the XOR) between different key bytes.
The only constraint is that the leakages of the different bytes occur at the same time, that is, on the same samples. The original attack splits and regroups several time frames, but that did not look feasible with my signal. The MixColumns leakage tends to show that several bytes are processed in parallel, so the attack should be applicable and should work.
Once I understood that the exploitable leakages are "per column", the attack does recover the distances between the bytes of a single column. In the end I recovered 17 of the 24 key distances (24 = 4 × 6, where 6 is the number of pairwise distances within a column of 4 bytes). Figure 6 shows the result for the delta between bytes 0 and 15. The attack does not work perfectly, though, and some results are incorrect, as in Figure 7 for bytes 0 and 5.


An important point is that there is redundancy in the returned information, which can be used to consolidate the results. For each MixColumns column, the attack measures the six deltas between the four byte pairs, but these six values are not independent: they satisfy XOR relations (for instance Δ_bc = Δ_ab ⊕ Δ_ac, see Figure 8).

The collision-correlation attack estimates the six deltas independently and with noise, which gives mutually inconsistent results. Re-imposing the XOR constraints fuses the six noisy distributions into three consistent ones that link the four bytes together. The consolidated distributions are better, which lowers the rank of the correct deltas.
The correct combination of deltas does not always come out first, even after consolidation. Using SKEA (see references) we can compute the rank of the correct combination of deltas for each column, out of the 256³ possible combinations: column 1 ranks 8, column 2 ranks 35, column 3 ranks 0, column 4 ranks 0. This is overall a good result, we have material to carry on the attack.
5. Attacking the MixColumns output
We have now recovered enough information about the key to attack the MixColumns output in a reasonable time. The strategy is the following.
We consider that the consolidated probabilities from the CoCo attack are not perfect. Rather than taking only the best candidate for the deltas between key bytes, we explore a little. The exploration algorithm is SKEA and the exploration depth is set, arbitrarily, to 64 (I like powers of two).
For each column we also need to guess the first key byte k_a, which, together with the deltas, allows the full column to be reconstructed and the MixColumns output to be computed. We therefore have, per column, 64 × 256 = 16,384 hypotheses, which is far more reasonable than the 2³² initially required for a direct attack on the MixColumns output.
Finally, I noticed during the characterization phase that the MixColumns output leaks per full column, that is, per 32-bit word. So instead of correlating each output byte independently, I correlate the 32-bit Hamming weight of the whole column.
def mxc_output_column_0(plaintext, guesses):
column_index = 0
delta_candidates = get_delta_candidates(coco_attack.scores, column_index, n=64)
result = np.zeros((n_candidates, 256, len(plaintext), 1), dtype='uint8')
for guess in guesses:
# reconstruct the column from k_a and the candidate deltas, compute the
# MixColumns output, then take the 32-bit Hamming weight of the column
result[guess] = scared.HammingWeight(nb_words=4)(
compute_mxout_from_deltas(delta_candidates[guess[0]], plaintext, guess[1], column_index)
)
return result # some transposition and reshaping omitted for clarityThis is not exactly the code that was used. For performance reasons, some parts were optimized with Numba-compiled code and parallelism, but the selection function above captures the idea.
And it works rather well. For the first three columns the correct candidate comes out quickly, as in Figure 9.

It is important to note that the convergence traces, which show how the attack result evolves with the number of traces, must be read with caution. Indeed, the MixColumns attack relies on the deltas recovered with the CoCo attack, which were themselves computed on 5 million traces. In fact, around 2 million traces are already enough for the CoCo attack to reduce the key entropy sufficiently to move on to attack step.
There is, however, no leakage on the last column, as shown in Figure 10.

6. Final exhaustive search
Even though there is no result on the last column, very little entropy remains in theory. The deltas for the last column are still expected to be fairly correct (the CoCo attack ranked them well), so the only real unknowns there are the delta exploration depth and the first key byte k_a. After the four Mix Columns-output attacks, the measured remaining entropy of the full key is about 11.3 bits, which is trivially brute-forceable.
I therefore ran a key enumeration with SKEA on the results of the previous MixColumns-output attack. As mentioned earlier, each candidate corresponds, for each column, to a list of three deltas and a candidate for the first key byte. That is all we need to reconstruct the master key and, thanks to one plaintext-ciphertext pair, to test it. This is done by the simple loop below.
skea_mx_attacks = skea_enumerate_keys(mx_attacks_scores) # number of guesses x number of key bytes
plaintext = ets[0].plaintext
ciphertext = ets[0].ciphertext.tolist()
for i, skmxa in enumerate(skea_mx_attacks):
key_candidate = get_key_from_skea(skmxa)
if scared.aes.encrypt(plaintext, key_candidate).tolist() == ciphertext:
print(i, key_candidate)
breakAnd tada, we recover the correct key. It comes out at enumeration index 2,533, which is consistent with the roughly 11.3 bits of remaining entropy (2,533 is close to 2¹¹·³). The recovered key is b5 51 b0 84 03 c3 50 e7 d7 50 58 43 42 ea 6b 37.
7. Conclusion
We have walked through a complete workflow, from acquisition to the final attack, for a grey-box side-channel analysis. I controlled the inputs and the key, and I had a clean trigger, but I had no details on the exact implementation of the AES inside the Arm CC310.
Although it is not protected, the Arm CC310 turned out to be non-trivial to attack. The signal is not obvious, and you do not see a clean AES pattern. There is no leakage on the SubBytes output, the usual target for easy attacks on AES.
By focusing on first round leakages, we managed to retrieve the whole secret. It took an order of a few million traces to recover the key, with the collision-correlation step being the most data-hungry part of the chain.
I have no doubt that many optimizations are possible to improve this attack. The goal was to share an original attack path, and to our knowledge the first successful attack targeting the Arm CC310.
References
- A. Moradi, O. Mischke, T. Eisenbarth. Correlation-Enhanced Power Analysis Collision Attack. CHES 2010, LNCS 6225, pp. 125-139. Extended version: IACR ePrint 2010/297. https://eprint.iacr.org/2010/297
- A. Vasselle, A. Wurcker. Optimizations of Side-Channel Attack on AES MixColumns Using Chosen Input. IACR ePrint 2019/343. https://eprint.iacr.org/2019/343
- SKEA: Fast and Memory-Efficient Key Recovery in Side-Channel Attacks. IACR ePrint 2015/795. https://eprint.iacr.org/2015/795.
