forked from espressif/esp-idf
Merge branch 'feature/add_test_to_verify_chip_revision_while_performing_ota' into 'master'
feat: updated check for chip revision and added testcase Closes IDF-12587 See merge request espressif/esp-idf!37546
This commit is contained in:
@@ -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
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
@@ -24,6 +24,19 @@ typedef enum {
|
|||||||
ESP_IMAGE_APPLICATION
|
ESP_IMAGE_APPLICATION
|
||||||
} esp_image_type;
|
} 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.
|
* @brief Read ota_info partition and fill array from two otadata structures.
|
||||||
*
|
*
|
||||||
|
@@ -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
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
@@ -31,6 +31,37 @@
|
|||||||
|
|
||||||
static const char* TAG = "boot_comm";
|
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)
|
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);
|
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;
|
err = ESP_FAIL;
|
||||||
} else {
|
} else {
|
||||||
#ifndef CONFIG_IDF_ENV_FPGA
|
#ifndef CONFIG_IDF_ENV_FPGA
|
||||||
unsigned revision = efuse_hal_chip_revision();
|
if (type == ESP_IMAGE_APPLICATION) {
|
||||||
unsigned int major_rev = revision / 100;
|
if (!bootloader_common_check_chip_revision_validity(img_hdr, true)) {
|
||||||
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);
|
|
||||||
err = ESP_FAIL;
|
err = ESP_FAIL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type == ESP_IMAGE_APPLICATION) {
|
|
||||||
unsigned max_rev = img_hdr->max_chip_rev_full;
|
// Maximum revision check is skipped for bootloader images
|
||||||
if ((IS_FIELD_SET(max_rev) && (revision > max_rev) && !efuse_hal_get_disable_wafer_version_major())) {
|
if (type == ESP_IMAGE_BOOTLOADER) {
|
||||||
ESP_LOGE(TAG, "Image requires chip rev <= v%d.%d, but chip is v%d.%d",
|
if (!bootloader_common_check_chip_revision_validity(img_hdr, false)) {
|
||||||
max_rev / 100, max_rev % 100,
|
|
||||||
major_rev, minor_rev);
|
|
||||||
err = ESP_FAIL;
|
err = ESP_FAIL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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_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));
|
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;
|
if (!bootloader_common_check_chip_revision_validity(data, true)) {
|
||||||
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);
|
|
||||||
return ESP_ERR_INVALID_VERSION;
|
return ESP_ERR_INVALID_VERSION;
|
||||||
}
|
}
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
|
BIN
examples/system/ota/advanced_https_ota/efuse_esp32c3.bin
Normal file
BIN
examples/system/ota/advanced_https_ota/efuse_esp32c3.bin
Normal file
Binary file not shown.
@@ -10,6 +10,7 @@ import struct
|
|||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import pexpect
|
import pexpect
|
||||||
import pytest
|
import pytest
|
||||||
@@ -79,17 +80,19 @@ def start_https_server(ota_image_dir: str, server_ip: str, server_port: int) ->
|
|||||||
|
|
||||||
def start_chunked_server(ota_image_dir: str, server_port: int) -> subprocess.Popen:
|
def start_chunked_server(ota_image_dir: str, server_port: int) -> subprocess.Popen:
|
||||||
os.chdir(ota_image_dir)
|
os.chdir(ota_image_dir)
|
||||||
chunked_server = subprocess.Popen([
|
chunked_server = subprocess.Popen(
|
||||||
'openssl',
|
[
|
||||||
's_server',
|
'openssl',
|
||||||
'-WWW',
|
's_server',
|
||||||
'-key',
|
'-WWW',
|
||||||
key_file,
|
'-key',
|
||||||
'-cert',
|
key_file,
|
||||||
server_file,
|
'-cert',
|
||||||
'-port',
|
server_file,
|
||||||
str(server_port),
|
'-port',
|
||||||
])
|
str(server_port),
|
||||||
|
]
|
||||||
|
)
|
||||||
return chunked_server
|
return chunked_server
|
||||||
|
|
||||||
|
|
||||||
@@ -129,6 +132,48 @@ def start_redirect_server(ota_image_dir: str, server_ip: str, server_port: int,
|
|||||||
httpd.serve_forever()
|
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
|
@pytest.mark.ethernet_ota
|
||||||
@idf_parametrize('target', ['esp32'], indirect=['target'])
|
@idf_parametrize('target', ['esp32'], indirect=['target'])
|
||||||
def test_examples_protocol_advanced_https_ota_example(dut: Dut) -> None:
|
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'
|
bin_name = 'advanced_https_ota.bin'
|
||||||
# Truncated binary file to be generated from original binary file
|
# Truncated binary file to be generated from original binary file
|
||||||
truncated_bin_name = 'truncated.bin'
|
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 is set to 64000 to reduce consumed by the test case
|
||||||
truncated_bin_size = 64000
|
truncated_bin_size = 64000
|
||||||
binary_file = os.path.join(dut.app.binary_path, bin_name)
|
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'])
|
@idf_parametrize('target', ['esp32', 'esp32c3', 'esp32s3'], indirect=['target'])
|
||||||
def test_examples_protocol_advanced_https_ota_example_nimble_gatts(dut: Dut) -> None:
|
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: |
|
steps: |
|
||||||
1. join AP/Ethernet
|
1. join AP/Ethernet
|
||||||
2. Run BLE advertise and then GATT server.
|
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'])
|
@idf_parametrize('target', ['esp32', 'esp32c3', 'esp32s3'], indirect=['target'])
|
||||||
def test_examples_protocol_advanced_https_ota_example_bluedroid_gatts(dut: Dut) -> None:
|
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: |
|
steps: |
|
||||||
1. join AP/Ethernet
|
1. join AP/Ethernet
|
||||||
2. Run BLE advertise and then GATT server.
|
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
|
pass
|
||||||
finally:
|
finally:
|
||||||
chunked_server.kill()
|
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()
|
||||||
|
@@ -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
|
Reference in New Issue
Block a user