feat(firmware): implement OTA download, ECDSA verify, and flash

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 11:18:44 -07:00
parent 66e6808e13
commit a21dcfa349

View File

@@ -49,6 +49,14 @@ bool ota_verify_signature_with_key(const uint8_t hash32[32], const uint8_t sig64
// ── device-only code ───────────────────────────────────────────────────────
#ifndef NATIVE_TEST
#include <Arduino.h>
#include <HTTPClient.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#include <esp_ota_ops.h>
#include <mbedtls/sha256.h>
#include <mbedtls/base64.h>
#include "hmac.h"
#include "ota_pubkey.h"
#include "version.h"
@@ -71,6 +79,148 @@ void ota_updater_init(const char* server_base, const char* device_id,
s_last_check_ms = 0; // force first check on next call
}
bool ota_updater_check_and_apply() { return false; } // filled in Task 8
static void add_hmac_headers(HTTPClient& http, const char* method, const char* path) {
uint32_t ts = (uint32_t)(esp_timer_get_time() / 1000000ULL);
String sig = hmac_sign(s_hmac_secret, method, path, ts, "");
if (sig.isEmpty()) {
log_e("[OTA] HMAC sign failed");
return;
}
http.addHeader("X-Device-Id", s_device_id);
http.addHeader("X-Timestamp", String(ts));
http.addHeader("X-HMAC-Signature", sig);
}
static bool download_and_flash(const char* fw_url, size_t expected_size,
const uint8_t sig64[64]) {
const esp_partition_t* target = esp_ota_get_next_update_partition(nullptr);
if (!target) {
log_e("[OTA] No update partition found");
return false;
}
esp_ota_handle_t handle;
if (esp_ota_begin(target, OTA_WITH_SEQUENTIAL_WRITES, &handle) != ESP_OK) {
log_e("[OTA] esp_ota_begin failed");
return false;
}
mbedtls_sha256_context sha_ctx;
mbedtls_sha256_init(&sha_ctx);
mbedtls_sha256_starts(&sha_ctx, 0); // 0 = SHA-256
HTTPClient http;
http.begin(fw_url);
add_hmac_headers(http, "GET", "/ota/firmware");
int code = http.GET();
if (code != HTTP_CODE_OK) {
log_e("[OTA] Firmware fetch failed: HTTP %d", code);
http.end();
mbedtls_sha256_free(&sha_ctx);
esp_ota_abort(handle);
return false;
}
WiFiClient* stream = http.getStreamPtr();
uint8_t buf[4096];
size_t written = 0;
while (written < expected_size) {
size_t want = min((size_t)sizeof(buf), expected_size - written);
int got = stream->readBytes(buf, want);
if (got <= 0) break;
esp_ota_write(handle, buf, (size_t)got);
mbedtls_sha256_update(&sha_ctx, buf, (size_t)got);
written += (size_t)got;
}
http.end();
uint8_t hash[32];
mbedtls_sha256_finish(&sha_ctx, hash);
mbedtls_sha256_free(&sha_ctx);
if (written != expected_size) {
log_e("[OTA] Download truncated (%zu/%zu bytes)", written, expected_size);
esp_ota_abort(handle);
return false;
}
if (!ota_verify_signature_with_key(hash, sig64, kOtaPublicKey)) {
log_e("[OTA] SIGNATURE INVALID — staying on current firmware");
esp_ota_abort(handle);
return false;
}
if (esp_ota_end(handle) != ESP_OK ||
esp_ota_set_boot_partition(target) != ESP_OK) {
log_e("[OTA] Commit failed");
return false;
}
log_i("[OTA] Firmware verified and committed — rebooting");
esp_restart();
return true; // unreachable
}
bool ota_updater_check_and_apply() {
if (!s_server_base || !s_device_id || !s_hmac_secret) return false;
if (s_last_check_ms != 0 &&
(uint32_t)(millis() - s_last_check_ms) < s_interval_ms) {
return false;
}
s_last_check_ms = millis();
char check_path[128];
snprintf(check_path, sizeof(check_path), "/ota/check?version=%s", FW_VERSION);
char check_url[256];
snprintf(check_url, sizeof(check_url), "%s%s", s_server_base, check_path);
HTTPClient http;
http.begin(check_url);
add_hmac_headers(http, "GET", check_path);
int code = http.GET();
if (code != HTTP_CODE_OK) {
log_w("[OTA] Check failed: HTTP %d", code);
http.end();
return false;
}
JsonDocument doc;
DeserializationError err = deserializeJson(doc, http.getStream());
http.end();
if (err) {
log_w("[OTA] JSON parse error: %s", err.c_str());
return false;
}
if (!doc["update"].as<bool>()) {
log_i("[OTA] Firmware up to date (%s)", FW_VERSION);
return false;
}
const char* remote_ver = doc["version"] | "";
size_t fw_size = doc["size"] | 0;
const char* sig_b64 = doc["sig_b64"] | "";
if (fw_size == 0 || strlen(sig_b64) == 0) {
log_e("[OTA] Invalid update manifest");
return false;
}
log_i("[OTA] Update: %s → %s (%zu bytes)", FW_VERSION, remote_ver, fw_size);
uint8_t sig64[64];
size_t sig_len = 0;
if (mbedtls_base64_decode(sig64, sizeof(sig64), &sig_len,
(const uint8_t*)sig_b64, strlen(sig_b64)) != 0 ||
sig_len != 64) {
log_e("[OTA] Bad signature encoding (len=%zu)", sig_len);
return false;
}
char fw_url[256];
snprintf(fw_url, sizeof(fw_url), "%s/ota/firmware", s_server_base);
return download_and_flash(fw_url, fw_size, sig64);
}
#endif // NATIVE_TEST