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:
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user