From 54eb749fd2d0c6c800acaa27c95a69679518d5ee Mon Sep 17 00:00:00 2001 From: "nilesh.kale" Date: Thu, 6 Mar 2025 14:38:57 +0530 Subject: [PATCH] feat: updated check for chip revision and respective testcases This commit have updated check for max chip revision along with min chip revision. Also added qemu based pytest to verify chip revision while performing OTA. --- .../include/bootloader_common.h | 15 +- .../src/bootloader_common_loader.c | 54 +++-- components/esp_https_ota/src/esp_https_ota.c | 5 +- .../ota/advanced_https_ota/efuse_esp32c3.bin | Bin 0 -> 1024 bytes .../advanced_https_ota/pytest_advanced_ota.py | 188 ++++++++++++++++-- .../sdkconfig.ci.verify_revision_esp32c3 | 12 ++ 6 files changed, 239 insertions(+), 35 deletions(-) create mode 100644 examples/system/ota/advanced_https_ota/efuse_esp32c3.bin create mode 100644 examples/system/ota/advanced_https_ota/sdkconfig.ci.verify_revision_esp32c3 diff --git a/components/bootloader_support/include/bootloader_common.h b/components/bootloader_support/include/bootloader_common.h index 4f819a13d1..ce678d162b 100644 --- a/components/bootloader_support/include/bootloader_common.h +++ b/components/bootloader_support/include/bootloader_common.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2018-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -24,6 +24,19 @@ typedef enum { ESP_IMAGE_APPLICATION } esp_image_type; +/** + * @brief Check if the chip revision meets the image requirements. + * + * This function verifies whether the actual chip revision satisfies the minimum + * and optionally the maximum chip revision requirements specified in the image. + * + * @param image_header Pointer to the image header containing revision details. + * @param check_max_revision If true, also checks the maximum chip revision requirements. + * + * @return true if the chip revision meets the requirements, false otherwise. + */ +bool bootloader_common_check_chip_revision_validity(const esp_image_header_t *image_header, bool check_max_revision); + /** * @brief Read ota_info partition and fill array from two otadata structures. * diff --git a/components/bootloader_support/src/bootloader_common_loader.c b/components/bootloader_support/src/bootloader_common_loader.c index 2d29ac6dbf..f1b902bfd4 100644 --- a/components/bootloader_support/src/bootloader_common_loader.c +++ b/components/bootloader_support/src/bootloader_common_loader.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -31,6 +31,37 @@ static const char* TAG = "boot_comm"; +bool bootloader_common_check_chip_revision_validity(const esp_image_header_t *img_hdr, bool check_max_revision) +{ + if (!img_hdr) { + return false; + } + + unsigned revision = efuse_hal_chip_revision(); + unsigned min_rev = img_hdr->min_chip_rev_full; + + bool is_min_rev_invalid = !ESP_CHIP_REV_ABOVE(revision, min_rev); + if (is_min_rev_invalid) { + ESP_LOGE(TAG, "chip revision check failed. Required >= v%d.%d, found v%d.%d.", + min_rev / 100, min_rev % 100, + revision / 100, revision % 100); + return false; + } + + if (check_max_revision) { + unsigned int max_rev = img_hdr->max_chip_rev_full; + bool is_max_rev_invalid = IS_FIELD_SET(max_rev) && revision > max_rev && !efuse_hal_get_disable_wafer_version_major(); + if (is_max_rev_invalid) { + ESP_LOGE(TAG, "chip revision check failed. Required <= v%d.%d, found v%d.%d.", + max_rev / 100, max_rev % 100, + revision / 100, revision % 100); + return false; + } + } + + return true; +} + uint32_t bootloader_common_ota_select_crc(const esp_ota_select_entry_t *s) { return esp_rom_crc32_le(UINT32_MAX, (uint8_t*)&s->ota_seq, 4); @@ -91,24 +122,15 @@ esp_err_t bootloader_common_check_chip_validity(const esp_image_header_t* img_hd err = ESP_FAIL; } else { #ifndef CONFIG_IDF_ENV_FPGA - unsigned revision = efuse_hal_chip_revision(); - unsigned int major_rev = revision / 100; - unsigned int minor_rev = revision % 100; - unsigned min_rev = img_hdr->min_chip_rev_full; - if (type == ESP_IMAGE_BOOTLOADER || type == ESP_IMAGE_APPLICATION) { - if (!ESP_CHIP_REV_ABOVE(revision, min_rev)) { - ESP_LOGE(TAG, "Image requires chip rev >= v%d.%d, but chip is v%d.%d", - min_rev / 100, min_rev % 100, - major_rev, minor_rev); + if (type == ESP_IMAGE_APPLICATION) { + if (!bootloader_common_check_chip_revision_validity(img_hdr, true)) { err = ESP_FAIL; } } - if (type == ESP_IMAGE_APPLICATION) { - unsigned max_rev = img_hdr->max_chip_rev_full; - if ((IS_FIELD_SET(max_rev) && (revision > max_rev) && !efuse_hal_get_disable_wafer_version_major())) { - ESP_LOGE(TAG, "Image requires chip rev <= v%d.%d, but chip is v%d.%d", - max_rev / 100, max_rev % 100, - major_rev, minor_rev); + + // Maximum revision check is skipped for bootloader images + if (type == ESP_IMAGE_BOOTLOADER) { + if (!bootloader_common_check_chip_revision_validity(img_hdr, false)) { err = ESP_FAIL; } } diff --git a/components/esp_https_ota/src/esp_https_ota.c b/components/esp_https_ota/src/esp_https_ota.c index ea140877dd..2757121378 100644 --- a/components/esp_https_ota/src/esp_https_ota.c +++ b/components/esp_https_ota/src/esp_https_ota.c @@ -631,10 +631,7 @@ static esp_err_t esp_ota_verify_chip_revision(const void *arg) esp_image_header_t *data = (esp_image_header_t *)(arg); esp_https_ota_dispatch_event(ESP_HTTPS_OTA_VERIFY_CHIP_REVISION, (void *)(&data->min_chip_rev_full), sizeof(uint16_t)); - uint16_t ota_img_revision = data->min_chip_rev_full; - uint32_t chip_revision = efuse_hal_chip_revision(); - if (ota_img_revision > chip_revision) { - ESP_LOGE(TAG, "Image requires chip rev >= v%d.%d, but chip is v%d.%d", ota_img_revision / 100, ota_img_revision % 100, chip_revision / 100, chip_revision % 100); + if (!bootloader_common_check_chip_revision_validity(data, true)) { return ESP_ERR_INVALID_VERSION; } return ESP_OK; diff --git a/examples/system/ota/advanced_https_ota/efuse_esp32c3.bin b/examples/system/ota/advanced_https_ota/efuse_esp32c3.bin new file mode 100644 index 0000000000000000000000000000000000000000..bcab7e0b284aa0d9f983e70d2f633f6ea4a91269 GIT binary patch literal 1024 XcmZP|3h)r6YE;o^2#kin& def start_chunked_server(ota_image_dir: str, server_port: int) -> subprocess.Popen: os.chdir(ota_image_dir) - chunked_server = subprocess.Popen([ - 'openssl', - 's_server', - '-WWW', - '-key', - key_file, - '-cert', - server_file, - '-port', - str(server_port), - ]) + chunked_server = subprocess.Popen( + [ + 'openssl', + 's_server', + '-WWW', + '-key', + key_file, + '-cert', + server_file, + '-port', + str(server_port), + ] + ) return chunked_server @@ -129,6 +132,48 @@ def start_redirect_server(ota_image_dir: str, server_ip: str, server_port: int, httpd.serve_forever() +# Function to modify chip revisions in the app header +def modify_chip_revision( + app_path: str, min_rev: Optional[int] = None, max_rev: Optional[int] = None, increment_min: bool = False +) -> None: + """ + Modify min_chip_rev_full and max_chip_rev_full in the app header. + + :param app_path: Path to the app binary. + :param min_rev: Value to set min_chip_rev_full (if provided). + :param max_rev: Value to set max_chip_rev_full (if provided). + :param increment_min: If True, increments min_chip_rev_full. + """ + + HEADER_SIZE = 512 + TARGET_OFFSET_MIN_REV = 0x0F + TARGET_OFFSET_MAX_REV = 0x11 + + if not os.path.exists(app_path): + raise FileNotFoundError(f"App binary file '{app_path}' not found") + + try: + with open(app_path, 'rb') as f: + header = bytearray(f.read(HEADER_SIZE)) + + # Increment or set min revision value + if increment_min: + header[TARGET_OFFSET_MIN_REV] = (header[TARGET_OFFSET_MIN_REV] + 1) & 0xFF + elif min_rev is not None: + header[TARGET_OFFSET_MIN_REV] = min_rev & 0xFF + + # Set max revision value + if max_rev is not None: + header[TARGET_OFFSET_MAX_REV] = max_rev & 0xFF + + # Write back the modified header to the binary file + with open(app_path, 'r+b') as f: + f.write(header) + + except IOError as e: + raise RuntimeError(f'Failed to modify app header: {e}') + + @pytest.mark.ethernet_ota @idf_parametrize('target', ['esp32'], indirect=['target']) def test_examples_protocol_advanced_https_ota_example(dut: Dut) -> None: @@ -253,7 +298,8 @@ def test_examples_protocol_advanced_https_ota_example_truncated_bin(dut: Dut) -> bin_name = 'advanced_https_ota.bin' # Truncated binary file to be generated from original binary file truncated_bin_name = 'truncated.bin' - # Size of truncated file to be grnerated. This value can range from 288 bytes (Image header size) to size of original binary file + # Size of truncated file to be grnerated. + # This value can range from 288 bytes (Image header size) to size of original binary file # truncated_bin_size is set to 64000 to reduce consumed by the test case truncated_bin_size = 64000 binary_file = os.path.join(dut.app.binary_path, bin_name) @@ -757,7 +803,8 @@ def test_examples_protocol_advanced_https_ota_example_ota_resumption_partial_dow @idf_parametrize('target', ['esp32', 'esp32c3', 'esp32s3'], indirect=['target']) def test_examples_protocol_advanced_https_ota_example_nimble_gatts(dut: Dut) -> None: """ - Run an OTA image update while a BLE GATT Server is running in background. This GATT server will be using NimBLE Host stack. + Run an OTA image update while a BLE GATT Server is running in background. + This GATT server will be using NimBLE Host stack. steps: | 1. join AP/Ethernet 2. Run BLE advertise and then GATT server. @@ -812,7 +859,8 @@ def test_examples_protocol_advanced_https_ota_example_nimble_gatts(dut: Dut) -> @idf_parametrize('target', ['esp32', 'esp32c3', 'esp32s3'], indirect=['target']) def test_examples_protocol_advanced_https_ota_example_bluedroid_gatts(dut: Dut) -> None: """ - Run an OTA image update while a BLE GATT Server is running in background. This GATT server will be using Bluedroid Host stack. + Run an OTA image update while a BLE GATT Server is running in background. + This GATT server will be using Bluedroid Host stack. steps: | 1. join AP/Ethernet 2. Run BLE advertise and then GATT server. @@ -907,3 +955,115 @@ def test_examples_protocol_advanced_https_ota_example_openssl_aligned_bin(dut: D pass finally: chunked_server.kill() + + +@pytest.mark.qemu +@pytest.mark.nightly_run +@pytest.mark.host_test +@pytest.mark.parametrize( + 'qemu_extra_args', + [ + f'-drive file={os.path.join(os.path.dirname(__file__), "efuse_esp32c3.bin")},if=none,format=raw,id=efuse ' + '-global driver=nvram.esp32c3.efuse,property=drive,value=efuse ' + '-global driver=timer.esp32c3.timg,property=wdt_disable,value=true', + ], + indirect=True, +) +@idf_parametrize('target', ['esp32c3'], indirect=['target']) +@pytest.mark.parametrize('config', ['verify_revision'], indirect=True) +def test_examples_protocol_advanced_https_ota_example_verify_min_chip_revision(dut: Dut) -> None: + """ + This is a QEMU test case that verifies the chip revision value in the application header. + steps: | + 1. join AP/Ethernet + 2. Fetch OTA image over HTTPS + 3. Reboot with the new OTA image + """ + + # Update the min full revision field in the app header + app_path = os.path.join(dut.app.binary_path, 'advanced_https_ota.bin') + # Increment min_chip_rev_full + modify_chip_revision(app_path, increment_min=True) + + server_port = 8001 + bin_name = 'advanced_https_ota.bin' + # Start server + thread1 = multiprocessing.Process(target=start_https_server, args=(dut.app.binary_path, '0.0.0.0', server_port)) + thread1.daemon = True + thread1.start() + try: + # start test + dut.expect('Loaded app from partition at offset', timeout=30) + + try: + ip_address = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)[1].decode() + print('Connected to AP/Ethernet with IP: {}'.format(ip_address)) + except pexpect.exceptions.TIMEOUT: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP/Ethernet') + + dut.expect('Starting Advanced OTA example', timeout=30) + host_ip = get_host_ip4_by_dest_ip(ip_address) + + print('writing to device: {}'.format('https://' + host_ip + ':' + str(server_port) + '/' + bin_name)) + dut.write('https://' + host_ip + ':' + str(server_port) + '/' + bin_name) + dut.expect('Starting OTA...', timeout=60) + dut.expect('chip revision check failed.', timeout=150) + + finally: + thread1.terminate() + + +@pytest.mark.qemu +@pytest.mark.nightly_run +@pytest.mark.host_test +@pytest.mark.parametrize( + 'qemu_extra_args', + [ + f'-drive file={os.path.join(os.path.dirname(__file__), "efuse_esp32c3.bin")},if=none,format=raw,id=efuse ' + '-global driver=nvram.esp32c3.efuse,property=drive,value=efuse ' + '-global driver=timer.esp32c3.timg,property=wdt_disable,value=true', + ], + indirect=True, +) +@idf_parametrize('target', ['esp32c3'], indirect=['target']) +@pytest.mark.parametrize('config', ['verify_revision'], indirect=True) +def test_examples_protocol_advanced_https_ota_example_verify_max_chip_revision(dut: Dut) -> None: + """ + This is a QEMU test case that verifies the chip revision value in the application header. + steps: | + 1. join AP/Ethernet + 2. Fetch OTA image over HTTPS + 3. Reboot with the new OTA image + """ + + # Update the min full revision field in the app header + app_path = os.path.join(dut.app.binary_path, 'advanced_https_ota.bin') + # Set min_chip_rev_full to 0.0 and max_chip_rev_full to 0.2 + modify_chip_revision(app_path, min_rev=0x00, max_rev=0x02) + + server_port = 8001 + bin_name = 'advanced_https_ota.bin' + # Start server + thread1 = multiprocessing.Process(target=start_https_server, args=(dut.app.binary_path, '0.0.0.0', server_port)) + thread1.daemon = True + thread1.start() + try: + # start test + dut.expect('Loaded app from partition at offset', timeout=30) + + try: + ip_address = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)[1].decode() + print('Connected to AP/Ethernet with IP: {}'.format(ip_address)) + except pexpect.exceptions.TIMEOUT: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP/Ethernet') + + dut.expect('Starting Advanced OTA example', timeout=30) + host_ip = get_host_ip4_by_dest_ip(ip_address) + + print('writing to device: {}'.format('https://' + host_ip + ':' + str(server_port) + '/' + bin_name)) + dut.write('https://' + host_ip + ':' + str(server_port) + '/' + bin_name) + dut.expect('Starting OTA...', timeout=60) + dut.expect('chip revision check failed.', timeout=150) + + finally: + thread1.terminate() diff --git a/examples/system/ota/advanced_https_ota/sdkconfig.ci.verify_revision_esp32c3 b/examples/system/ota/advanced_https_ota/sdkconfig.ci.verify_revision_esp32c3 new file mode 100644 index 0000000000..0dd6afaa55 --- /dev/null +++ b/examples/system/ota/advanced_https_ota/sdkconfig.ci.verify_revision_esp32c3 @@ -0,0 +1,12 @@ +CONFIG_IDF_TARGET="esp32c3" + +CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL="FROM_STDIN" +CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECK=y +CONFIG_EXAMPLE_SKIP_VERSION_CHECK=y +CONFIG_EXAMPLE_OTA_RECV_TIMEOUT=3000 + +# QEMU-Related configurations +CONFIG_EXAMPLE_CONNECT_ETHERNET=y +CONFIG_EXAMPLE_USE_OPENETH=y +CONFIG_EXAMPLE_CONNECT_WIFI=n +CONFIG_ETH_USE_SPI_ETHERNET=n