feat(ota): add authenticated firmware update workflow

- report firmware versions at startup and every six hours with a fresh device JWT
- support optional HMAC headers and retry once after authorization rejection
- handle forced and confirmed updates through managed OTA state
- stream images to the inactive partition and verify size, SHA-256, and format
- coordinate OTA HTTPS activity with voice teardown and restoration
- confirm pending images and expose configuration, status, and integration docs
This commit is contained in:
chenjintao
2026-08-14 15:25:34 +08:00
parent 0cfb10252c
commit 5a18036ec6
17 changed files with 1860 additions and 8 deletions
@@ -0,0 +1,298 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "platform.h"
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include "esp_crt_bundle.h"
#include "esp_heap_caps.h"
#include "esp_http_client.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "mbedtls/sha256.h"
#define PLATFORM_OTA_DOWNLOAD_BUFFER_SIZE 4096
#define PLATFORM_OTA_MAX_REDIRECTS 5
#define PLATFORM_OTA_IO_TIMEOUT_MS 10000
static const char *TAG = "platform_ota";
static bool platform_ota_deadline_expired(int64_t deadline_us)
{
return esp_timer_get_time() >= deadline_us;
}
static int platform_ota_remaining_timeout_ms(int64_t deadline_us)
{
int64_t remaining_us = deadline_us - esp_timer_get_time();
if (remaining_us <= 0) {
return 0;
}
int64_t remaining_ms = (remaining_us + 999) / 1000;
return remaining_ms < PLATFORM_OTA_IO_TIMEOUT_MS ?
(int)remaining_ms : PLATFORM_OTA_IO_TIMEOUT_MS;
}
esp_err_t platform_ota_install_image(const char *url,
const uint8_t expected_sha256[32],
uint64_t expected_size,
int timeout_ms,
platform_ota_progress_callback_t progress_cb,
platform_ota_cancel_callback_t cancel_cb,
void *user_data)
{
if (url == NULL || strncmp(url, "https://", 8) != 0 ||
expected_sha256 == NULL || expected_size == 0U || timeout_ms <= 0) {
return ESP_ERR_INVALID_ARG;
}
const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL);
if (partition == NULL || expected_size > partition->size) {
ESP_LOGE(TAG,
"OTA image does not fit inactive partition: image=%" PRIu64,
expected_size);
return ESP_ERR_INVALID_SIZE;
}
const int64_t deadline_us = esp_timer_get_time() + (int64_t)timeout_ms * 1000;
int io_timeout_ms = timeout_ms < PLATFORM_OTA_IO_TIMEOUT_MS ?
timeout_ms : PLATFORM_OTA_IO_TIMEOUT_MS;
esp_http_client_config_t http_config = {
.url = url,
.method = HTTP_METHOD_GET,
.timeout_ms = io_timeout_ms,
.crt_bundle_attach = esp_crt_bundle_attach,
.buffer_size = PLATFORM_OTA_DOWNLOAD_BUFFER_SIZE,
.max_redirection_count = PLATFORM_OTA_MAX_REDIRECTS,
.keep_alive_enable = true,
};
esp_http_client_handle_t client = esp_http_client_init(&http_config);
if (client == NULL) {
return ESP_ERR_NO_MEM;
}
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
ESP_LOGW(TAG, "OTA HTTPS open failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return err;
}
io_timeout_ms = platform_ota_remaining_timeout_ms(deadline_us);
if (io_timeout_ms <= 0) {
esp_http_client_close(client);
esp_http_client_cleanup(client);
return ESP_ERR_TIMEOUT;
}
esp_http_client_set_timeout_ms(client, io_timeout_ms);
int64_t content_length = esp_http_client_fetch_headers(client);
if (content_length < 0) {
int transport_errno = esp_http_client_get_errno(client);
ESP_LOGW(TAG,
"OTA response header fetch failed: result=%" PRIi64 " errno=%d",
content_length,
transport_errno);
esp_http_client_close(client);
esp_http_client_cleanup(client);
return ESP_ERR_INVALID_RESPONSE;
}
int status_code = esp_http_client_get_status_code(client);
if (status_code != 200) {
ESP_LOGW(TAG, "OTA download returned HTTP %d", status_code);
esp_http_client_close(client);
esp_http_client_cleanup(client);
return ESP_ERR_INVALID_RESPONSE;
}
if (content_length >= 0 && (uint64_t)content_length != expected_size) {
ESP_LOGE(TAG,
"OTA Content-Length mismatch: expected=%" PRIu64 " actual=%" PRIi64,
expected_size,
content_length);
esp_http_client_close(client);
esp_http_client_cleanup(client);
return ESP_ERR_INVALID_SIZE;
}
esp_ota_handle_t ota_handle = 0;
err = esp_ota_begin(partition, (size_t)expected_size, &ota_handle);
if (err != ESP_OK) {
esp_http_client_close(client);
esp_http_client_cleanup(client);
return err;
}
uint8_t *buffer = (uint8_t *)malloc(PLATFORM_OTA_DOWNLOAD_BUFFER_SIZE);
if (buffer == NULL) {
esp_ota_abort(ota_handle);
esp_http_client_close(client);
esp_http_client_cleanup(client);
return ESP_ERR_NO_MEM;
}
mbedtls_sha256_context sha_ctx;
mbedtls_sha256_init(&sha_ctx);
if (mbedtls_sha256_starts(&sha_ctx, 0) != 0) {
err = ESP_FAIL;
goto cleanup;
}
uint64_t received = 0U;
while (received < expected_size) {
bool cancelled = cancel_cb != NULL && cancel_cb(user_data);
if (cancelled || platform_ota_deadline_expired(deadline_us)) {
err = cancelled ? ESP_ERR_INVALID_STATE : ESP_ERR_TIMEOUT;
goto cleanup_sha;
}
io_timeout_ms = platform_ota_remaining_timeout_ms(deadline_us);
if (io_timeout_ms <= 0) {
err = ESP_ERR_TIMEOUT;
goto cleanup_sha;
}
esp_http_client_set_timeout_ms(client, io_timeout_ms);
size_t remaining = (size_t)(expected_size - received);
int read_capacity = remaining < PLATFORM_OTA_DOWNLOAD_BUFFER_SIZE ?
(int)remaining : PLATFORM_OTA_DOWNLOAD_BUFFER_SIZE;
int read_len = esp_http_client_read(client, (char *)buffer, read_capacity);
if (read_len == -ESP_ERR_HTTP_EAGAIN) {
continue;
}
if (read_len < 0) {
err = ESP_ERR_INVALID_RESPONSE;
goto cleanup_sha;
}
if (read_len == 0) {
err = esp_http_client_is_complete_data_received(client) ?
ESP_ERR_INVALID_SIZE : ESP_ERR_HTTP_EAGAIN;
goto cleanup_sha;
}
if (mbedtls_sha256_update(&sha_ctx, buffer, (size_t)read_len) != 0) {
err = ESP_FAIL;
goto cleanup_sha;
}
err = esp_ota_write(ota_handle, buffer, (size_t)read_len);
if (err != ESP_OK) {
goto cleanup_sha;
}
received += (uint64_t)read_len;
if (progress_cb != NULL) {
progress_cb(received, expected_size, user_data);
}
}
if (content_length < 0) {
while (!esp_http_client_is_complete_data_received(client)) {
io_timeout_ms = platform_ota_remaining_timeout_ms(deadline_us);
if (io_timeout_ms <= 0) {
err = ESP_ERR_TIMEOUT;
goto cleanup_sha;
}
esp_http_client_set_timeout_ms(client, io_timeout_ms);
int overflow_len = esp_http_client_read(client, (char *)buffer, 1);
if (overflow_len == -ESP_ERR_HTTP_EAGAIN) {
continue;
}
if (overflow_len < 0) {
err = ESP_ERR_INVALID_RESPONSE;
goto cleanup_sha;
}
if (overflow_len > 0) {
err = ESP_ERR_INVALID_SIZE;
goto cleanup_sha;
}
break;
}
}
uint8_t actual_sha256[32];
if (mbedtls_sha256_finish(&sha_ctx, actual_sha256) != 0) {
err = ESP_FAIL;
goto cleanup_sha;
}
mbedtls_sha256_free(&sha_ctx);
if (memcmp(actual_sha256, expected_sha256, sizeof(actual_sha256)) != 0) {
ESP_LOGE(TAG, "OTA SHA-256 mismatch");
err = ESP_ERR_INVALID_CRC;
goto cleanup;
}
err = esp_ota_end(ota_handle);
ota_handle = 0;
if (err != ESP_OK) {
ESP_LOGE(TAG, "OTA image validation failed: %s", esp_err_to_name(err));
goto cleanup_no_abort;
}
err = esp_ota_set_boot_partition(partition);
if (err == ESP_OK) {
ESP_LOGI(TAG,
"OTA image installed in %s (%" PRIu64 " bytes)",
partition->label,
received);
}
goto cleanup_no_abort;
cleanup_sha:
mbedtls_sha256_free(&sha_ctx);
cleanup:
if (ota_handle != 0) {
esp_ota_abort(ota_handle);
}
cleanup_no_abort:
free(buffer);
esp_http_client_close(client);
esp_http_client_cleanup(client);
return err;
}
esp_err_t platform_ota_confirm_running_image(void)
{
const esp_partition_t *running = esp_ota_get_running_partition();
if (running == NULL) {
return ESP_ERR_NOT_FOUND;
}
esp_ota_img_states_t state = ESP_OTA_IMG_UNDEFINED;
esp_err_t err = esp_ota_get_state_partition(running, &state);
if (err == ESP_ERR_NOT_FOUND || state != ESP_OTA_IMG_PENDING_VERIFY) {
return ESP_OK;
}
if (err != ESP_OK) {
return err;
}
return esp_ota_mark_app_valid_cancel_rollback();
}
esp_err_t platform_ota_get_memory_headroom(uint32_t *free_internal,
uint32_t *largest_internal,
uint32_t *largest_dma)
{
if (free_internal == NULL || largest_internal == NULL || largest_dma == NULL) {
return ESP_ERR_INVALID_ARG;
}
*free_internal = (uint32_t)heap_caps_get_free_size(
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
*largest_internal = (uint32_t)heap_caps_get_largest_free_block(
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
*largest_dma = (uint32_t)heap_caps_get_largest_free_block(MALLOC_CAP_DMA);
return ESP_OK;
}
void platform_restart(void)
{
esp_restart();
}