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