Merge branch 'feature/add_data_partition_secure_boot_verification' into 'master'

feat(secure_boot): adds api to verify data partition integrity

Closes IDFGH-16339

See merge request espressif/esp-idf!41721
This commit is contained in:
Mahavir Jain
2026-03-05 09:44:15 +05:30
16 changed files with 286 additions and 32 deletions
+1
View File
@@ -40,6 +40,7 @@ repos:
- id: check-executables-have-shebangs
- id: mixed-line-ending
args: ['-f=lf']
exclude: *whitespace_excludes
- id: double-quote-string-fixer
- id: no-commit-to-branch
name: Do not use more than one slash in the branch name
+4
View File
@@ -9,6 +9,10 @@ idf_component_register(SRCS "esp_ota_ops.c"
REQUIRES partition_table bootloader_support esp_app_format esp_bootloader_format esp_partition
PRIV_REQUIRES esptool_py efuse spi_flash)
if(CONFIG_SECURE_SIGNED_DATA_PARTITION)
idf_component_optional_requires(PRIVATE mbedtls)
endif()
if(NOT BOOTLOADER_BUILD)
partition_table_get_partition_info(otadata_offset "--partition-type data --partition-subtype ota" "offset")
partition_table_get_partition_info(otadata_size "--partition-type data --partition-subtype ota" "size")
+9
View File
@@ -0,0 +1,9 @@
menu "App Update config"
config SECURE_SIGNED_DATA_PARTITION
default n
bool "Require signed Data partition images"
depends on SECURE_SIGNED_ON_UPDATE_NO_SECURE_BOOT || SECURE_SIGNED_APPS
help
If set, the Data partition images will be verified during OTA updates.
Only partitions with subtype ESP_PARTITION_SUBTYPE_DATA_UNDEFINED will be verified.
endmenu # App Update config
+89
View File
@@ -31,6 +31,9 @@
#include "esp_bootloader_desc.h"
#include "esp_flash.h"
#include "esp_private/esp_flash_internal.h" //For dangerous write protection
#if CONFIG_SECURE_SIGNED_DATA_PARTITION
#include "psa/crypto.h"
#endif // CONFIG_SECURE_SIGNED_DATA_PARTITION
#define OTA_SLOT(i) (i & 0x0F)
#define ALIGN_UP(num, align) (((num) + ((align) - 1)) & ~((align) - 1))
@@ -470,6 +473,81 @@ esp_err_t esp_ota_abort(esp_ota_handle_t handle)
return ESP_OK;
}
#if CONFIG_SECURE_SIGNED_DATA_PARTITION
#define SHA_CHUNK 256
static esp_err_t ota_calc_partition_bin_sha(const esp_partition_t *partition, uint32_t length, uint8_t out_digest[ESP_SECURE_BOOT_DIGEST_LEN], psa_algorithm_t alg)
{
esp_err_t err = ESP_OK;
psa_hash_operation_t hash_operation = PSA_HASH_OPERATION_INIT;
uint8_t sha_buf[SHA_CHUNK];
size_t sha_length = 0;
uint32_t i = 0;
psa_status_t status = psa_hash_setup(&hash_operation, alg);
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "Failed to setup psa, status: %d", status);
return ESP_FAIL;
}
while (i < length) {
uint32_t n = (length - i > SHA_CHUNK) ? SHA_CHUNK : (length - i);//take a chunk, or the last of it
err = esp_partition_read(partition, i, sha_buf, n);
if (err != ESP_OK) {
psa_hash_abort(&hash_operation);
return err;
}
status = psa_hash_update(&hash_operation, sha_buf, n);
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "Failed to update psa hash, status: %d", status);
psa_hash_abort(&hash_operation);
return ESP_FAIL;
}
i += n;
}
status = psa_hash_finish(&hash_operation, out_digest, ESP_SECURE_BOOT_DIGEST_LEN, &sha_length);
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "Failed to finish psa hash, status: %d", status);
psa_hash_abort(&hash_operation);
return ESP_FAIL;
}
return err;
}
static esp_err_t ota_verify_data_partition_signature(const esp_partition_t *partition, uint32_t total_written_size)
{
esp_err_t err = ESP_FAIL;
uint8_t digest[ESP_SECURE_BOOT_DIGEST_LEN] = {0};
/* Calculate data length by excluding the signature sector from total written size */
uint32_t data_length = ((total_written_size) & ~((SPI_FLASH_SEC_SIZE) - 1)) - SPI_FLASH_SEC_SIZE;
/* Rounding off data length to the upper 4k boundary for hash calculation */
uint32_t padded_length = ALIGN_UP(data_length, SPI_FLASH_SEC_SIZE);
#if CONFIG_SECURE_BOOT_ECDSA_KEY_LEN_384_BITS
err = ota_calc_partition_bin_sha(partition, padded_length, digest, PSA_ALG_SHA_384);
#else
err = ota_calc_partition_bin_sha(partition, padded_length, digest, PSA_ALG_SHA_256);
#endif
if (err != ESP_OK) {
ESP_LOGE(TAG, "Digest calculation failed partition: %s", partition->label);
return err;
}
const ets_secure_boot_signature_t sig_block = {0};
err = esp_partition_read(partition, data_length, (void*)&sig_block, sizeof(ets_secure_boot_signature_t));
if (err != ESP_OK) {
ESP_LOGE(TAG, "Reading signature block failed for partition: %s", partition->label);
return err;
}
err = esp_secure_boot_verify_sbv2_signature_block(&sig_block, digest, NULL);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Secure Boot V2 verification failed.");
}
return err;
}
#endif // CONFIG_SECURE_SIGNED_DATA_PARTITION
static esp_err_t ota_verify_partition(ota_ops_entry_t *ota_ops)
{
esp_err_t ret = ESP_OK;
@@ -495,6 +573,17 @@ static esp_err_t ota_verify_partition(ota_ops_entry_t *ota_ops)
esp_partition_munmap(partition_table_map);
}
}
#if CONFIG_SECURE_SIGNED_DATA_PARTITION
else if (ota_ops->partition.final->type == ESP_PARTITION_TYPE_DATA &&
ota_ops->partition.final->subtype == ESP_PARTITION_SUBTYPE_DATA_UNDEFINED) {
esp_err_t err = ota_verify_data_partition_signature(ota_ops->partition.staging, ota_ops->wrote_size);
if (err != ESP_OK) {
ESP_LOGE(TAG,"esp_secure_boot_verify_signature failed for partition %s, return %d", ota_ops->partition.final->label, err);
return ESP_ERR_OTA_VALIDATE_FAILED;
}
return ESP_OK;
}
#endif // CONFIG_SECURE_SIGNED_DATA_PARTITION
return ret;
}
+51 -22
View File
@@ -601,34 +601,63 @@ esp_err_t esp_partition_copy(const esp_partition_t* dest_part, uint32_t dest_off
uint32_t src_current_offset = src_offset;
uint32_t dest_current_offset = dest_offset;
size_t remaining_size = size;
/* Read the portion that fits in the free MMU pages */
uint32_t mmu_free_pages_count = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA);
int attempts_for_mmap = 0;
while (remaining_size > 0) {
uint32_t chunk_size = MIN(remaining_size, mmu_free_pages_count * SPI_FLASH_MMU_PAGE_SIZE);
esp_partition_mmap_handle_t src_part_map;
const void *src_data = NULL;
error = esp_partition_mmap(src_part, src_current_offset, chunk_size, ESP_PARTITION_MMAP_DATA, &src_data, &src_part_map);
if (error == ESP_OK) {
attempts_for_mmap = 0;
if (src_part->encrypted) {
/* Read the portion that fits in the free MMU pages */
uint32_t mmu_free_pages_count = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA);
int attempts_for_mmap = 0;
while (remaining_size > 0) {
uint32_t chunk_size = MIN(remaining_size, mmu_free_pages_count * SPI_FLASH_MMU_PAGE_SIZE);
esp_partition_mmap_handle_t src_part_map;
const void *src_data = NULL;
error = esp_partition_mmap(src_part, src_current_offset, chunk_size, ESP_PARTITION_MMAP_DATA, &src_data, &src_part_map);
if (error == ESP_OK) {
attempts_for_mmap = 0;
error = esp_partition_write(dest_part, dest_current_offset, src_data, chunk_size);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Writing to destination partition failed (err=0x%x)", error);
esp_partition_munmap(src_part_map);
break;
}
esp_partition_munmap(src_part_map);
} else {
mmu_free_pages_count = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA);
chunk_size = 0;
if (++attempts_for_mmap >= 3) {
ESP_LOGE(TAG, "Failed to mmap source partition after a few attempts, mmu_free_pages = %" PRIu32 " (err=0x%x)", mmu_free_pages_count, error);
break;
}
}
src_current_offset += chunk_size;
dest_current_offset += chunk_size;
remaining_size -= chunk_size;
}
} else {
// In case of unencrypted partition, we can read and write directly
while (remaining_size > 0) {
uint32_t chunk_size = MIN(remaining_size, SPI_FLASH_SEC_SIZE);
void *src_data = malloc(chunk_size);
if (src_data == NULL) {
ESP_LOGE(TAG, "Failed to allocate memory for chunk (size: %" PRIu32 ")", chunk_size);
error = ESP_ERR_NO_MEM;
break;
}
error = esp_partition_read(src_part, src_current_offset, src_data, chunk_size);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Reading from source partition failed (err=0x%x)", error);
free(src_data);
break;
}
error = esp_partition_write(dest_part, dest_current_offset, src_data, chunk_size);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Writing to destination partition failed (err=0x%x)", error);
esp_partition_munmap(src_part_map);
break;
}
esp_partition_munmap(src_part_map);
} else {
mmu_free_pages_count = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA);
chunk_size = 0;
if (++attempts_for_mmap >= 3) {
ESP_LOGE(TAG, "Failed to mmap source partition after a few attempts, mmu_free_pages = %" PRIu32 " (err=0x%x)", mmu_free_pages_count, error);
free(src_data);
break;
}
free(src_data);
src_current_offset += chunk_size;
dest_current_offset += chunk_size;
remaining_size -= chunk_size;
}
src_current_offset += chunk_size;
dest_current_offset += chunk_size;
remaining_size -= chunk_size;
}
return error;
}
+17
View File
@@ -229,6 +229,23 @@ The verification of signed OTA updates can be performed even without enabling ha
For more information, please refer to :ref:`signed-app-verify`.
.. _secure-signed-data-partition:
Signed Data Partition Updates
------------------------------
Data partition images can be verified using the same Secure Boot v2 signature mechanism as application images. Enable :ref:`CONFIG_SECURE_SIGNED_DATA_PARTITION` to verify data partitions with subtype ``ESP_PARTITION_SUBTYPE_DATA_UNDEFINED`` during OTA updates.
Sign data partition images using:
.. code-block:: bash
idf.py secure-sign-data --keyfile PRIVATE_SIGNING_KEY --output signed_data.bin data.bin
The signing key must match the one used for application signing, with its public key digest programmed into eFuse. The signed image format is: data content (padded to 4 KB) + 4 KB signature block (see :ref:`signature-block-format` in :doc:`/security/secure-boot-v2`).
For a complete example, see :example:`system/ota/partitions_ota`.
Tuning OTA Performance
----------------------
+10
View File
@@ -408,6 +408,16 @@ An image is verified if the public key stored in any signature block is valid fo
3. Use the public key to verify the signature of the bootloader image, using either RSA-PSS (section 8.1.2 of RFC8017) or ECDSA signature verification (section 5.3.3 of RFC6090) with the image digest calculated in step (2) for comparison.
Verifying Data Partitions
--------------------------
The Secure Boot v2 signature verification can also verify data partition images during OTA updates. Enable :ref:`CONFIG_SECURE_SIGNED_DATA_PARTITION` to verify data partitions with subtype ``ESP_PARTITION_SUBTYPE_DATA_UNDEFINED``.
Data partition images must be signed using ``idf.py secure-sign-data`` with the same signing key and follow the same format as application images. The verification uses the public key digest(s) stored in eFuse and follows the process described in :ref:`verify_image`.
For detailed information including OTA procedures and partition configuration, see :ref:`secure-signed-data-partition`.
Bootloader Size
---------------
+17
View File
@@ -229,6 +229,23 @@ Kconfig 中的 :ref:`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` 可以帮助用户
具体可参考 :ref:`signed-app-verify`
.. _secure-signed-data-partition:
签名数据分区的更新
------------------
数据分区镜像可以使用与应用镜像相同的 Secure Boot v2 签名机制进行验证。启用 :ref:`CONFIG_SECURE_SIGNED_DATA_PARTITION`,以便在 OTA 更新期间验证子类型为 ``ESP_PARTITION_SUBTYPE_DATA_UNDEFINED`` 的数据分区。
使用以下命令对数据分区镜像进行签名:
.. code-block:: bash
idf.py secure-sign-data --keyfile PRIVATE_SIGNING_KEY --output signed_data.bin data.bin
签名密钥必须与用于应用签名的密钥一致,并将其公钥摘要写入 eFuse。签名镜像格式为:数据内容(填充到 4 KB)+ 4 KB 签名块(参见 :doc:`/security/secure-boot-v2` 中的 :ref:`signature-block-format`)。
如需完整示例,请参见 :example:`system/ota/partitions_ota`
OTA 性能调优
------------
+10
View File
@@ -408,6 +408,16 @@
3. 使用公钥,采用 RSA-PSSRFC8017 的第 8.1.2 节)算法或 ECDSARFC6090 的第 5.3.3 节)算法,验证引导加载程序镜像的签名,并与步骤 (2) 中计算的镜像摘要比较。
验证数据分区
------------
Secure Boot v2 签名验证也可以在 OTA 更新期间验证数据分区镜像。启用 :ref:`CONFIG_SECURE_SIGNED_DATA_PARTITION` 以验证子类型为 ``ESP_PARTITION_SUBTYPE_DATA_UNDEFINED`` 的数据分区。
数据分区镜像必须使用相同的签名密钥,通过 ``idf.py secure-sign-data`` 进行签名,并采用与应用镜像相同的格式。验证使用存储在 eFuse 中的一个或多个公钥摘要,并遵循 :ref:`verify_image` 中所述的流程。
关于包括 OTA 流程和分区配置在内的详细信息,请参见 :ref:`secure-signed-data-partition`
引导加载程序的大小
------------------
@@ -6,10 +6,15 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(partitions_ota)
# Copy storage.bin from test folder to build directory
if(CONFIG_SECURE_SIGNED_DATA_PARTITION)
set(storage_file signed_storage.bin)
else()
set(storage_file storage.bin)
endif()
add_custom_target(copy_storage_bin ALL
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/test/storage.bin
${CMAKE_BINARY_DIR}/storage.bin
COMMENT "Copying test/storage.bin to build directory"
DEPENDS ${CMAKE_SOURCE_DIR}/test/storage.bin
${CMAKE_SOURCE_DIR}/test/${storage_file}
${CMAKE_BINARY_DIR}/${storage_file}
COMMENT "Copying test/${storage_file} to build directory"
DEPENDS ${CMAKE_SOURCE_DIR}/test/${storage_file}
)
@@ -209,14 +209,19 @@ static esp_err_t ota_update_partitions(esp_https_ota_config_t *ota_config)
}
} else if (strstr(ota_config->http_config->url, "storage.bin") != NULL) {
#if CONFIG_SECURE_SIGNED_DATA_PARTITION
ota_config->partition.staging = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "staging");
assert(ota_config->partition.staging != NULL);
#else
ota_config->partition.staging = NULL; // free app ota partition will be selected and used for downloading a new image
#endif // SECURE_SIGNED_DATA_PARTITION
ota_config->partition.final = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "storage");
assert(ota_config->partition.final != NULL);
ota_config->partition.finalize_with_copy = true; // After the download is complete, copy the received image to the final partition automatically
ret = esp_https_ota(ota_config);
char text[16];
char text[16] = {0};
ESP_ERROR_CHECK(esp_partition_read(ota_config->partition.final, 0, text, sizeof(text)));
ESP_LOG_BUFFER_CHAR(TAG, text, sizeof(text));
ESP_LOG_BUFFER_HEXDUMP(TAG, text, sizeof(text), ESP_LOG_INFO);
assert(memcmp("7296406769363431", text, sizeof(text)) == 0);
} else {
@@ -93,7 +93,7 @@ def test_examples_partitions_ota(dut: Dut) -> None:
dut.serial.bootloader_flash()
print(' - Start app (flash partition_table and app)')
dut.serial.write_flash_no_enc()
update_partitions(dut, 'wifi_high_traffic')
update_partitions(dut, 'wifi_high_traffic', False)
@pytest.mark.flash_encryption_wifi_high_traffic
@@ -105,19 +105,32 @@ def test_examples_partitions_ota(dut: Dut) -> None:
def test_examples_partitions_ota_with_flash_encryption_wifi(dut: Dut) -> None:
dut.serial.erase_flash()
dut.serial.flash()
update_partitions(dut, 'flash_encryption_wifi_high_traffic')
update_partitions(dut, 'flash_encryption_wifi_high_traffic', False)
def update_partitions(dut: Dut, env_name: str | None) -> None:
@pytest.mark.flash_encryption_wifi_high_traffic
@pytest.mark.parametrize('config', ['flash_enc_wifi_2.data_partition_verification'], indirect=True)
@pytest.mark.parametrize('skip_autoflash', ['y'], indirect=True)
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
def test_examples_partitions_ota_with_flash_enc_wifi_2_data_partition_verification(dut: Dut) -> None:
dut.serial.erase_flash()
dut.serial.flash()
update_partitions(dut, 'flash_encryption_wifi_high_traffic', True)
def update_partitions(dut: Dut, env_name: str | None, signed_storage: bool | None) -> None:
port = 8000
thread1 = multiprocessing.Process(target=start_https_server, args=(dut.app.binary_path, '0.0.0.0', port))
thread1.daemon = True
thread1.start()
try:
if signed_storage:
update(dut, port, 'signed_storage.bin', env_name)
else:
update(dut, port, 'storage.bin', env_name)
update(dut, port, 'partitions_ota.bin', env_name)
update(dut, port, 'bootloader/bootloader.bin', env_name)
update(dut, port, 'partition_table/partition-table.bin', env_name)
update(dut, port, 'storage.bin', env_name)
finally:
thread1.terminate()
@@ -0,0 +1,32 @@
# Common configs
CONFIG_EXAMPLE_WIFI_SSID_PWD_FROM_STDIN=y
CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL="FROM_STDIN"
CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECK=y
CONFIG_EXAMPLE_FIRMWARE_UPGRADE_BIND_IF=y
CONFIG_MBEDTLS_TLS_CLIENT_ONLY=y
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_EXAMPLE_CONNECT_IPV6=n
CONFIG_SECURE_FLASH_ENC_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y
CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC=y
CONFIG_SECURE_BOOT_ALLOW_JTAG=y
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_ENC=y
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_DEC=y
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_CACHE=y
CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED=y
CONFIG_NVS_SEC_KEY_PROTECT_USING_FLASH_ENC=y
# This is required for nvs encryption (which is enabled by default with flash encryption)
CONFIG_PARTITION_TABLE_OFFSET=0x9000
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="test/partitions_efuse_emul_4.csv"
CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT=y
CONFIG_SECURE_SIGNED_ON_UPDATE_NO_SECURE_BOOT=y
CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME=y
CONFIG_SECURE_BOOT_SIGNING_KEY="test/secure_boot_signing_key.pem"
CONFIG_SECURE_BOOT_ALLOW_SHORT_APP_PARTITION=y
CONFIG_SECURE_SIGNED_DATA_PARTITION=y
@@ -0,0 +1,3 @@
# ESP32 supports SECURE_BOOT_V2 only in ECO3
CONFIG_IDF_TARGET="esp32"
CONFIG_ESP32_REV_MIN_3=y
@@ -0,0 +1,10 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, , 0x6000,
nvs_key, data, nvs_keys, , 4K,
staging, data, , , 0x4000,
storage, data, , , 0x4000, encrypted
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
emul_efuse, data, efuse, , 0x2000,
ota_0, app, ota_0, , 0x1B0000,
ota_1, app, ota_1, , 0x1B0000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x6000
3 nvs_key data nvs_keys 4K
4 staging data 0x4000
5 storage data 0x4000 encrypted
6 otadata data ota 0x2000
7 phy_init data phy 0x1000
8 emul_efuse data efuse 0x2000
9 ota_0 app ota_0 0x1B0000
10 ota_1 app ota_1 0x1B0000