STM32L4: fix byte-swapped IV writeback in mbedTLS AES-CBC decrypt - #600
STM32L4: fix byte-swapped IV writeback in mbedTLS AES-CBC decrypt#60061ca52 wants to merge 3 commits into
Conversation
|
Nice find on this! Do you think it would be possible to add a greentea test that reproduces the issue, either in this PR or in a subsequent one? I'd rather have a regression test for this type of issue on other targets if possible. |
|
Also, by the way, we really appreciate all the fixes you've contributed over the last few weeks! If you are interested in contributing to Mbed CE long-term and in having a say in future development, we'd love to invite you to our Discord server and add you to the GitHub organization. Sound good? |
|
Thanks, and that request turned out to be a good one, because writing the test immediately found a second, independent defect in the same function. The now to this PR added regression test ( With it I found that the /* current output is the IV vector for the next encrypt */
memcpy(iv, output, 16);Reproduced to the byte against the values of the appendix under F.2.1 NIST SP 800-38A (https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf) with a I assume that both defects survived this long since The split shape decides detection, and the two defects have different profiles. The Beyond the output comparison, the suite also asserts the documented contract directly, that on return Run on three boards:
greentea-log-l4a6-aes-cbc.txt And other targets seems to be affected by the encrypt defect as well!
mbed-os/connectivity/drivers/mbedtls/TARGET_STM/aes_alt.cpp:457-469 } else {
if (HAL_CRYP_Encrypt(&ctx->hcryp_aes,
(uint32_t *)input,
length,
(uint32_t *)output,
ST_CRYP_TIMEOUT) != HAL_OK) {
ret = MBEDTLS_ERR_PLATFORM_HW_ACCEL_FAILED;
goto exit;
}
/* current output is the IV vector for the next encrypt */
memcpy(iv, output, 16);
}Note this is a different decrypt bug from the L4 one. The same pattern is also present in STs current Cube examples, for instance STM32CubeH7 This is code reading only, as I do not own any of the affected ST boards. The only other ST part I have is a U585I, and the crypto support for it is currently missing in mbed-ce, so it does not reach this driver. If anyone with a DISCO_L562QE, NUCLEO_WB55RG or any F4/F7 board can flash this test suite against current master, that would confirm it in a few seconds. I would then send the To be sure, since I am a little bit surprised that this has survived this long and is still present in the STM ecosystem, I looked at implementations of other vendors. The Silicon Labs driver implements exactly the corrected pattern, written independently: mbed-os/connectivity/drivers/mbedtls/TARGET_Silicon_Labs/aes_aes.c:247-253 if ( length >= 16 )
{
if ( mode == MBEDTLS_AES_ENCRYPT )
memcpy( iv, &output[length-16], 16 );
else
memcpy( iv, tmpIv, 16 );
}Two things are worth noting there. The decrypt IV is saved before the operation, which is necessary since For the EFM32GG_STK3700 target i needed to add Yes, I would like to contribute to mbed-ce in the long term. However, I cannot guarantee fix regularity as I will only be able to contribute as much time as my free time allows. However, I would be happy to do so and would like to be invited. |
|
Sounds good, what's your Discord username? |
| { | ||
| run_all_splits("AES-128-CBC encrypt", MBEDTLS_AES_ENCRYPT, AES128_KEY, 128, | ||
| PLAINTEXT, AES128_CIPHERTEXT); | ||
| } |
There was a problem hiding this comment.
Hmm, fwiw, there is an easier way to do this with the Greentea test framework. You could instead define run_all_splits like:
template<int mode, unsigned char *key, unsigned int keybits, const unsigned char *input, const unsigned char *expected>
static void run_all_splits()
{
...and then in the test cases block have:
Case("AES-128-CBC (enc) call all splits", run_all_splits<MBEDTLS_AES_ENCRYPT, AES128_KEY, 128, PLAINTEXT, AES128_CIPHERTEXT>),I'd say this is the more idiomatic way to do multiple different test cases that use the same test code with different parameters. But it also works fine the way it is now so up to you.
|
Also wait you have a EFM32GG_STK3700 to test with?? That's been on my list to try but it's a bit pricey. I am glad to hear that Mbed CE still actually works on that target, and if you have time, I'd appreciate if you could contribute an upload method file for it! |
|
I do have a NUCLEO_WB55RG to test with but I am in the middle of moving and it's packed away. I trust you to make the fix for now, and I will run the tests on it when I get a chance in a month or so. |
| * Targets with MBEDTLS_AES_ALT (like the STM32 AES drivers) may rewrite this, | ||
| * and may get this wrong. | ||
| * | ||
| * Plain MBEDTLS API level. Equally valid on software-only targets. |
There was a problem hiding this comment.
Can you add a copyright (you can copyright it to yourself) and an SPDX-License-Identifier here? That's why CI is failing.
Summary of changes
mbedtls_aes_crypt_cbc()inconnectivity/drivers/mbedtls/TARGET_STM/aes_alt_stm32l4.csaves the chained IV for the next call by reading the AES peripherals IVR3 through IVR0 registers directly. The STM HAL writes those registers byte-reversed relative to the callers IV buffer, but the read back does not reverse them again. The saved IV is then byte-swapped within each 32-bit word.Therefore, any CBC decryption split across more than one call to the
mbedtls_aes_crypt_cbc()function returns wrong plaintext. This issue can be resolved by applying__REV()to the readback, ensuring that the write and read paths are consistent.For further clarification, here is how the L4 HAL writes to and reads from the IVR3 through IVR0 registers:
mbed-os/targets/TARGET_STM/TARGET_STM32L4/STM32Cube_FW/STM32L4xx_HAL_Driver/stm32l4xx_hal_cryp.c:1586-1592
mbed-os/targets/TARGET_STM/TARGET_STM32L4/STM32Cube_FW/STM32L4xx_HAL_Driver/stm32l4xx_hal_cryp_ex.c:1865-1875
Impact of changes
This will only affect decryption on STM32L4 devices. The encrypt branch already saves the IV correctly, by copying the last ciphertext block out of the
outputbuffermemcpy(iv, output, 16), so it never touches the IVR registers and is unaffected. And what makes this so difficult to catch it only affects chained or streaming use. A single self-contained call never consumes the IV it wrote back. So the corruption is partial, only the first 16-byte block of each follow-up call is wrong. Later blocks in that call chain correctly off the input ciphertext, so the failure looks like intermittent or partial corruption and not like a clean break.All other STM32 devices are out of scope and rely on
aes_alt.c.Migration actions required
Documentation
Pull request type
Test results
As there is no greentea test for this, it was verified using the mbed TLS AES self-test function (
mbedtls_aes_self_test(1);) and a NUCLEO-L4A6ZG.Output before and after: