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
+471
View File
@@ -0,0 +1,471 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "domain.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "cJSON.h"
#include "esp_log.h"
#include "esp_random.h"
#include "mbedtls/base64.h"
#include "mbedtls/md.h"
#include "mbedtls/sha256.h"
#include "platform.h"
#define DOMAIN_OTA_RESPONSE_CAPACITY 4096
#define DOMAIN_OTA_NONCE_LEN 36
#define DOMAIN_OTA_AUTH_VALUE_MAX_LEN 128
#define DOMAIN_OTA_MIN_VALID_EPOCH 1700000000LL
static const char *TAG = "domain_ota";
typedef struct {
domain_ota_progress_callback_t progress_cb;
domain_ota_cancel_callback_t cancel_cb;
void *user_data;
} domain_ota_install_bridge_t;
static esp_err_t domain_ota_copy_json_string(cJSON *object,
const char *name,
char *out,
size_t out_len)
{
cJSON *item = cJSON_GetObjectItemCaseSensitive(object, name);
if (!cJSON_IsString(item) || item->valuestring == NULL ||
strlcpy(out, item->valuestring, out_len) >= out_len) {
return ESP_ERR_INVALID_RESPONSE;
}
return ESP_OK;
}
static esp_err_t domain_ota_base64(const uint8_t *input,
size_t input_len,
char *out,
size_t out_len)
{
size_t encoded_len = 0U;
int ret = mbedtls_base64_encode((uint8_t *)out,
out_len,
&encoded_len,
input,
input_len);
if (ret != 0 || encoded_len >= out_len) {
return ESP_FAIL;
}
out[encoded_len] = '\0';
return ESP_OK;
}
static esp_err_t domain_ota_parse_url_target(const char *url,
char *path,
size_t path_len,
char *query,
size_t query_len)
{
const char *authority = strstr(url, "://");
if (authority == NULL) {
return ESP_ERR_INVALID_ARG;
}
const char *target = strchr(authority + 3, '/');
if (target == NULL) {
target = "/";
}
const char *query_begin = strchr(target, '?');
size_t target_len = query_begin == NULL ? strlen(target) :
(size_t)(query_begin - target);
if (target_len == 0U || target_len >= path_len) {
return ESP_ERR_INVALID_SIZE;
}
memcpy(path, target, target_len);
path[target_len] = '\0';
query[0] = '\0';
if (query_begin != NULL && strlcpy(query, query_begin + 1, query_len) >= query_len) {
return ESP_ERR_INVALID_SIZE;
}
return ESP_OK;
}
static void domain_ota_generate_nonce(char out[DOMAIN_OTA_NONCE_LEN + 1])
{
uint8_t random_bytes[16];
esp_fill_random(random_bytes, sizeof(random_bytes));
snprintf(out,
DOMAIN_OTA_NONCE_LEN + 1,
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
random_bytes[0], random_bytes[1], random_bytes[2], random_bytes[3],
random_bytes[4], random_bytes[5], random_bytes[6], random_bytes[7],
random_bytes[8], random_bytes[9], random_bytes[10], random_bytes[11],
random_bytes[12], random_bytes[13], random_bytes[14], random_bytes[15]);
}
static esp_err_t domain_ota_build_internal_auth(const char *url,
const char *payload,
const char *secret,
char *timestamp,
size_t timestamp_len,
char *nonce,
size_t nonce_len,
char *signature,
size_t signature_len)
{
time_t now = time(NULL);
if ((int64_t)now < DOMAIN_OTA_MIN_VALID_EPOCH) {
return ESP_ERR_INVALID_STATE;
}
if (snprintf(timestamp, timestamp_len, "%lld", (long long)now) >= (int)timestamp_len) {
return ESP_ERR_INVALID_SIZE;
}
if (nonce_len < DOMAIN_OTA_NONCE_LEN + 1U) {
return ESP_ERR_INVALID_SIZE;
}
domain_ota_generate_nonce(nonce);
uint8_t body_digest[32];
if (mbedtls_sha256((const uint8_t *)payload,
strlen(payload),
body_digest,
0) != 0) {
return ESP_FAIL;
}
char body_hash[DOMAIN_OTA_AUTH_VALUE_MAX_LEN];
esp_err_t err = domain_ota_base64(body_digest,
sizeof(body_digest),
body_hash,
sizeof(body_hash));
if (err != ESP_OK) {
return err;
}
char path[256];
char query[256];
err = domain_ota_parse_url_target(url, path, sizeof(path), query, sizeof(query));
if (err != ESP_OK) {
return err;
}
int payload_len = snprintf(NULL,
0,
"POST\n%s\n%s\n%s\n%s\n%s",
path,
query,
body_hash,
timestamp,
nonce);
if (payload_len <= 0) {
return ESP_FAIL;
}
char *signature_payload = (char *)malloc((size_t)payload_len + 1U);
if (signature_payload == NULL) {
return ESP_ERR_NO_MEM;
}
snprintf(signature_payload,
(size_t)payload_len + 1U,
"POST\n%s\n%s\n%s\n%s\n%s",
path,
query,
body_hash,
timestamp,
nonce);
uint8_t digest[32];
const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
int hmac_ret = md_info == NULL ? -1 :
mbedtls_md_hmac(md_info,
(const uint8_t *)secret,
strlen(secret),
(const uint8_t *)signature_payload,
(size_t)payload_len,
digest);
free(signature_payload);
if (hmac_ret != 0) {
return ESP_FAIL;
}
return domain_ota_base64(digest, sizeof(digest), signature, signature_len);
}
static esp_err_t domain_ota_build_report_payload(const domain_ota_report_request_t *request,
char **out_payload)
{
cJSON *root = cJSON_CreateObject();
if (root == NULL) {
return ESP_ERR_NO_MEM;
}
bool ok = cJSON_AddStringToObject(root, "deviceId", request->device_id) != NULL &&
cJSON_AddStringToObject(root, "version", request->current_version) != NULL;
if (ok && request->serial_number != NULL && request->serial_number[0] != '\0') {
ok = cJSON_AddStringToObject(root, "serialNumber", request->serial_number) != NULL;
}
char *payload = ok ? cJSON_PrintUnformatted(root) : NULL;
cJSON_Delete(root);
if (payload == NULL) {
return ESP_ERR_NO_MEM;
}
*out_payload = payload;
return ESP_OK;
}
static esp_err_t domain_ota_parse_report_response(const char *body,
domain_ota_report_response_t *response)
{
cJSON *root = cJSON_Parse(body);
if (root == NULL) {
return ESP_ERR_INVALID_RESPONSE;
}
cJSON *code = cJSON_GetObjectItemCaseSensitive(root, "code");
response->business_code = cJSON_IsNumber(code) ? code->valueint : 0;
cJSON *data = cJSON_GetObjectItemCaseSensitive(root, "data");
cJSON *not_uptodate = cJSON_IsObject(data) ?
cJSON_GetObjectItemCaseSensitive(data, "notUptodate") : NULL;
if (response->business_code != 200 || !cJSON_IsNumber(not_uptodate) ||
not_uptodate->valueint != 1) {
response->outcome = DOMAIN_OTA_REPORT_SERVER_ERROR;
cJSON_Delete(root);
return ESP_OK;
}
domain_ota_firmware_t *firmware = &response->firmware;
esp_err_t err = domain_ota_copy_json_string(data,
"version",
firmware->version,
sizeof(firmware->version));
if (err == ESP_OK) {
err = domain_ota_copy_json_string(data,
"url",
firmware->url,
sizeof(firmware->url));
}
if (err == ESP_OK) {
err = domain_ota_copy_json_string(data,
"sha256",
firmware->sha256,
sizeof(firmware->sha256));
}
cJSON *size_bytes = cJSON_GetObjectItemCaseSensitive(data, "sizeBytes");
cJSON *forced = cJSON_GetObjectItemCaseSensitive(data, "force");
cJSON *confirmed = cJSON_GetObjectItemCaseSensitive(data, "update");
if (err != ESP_OK || !cJSON_IsNumber(size_bytes) || size_bytes->valuedouble <= 0 ||
size_bytes->valuedouble > (double)UINT32_MAX ||
(double)(uint64_t)size_bytes->valuedouble != size_bytes->valuedouble ||
strlen(firmware->sha256) != 64U) {
cJSON_Delete(root);
return ESP_ERR_INVALID_RESPONSE;
}
firmware->size_bytes = (uint64_t)size_bytes->valuedouble;
firmware->forced = cJSON_IsNumber(forced) && forced->valueint == 1;
firmware->confirmed = cJSON_IsNumber(confirmed) && confirmed->valueint == 1;
response->outcome = DOMAIN_OTA_REPORT_UPDATE_AVAILABLE;
cJSON_Delete(root);
return ESP_OK;
}
esp_err_t domain_runtime_ota_report(const domain_ota_report_request_t *request,
domain_ota_report_response_t *response)
{
if (response != NULL) {
memset(response, 0, sizeof(*response));
response->http_status = -1;
}
if (request == NULL || response == NULL || request->report_url == NULL ||
strncmp(request->report_url, "https://", 8) != 0 ||
request->device_id == NULL || request->device_id[0] == '\0' ||
request->current_version == NULL || request->current_version[0] == '\0' ||
request->device_jwt == NULL || request->device_jwt[0] == '\0' ||
request->timeout_ms <= 0) {
return ESP_ERR_INVALID_ARG;
}
char *payload = NULL;
esp_err_t err = domain_ota_build_report_payload(request, &payload);
if (err != ESP_OK) {
return err;
}
char bearer[1600];
if (snprintf(bearer, sizeof(bearer), "Bearer %s", request->device_jwt) >=
(int)sizeof(bearer)) {
cJSON_free(payload);
return ESP_ERR_INVALID_SIZE;
}
platform_https_header_t headers[4] = {
{.name = "Authorization", .value = bearer},
};
size_t header_count = 1U;
char timestamp[32] = {0};
char nonce[DOMAIN_OTA_NONCE_LEN + 1] = {0};
char signature[DOMAIN_OTA_AUTH_VALUE_MAX_LEN] = {0};
if (request->internal_auth_secret != NULL && request->internal_auth_secret[0] != '\0') {
err = domain_ota_build_internal_auth(request->report_url,
payload,
request->internal_auth_secret,
timestamp,
sizeof(timestamp),
nonce,
sizeof(nonce),
signature,
sizeof(signature));
if (err != ESP_OK) {
cJSON_free(payload);
return err;
}
headers[header_count++] = (platform_https_header_t) {
.name = "X-Internal-Timestamp", .value = timestamp,
};
headers[header_count++] = (platform_https_header_t) {
.name = "X-Internal-Nonce", .value = nonce,
};
headers[header_count++] = (platform_https_header_t) {
.name = "X-Internal-Signature", .value = signature,
};
}
char *body = (char *)calloc(1, DOMAIN_OTA_RESPONSE_CAPACITY);
if (body == NULL) {
cJSON_free(payload);
return ESP_ERR_NO_MEM;
}
platform_https_post_json_request_t platform_request = {
.url = request->report_url,
.payload = payload,
.headers = headers,
.header_count = header_count,
.timeout_ms = request->timeout_ms,
.tx_buffer_size = 2048,
.rx_buffer_size = 2048,
};
platform_https_response_t platform_response = {
.body = body,
.body_capacity = DOMAIN_OTA_RESPONSE_CAPACITY,
};
err = platform_https_post_json(&platform_request, &platform_response);
cJSON_free(payload);
if (err != ESP_OK) {
free(body);
return err;
}
response->http_status = platform_response.status_code;
ESP_LOGI(TAG,
"OTA version report response: http=%d body_len=%u body=%.*s",
platform_response.status_code,
(unsigned)platform_response.body_len,
(int)platform_response.body_len,
body);
if (platform_response.status_code == 204) {
response->outcome = DOMAIN_OTA_REPORT_NO_UPDATE;
free(body);
return ESP_OK;
}
if (platform_response.status_code != 200) {
response->outcome = DOMAIN_OTA_REPORT_SERVER_ERROR;
free(body);
return ESP_OK;
}
err = domain_ota_parse_report_response(body, response);
free(body);
return err;
}
static int domain_ota_hex_value(char value)
{
if (value >= '0' && value <= '9') {
return value - '0';
}
if (value >= 'a' && value <= 'f') {
return value - 'a' + 10;
}
if (value >= 'A' && value <= 'F') {
return value - 'A' + 10;
}
return -1;
}
static esp_err_t domain_ota_parse_sha256(const char *hex, uint8_t out[32])
{
if (hex == NULL || strlen(hex) != 64U) {
return ESP_ERR_INVALID_ARG;
}
for (size_t i = 0; i < 32U; ++i) {
int high = domain_ota_hex_value(hex[i * 2U]);
int low = domain_ota_hex_value(hex[i * 2U + 1U]);
if (high < 0 || low < 0) {
return ESP_ERR_INVALID_ARG;
}
out[i] = (uint8_t)((high << 4) | low);
}
return ESP_OK;
}
static void domain_ota_progress_bridge(uint64_t received, uint64_t total, void *user_data)
{
domain_ota_install_bridge_t *bridge = (domain_ota_install_bridge_t *)user_data;
if (bridge->progress_cb != NULL) {
bridge->progress_cb(received, total, bridge->user_data);
}
}
static bool domain_ota_cancel_bridge(void *user_data)
{
domain_ota_install_bridge_t *bridge = (domain_ota_install_bridge_t *)user_data;
return bridge->cancel_cb != NULL && bridge->cancel_cb(bridge->user_data);
}
esp_err_t domain_runtime_ota_install(const domain_ota_firmware_t *firmware,
int timeout_ms,
domain_ota_progress_callback_t progress_cb,
domain_ota_cancel_callback_t cancel_cb,
void *user_data)
{
if (firmware == NULL || strncmp(firmware->url, "https://", 8) != 0 ||
firmware->size_bytes == 0U || timeout_ms <= 0) {
return ESP_ERR_INVALID_ARG;
}
uint8_t expected_sha256[32];
esp_err_t err = domain_ota_parse_sha256(firmware->sha256, expected_sha256);
if (err != ESP_OK) {
return err;
}
domain_ota_install_bridge_t bridge = {
.progress_cb = progress_cb,
.cancel_cb = cancel_cb,
.user_data = user_data,
};
return platform_ota_install_image(firmware->url,
expected_sha256,
firmware->size_bytes,
timeout_ms,
domain_ota_progress_bridge,
domain_ota_cancel_bridge,
&bridge);
}
esp_err_t domain_runtime_ota_confirm_running_image(void)
{
return platform_ota_confirm_running_image();
}
esp_err_t domain_runtime_ota_get_memory_headroom(domain_ota_memory_headroom_t *out)
{
if (out == NULL) {
return ESP_ERR_INVALID_ARG;
}
return platform_ota_get_memory_headroom(&out->free_internal,
&out->largest_internal,
&out->largest_dma);
}
void domain_runtime_restart(void)
{
platform_restart();
}
@@ -859,6 +859,11 @@ void domain_runtime_read_device_identity(char *device_id,
platform_read_device_identity(device_id, device_id_len, serial_number, serial_number_len);
}
esp_err_t domain_runtime_read_firmware_version(char *out, size_t out_len)
{
return platform_read_firmware_version(out, out_len);
}
esp_err_t domain_runtime_write_device_identity(const char *device_id, const char *serial_number)
{
return platform_write_device_identity(device_id, serial_number);