F-9750: decrypt the stored block before the encrypted RMW patch

Both partial-block read-modify-write paths in ext_flash_encrypt_write
read the stored block (ciphertext), spliced the new plaintext in, and
re-encrypted the whole block. The untouched bytes were therefore
XOR'd with the keystream a second time: stored ciphertext came back
as plaintext in flash, and the next read of those bytes returned raw
ciphertext instead of the original data. Any encrypted update whose
first or last block partially overlaps a block with previous content
- e.g. a retry over a previously written update image - silently
corrupted the neighbouring bytes.

Decrypt the stored block before splicing (into the scratch buffer, so
no backend has to handle in-place decrypt) and re-encrypt the merged
plaintext. Erased (0xFF) bytes round-trip unchanged because the
decrypt/encrypt pair is the identity on the stored value.

The re-encryption re-syncs the stream to the block index first: the
decrypt step consumes keystream, and on the ChaCha/PKCS#11 backends
encrypt and decrypt share a single stream state, while on the AES
backends the decrypt context had not advanced with the full-block
writes. The tail path also syncs the decrypt context, which on the
AES backends sits at the first block's index after the aligned
writes. Fallback-IV offset handling mirrors ext_flash_decrypt_read.

New unit-extflash tests (run under the plain, AES-128, AES-256 and
ChaCha20 variants): a mid-block patch must leave the untouched bytes
of a previously written block intact, a trailing partial block must
leave the rest of a previously written block intact, and a stream
written in small unaligned chunks must round-trip byte for byte. All
three fail on the pre-fix code with every cipher.
pull/868/head
Daniele Lacamera 2026-08-21 03:31:41 +02:00 committed by Daniele Lacamera
parent d64bf67151
commit 5caac27eb9
2 changed files with 141 additions and 0 deletions

View File

@ -2720,7 +2720,19 @@ int RAMFUNCTION ext_flash_encrypt_write(uintptr_t address, const uint8_t *data,
!= ENCRYPT_BLOCK_SIZE) {
return -1;
}
/* The stored block is ciphertext: decrypt it so the untouched bytes
* can be patched as plaintext and re-encrypted. Re-encrypting the
* ciphertext as-is would double-XOR the untouched bytes and store
* them in plaintext. */
crypto_decrypt(enc_block, block, ENCRYPT_BLOCK_SIZE);
XMEMCPY(block, enc_block, ENCRYPT_BLOCK_SIZE);
XMEMCPY(block + row_offset, data, step);
/* The decrypt above consumed keystream on backends that share one
* stream state between encrypt and decrypt; re-sync the stream to
* this block before re-encrypting. */
if (fallback_iv_forced)
encrypt_iv_offset = FALLBACK_IV_OFFSET;
wolfBoot_crypto_set_iv(encrypt_iv_nonce, iv_counter);
crypto_encrypt(enc_block, block, ENCRYPT_BLOCK_SIZE);
ret = ext_flash_write(row_address, enc_block, ENCRYPT_BLOCK_SIZE);
if (ret < 0)
@ -2758,11 +2770,26 @@ int RAMFUNCTION ext_flash_encrypt_write(uintptr_t address, const uint8_t *data,
* the same way the unaligned head above is handled. */
step = sz & (ENCRYPT_BLOCK_SIZE - 1);
if (step > 0) {
/* "address" is block-aligned here; index of the block being patched */
uint32_t tail_iv_counter = (address - WOLFBOOT_PARTITION_UPDATE_ADDRESS) /
ENCRYPT_BLOCK_SIZE;
if (ext_flash_read(address, block, ENCRYPT_BLOCK_SIZE)
!= ENCRYPT_BLOCK_SIZE) {
return -1;
}
/* Sync the decrypt context to this block (on backends with separate
* encrypt/decrypt contexts it did not advance with the full-block
* writes above), then decrypt the stored ciphertext so the untouched
* tail bytes survive the re-encryption. */
if (fallback_iv_forced)
encrypt_iv_offset = FALLBACK_IV_OFFSET;
wolfBoot_crypto_set_iv(encrypt_iv_nonce, tail_iv_counter);
crypto_decrypt(enc_block, block, ENCRYPT_BLOCK_SIZE);
XMEMCPY(block, enc_block, ENCRYPT_BLOCK_SIZE);
XMEMCPY(block, data, step);
if (fallback_iv_forced)
encrypt_iv_offset = FALLBACK_IV_OFFSET;
wolfBoot_crypto_set_iv(encrypt_iv_nonce, tail_iv_counter);
crypto_encrypt(enc_block, block, ENCRYPT_BLOCK_SIZE);
ret = ext_flash_write(address, enc_block, ENCRYPT_BLOCK_SIZE);
}

View File

@ -373,6 +373,111 @@ START_TEST(test_ext_enc_flash_oversized_write) {
}
END_TEST
/* A mid-block patch must not destroy the untouched bytes of a block that
* already holds encrypted content: the read-modify-write has to decrypt the
* stored block, splice the patch in, and re-encrypt. Re-encrypting the
* ciphertext as-is double-XORs the untouched bytes and stores plaintext,
* which then reads back as raw ciphertext. */
START_TEST(test_ext_enc_flash_rmw_head_preserves_neighbors) {
uint32_t address = 0x1000;
uint8_t block_a[TEST_BLOCK_SIZE];
uint8_t block_p[TEST_BLOCK_SIZE];
uint8_t expect[TEST_BLOCK_SIZE];
uint8_t data[TEST_BLOCK_SIZE];
const int off = 4, len = 8;
int i, rres, wres;
for (i = 0; i < TEST_BLOCK_SIZE; i++) {
block_a[i] = (uint8_t)(0xA0 + i);
block_p[i] = (uint8_t)(0xB0 + i);
}
/* Prime the block with known content */
wres = ext_flash_check_write(address, block_a, TEST_BLOCK_SIZE);
ck_assert_int_eq(wres, 0);
/* Patch an unaligned mid-block subrange */
wres = ext_flash_check_write(address + off, block_p, len);
ck_assert_int_eq(wres, 0);
memcpy(expect, block_a, TEST_BLOCK_SIZE);
memcpy(expect + off, block_p, len);
rres = ext_flash_check_read(address, data, TEST_BLOCK_SIZE);
ck_assert_int_eq(rres, TEST_BLOCK_SIZE);
ck_assert_mem_eq(data, expect, TEST_BLOCK_SIZE);
}
END_TEST
/* A write whose trailing partial block lands on a block that already holds
* encrypted content: the untouched tail of that block must survive. */
START_TEST(test_ext_enc_flash_rmw_tail_preserves_neighbors) {
uint32_t address = 0x1000;
uint8_t block_b[TEST_BLOCK_SIZE];
uint8_t payload[2 * TEST_BLOCK_SIZE];
uint8_t expect[2 * TEST_BLOCK_SIZE];
uint8_t data[2 * TEST_BLOCK_SIZE];
const int tail = 8;
int i, rres, wres;
for (i = 0; i < TEST_BLOCK_SIZE; i++) {
block_b[i] = (uint8_t)(0xC0 + i);
payload[i] = (uint8_t)(0xD0 + i);
payload[TEST_BLOCK_SIZE + i] = (uint8_t)(0xE0 + i);
}
/* Two primed blocks */
wres = ext_flash_check_write(address, payload, TEST_BLOCK_SIZE);
ck_assert_int_eq(wres, 0);
wres = ext_flash_check_write(address + TEST_BLOCK_SIZE, block_b,
TEST_BLOCK_SIZE);
ck_assert_int_eq(wres, 0);
/* Aligned write of one full block plus a partial tail into block two */
wres = ext_flash_check_write(address, payload, TEST_BLOCK_SIZE + tail);
ck_assert_int_eq(wres, 0);
/* Block one is fully replaced; block two holds the spliced tail and the
* original bytes beyond it. */
memcpy(expect, payload, TEST_BLOCK_SIZE + tail);
memcpy(expect + TEST_BLOCK_SIZE + tail, block_b + tail,
TEST_BLOCK_SIZE - tail);
rres = ext_flash_check_read(address, data, 2 * TEST_BLOCK_SIZE);
ck_assert_int_eq(rres, 2 * TEST_BLOCK_SIZE);
ck_assert_mem_eq(data, expect, 2 * TEST_BLOCK_SIZE);
}
END_TEST
/* A stream written in small, unaligned chunks must round-trip: every chunk
* boundary exercises the partial-block read-modify-write against content the
* previous chunks already encrypted. */
START_TEST(test_ext_enc_flash_chunked_stream_roundtrip) {
uint32_t address = 0x1000;
const uint32_t total = 3 * TEST_BLOCK_SIZE + 7;
static uint8_t payload[3 * TEST_BLOCK_SIZE + 7];
static uint8_t data[3 * TEST_BLOCK_SIZE + 7];
uint32_t written = 0;
int i, rres, wres;
for (i = 0; i < (int)total; i++)
payload[i] = (uint8_t)(i * 7 + 3);
while (written < total) {
uint32_t chunk = total - written;
if (chunk > 13)
chunk = 13;
wres = ext_flash_check_write(address + written, payload + written,
chunk);
ck_assert_int_eq(wres, 0);
written += chunk;
}
memset(data, 0xA5, sizeof(data));
rres = ext_flash_check_read(address, data, total);
ck_assert_int_eq(rres, (int)total);
ck_assert_mem_eq(data, payload, total);
}
END_TEST
Suite *wolfboot_suite(void)
{
@ -386,6 +491,7 @@ Suite *wolfboot_suite(void)
TCase *ext_enc_flash_short_read = tcase_create("External encrypted flash short unaligned read");
TCase *ext_enc_flash_short_write = tcase_create("External encrypted flash short unaligned write");
TCase *ext_enc_flash_oversized_write = tcase_create("External encrypted flash oversized write");
TCase *ext_enc_flash_rmw = tcase_create("External encrypted flash RMW neighbour preservation");
/* Set parameters + add to suite */
tcase_add_test(ext_flash_operations, test_ext_flash_operations);
@ -396,17 +502,25 @@ Suite *wolfboot_suite(void)
test_ext_enc_flash_short_unaligned_write);
tcase_add_test(ext_enc_flash_oversized_write,
test_ext_enc_flash_oversized_write);
tcase_add_test(ext_enc_flash_rmw,
test_ext_enc_flash_rmw_head_preserves_neighbors);
tcase_add_test(ext_enc_flash_rmw,
test_ext_enc_flash_rmw_tail_preserves_neighbors);
tcase_add_test(ext_enc_flash_rmw,
test_ext_enc_flash_chunked_stream_roundtrip);
tcase_set_timeout(ext_flash_operations, 20);
tcase_set_timeout(ext_enc_flash_operations, 20);
tcase_set_timeout(ext_enc_flash_short_read, 20);
tcase_set_timeout(ext_enc_flash_short_write, 20);
tcase_set_timeout(ext_enc_flash_oversized_write, 20);
tcase_set_timeout(ext_enc_flash_rmw, 20);
suite_add_tcase(s, ext_flash_operations);
suite_add_tcase(s, ext_enc_flash_operations);
suite_add_tcase(s, ext_enc_flash_short_read);
suite_add_tcase(s, ext_enc_flash_short_write);
suite_add_tcase(s, ext_enc_flash_oversized_write);
suite_add_tcase(s, ext_enc_flash_rmw);
return s;
}