From 8c6845bb96268794f194c3af540d775665705cb5 Mon Sep 17 00:00:00 2001 From: Ashish Sharma Date: Wed, 3 Sep 2025 18:29:32 +0800 Subject: [PATCH] feat(secure_boot): adds api to verify data partition integrity Closes https://github.com/espressif/esp-idf/issues/17482 --- .pre-commit-config.yaml | 1 + components/app_update/CMakeLists.txt | 4 + components/app_update/Kconfig.projbuild | 9 ++ components/app_update/esp_ota_ops.c | 89 ++++++++++++++++++ components/esp_partition/partition.c | 73 +++++++++----- .../system/ota/partitions_ota/CMakeLists.txt | 13 ++- .../system/ota/partitions_ota/main/app_main.c | 9 +- .../partitions_ota/pytest_partitions_ota.py | 21 ++++- ...ash_enc_wifi_2.data_partition_verification | 32 +++++++ ...c_wifi_2.data_partition_verification.esp32 | 3 + .../test/partitions_efuse_emul_4.csv | 10 ++ .../partitions_ota/test/signed_storage.bin | Bin 0 -> 8192 bytes 12 files changed, 232 insertions(+), 32 deletions(-) create mode 100644 components/app_update/Kconfig.projbuild create mode 100644 examples/system/ota/partitions_ota/sdkconfig.ci.flash_enc_wifi_2.data_partition_verification create mode 100644 examples/system/ota/partitions_ota/sdkconfig.ci.flash_enc_wifi_2.data_partition_verification.esp32 create mode 100644 examples/system/ota/partitions_ota/test/partitions_efuse_emul_4.csv create mode 100644 examples/system/ota/partitions_ota/test/signed_storage.bin diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0d521469e..2f80070ebe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/components/app_update/CMakeLists.txt b/components/app_update/CMakeLists.txt index 1f4d7051dd..d1be3bdab9 100644 --- a/components/app_update/CMakeLists.txt +++ b/components/app_update/CMakeLists.txt @@ -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") diff --git a/components/app_update/Kconfig.projbuild b/components/app_update/Kconfig.projbuild new file mode 100644 index 0000000000..516ad82a83 --- /dev/null +++ b/components/app_update/Kconfig.projbuild @@ -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 diff --git a/components/app_update/esp_ota_ops.c b/components/app_update/esp_ota_ops.c index af25229b4d..c49dfd66f0 100644 --- a/components/app_update/esp_ota_ops.c +++ b/components/app_update/esp_ota_ops.c @@ -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)) @@ -446,6 +449,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; @@ -471,6 +549,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; } diff --git a/components/esp_partition/partition.c b/components/esp_partition/partition.c index e58d56dc28..7896e0a222 100644 --- a/components/esp_partition/partition.c +++ b/components/esp_partition/partition.c @@ -602,34 +602,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; } diff --git a/examples/system/ota/partitions_ota/CMakeLists.txt b/examples/system/ota/partitions_ota/CMakeLists.txt index 2ecdf1071f..e56a87b667 100644 --- a/examples/system/ota/partitions_ota/CMakeLists.txt +++ b/examples/system/ota/partitions_ota/CMakeLists.txt @@ -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} ) diff --git a/examples/system/ota/partitions_ota/main/app_main.c b/examples/system/ota/partitions_ota/main/app_main.c index 2824e9e78a..8fde4d06cf 100644 --- a/examples/system/ota/partitions_ota/main/app_main.c +++ b/examples/system/ota/partitions_ota/main/app_main.c @@ -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 { diff --git a/examples/system/ota/partitions_ota/pytest_partitions_ota.py b/examples/system/ota/partitions_ota/pytest_partitions_ota.py index 27fcc9a7a8..75b162ada9 100644 --- a/examples/system/ota/partitions_ota/pytest_partitions_ota.py +++ b/examples/system/ota/partitions_ota/pytest_partitions_ota.py @@ -97,7 +97,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 @@ -109,19 +109,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() diff --git a/examples/system/ota/partitions_ota/sdkconfig.ci.flash_enc_wifi_2.data_partition_verification b/examples/system/ota/partitions_ota/sdkconfig.ci.flash_enc_wifi_2.data_partition_verification new file mode 100644 index 0000000000..a469358b50 --- /dev/null +++ b/examples/system/ota/partitions_ota/sdkconfig.ci.flash_enc_wifi_2.data_partition_verification @@ -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 diff --git a/examples/system/ota/partitions_ota/sdkconfig.ci.flash_enc_wifi_2.data_partition_verification.esp32 b/examples/system/ota/partitions_ota/sdkconfig.ci.flash_enc_wifi_2.data_partition_verification.esp32 new file mode 100644 index 0000000000..413efc1cbd --- /dev/null +++ b/examples/system/ota/partitions_ota/sdkconfig.ci.flash_enc_wifi_2.data_partition_verification.esp32 @@ -0,0 +1,3 @@ +# ESP32 supports SECURE_BOOT_V2 only in ECO3 +CONFIG_IDF_TARGET="esp32" +CONFIG_ESP32_REV_MIN_3=y diff --git a/examples/system/ota/partitions_ota/test/partitions_efuse_emul_4.csv b/examples/system/ota/partitions_ota/test/partitions_efuse_emul_4.csv new file mode 100644 index 0000000000..8d5c702ad5 --- /dev/null +++ b/examples/system/ota/partitions_ota/test/partitions_efuse_emul_4.csv @@ -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, diff --git a/examples/system/ota/partitions_ota/test/signed_storage.bin b/examples/system/ota/partitions_ota/test/signed_storage.bin new file mode 100644 index 0000000000000000000000000000000000000000..8ddc5d534667a5fd57343ce538c137ed813cce8d GIT binary patch literal 8192 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 B!PNi& literal 0 HcmV?d00001