feat(firmware): OTA SHA-256 verification and captive portal URL decoding

- Add git-based version header generation for firmware builds
- Implement SHA-256 hash verification for OTA downloads with mbedtls
- Add URL decoding for captive portal form parsing (spaces, special chars)
- Add mbedtls dependency for SHA-256 verification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-03-28 19:56:11 -04:00
parent 7b325703bd
commit f76ab62698
5 changed files with 101 additions and 9 deletions

View file

@ -3,5 +3,30 @@ cmake_minimum_required(VERSION 3.16)
# SPAXEL ESP32-S3 Firmware
# WiFi CSI-based indoor positioning node
# Get version from git tags, fallback to 0.1.0
execute_process(
COMMAND git describe --tags --dirty 2>/dev/null
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GIT_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(GIT_VERSION)
set(PROJECT_VERSION "${GIT_VERSION}")
else()
set(PROJECT_VERSION "0.1.0")
endif()
message(STATUS "Building SPAXEL firmware version: ${PROJECT_VERSION}")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(spaxel-firmware)
project(spaxel-firmware VERSION "${PROJECT_VERSION}")
# Generate version header
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/main/version.h.in
${CMAKE_CURRENT_BINARY_DIR}/spaxel-firmware/main/version.h
@ONLY
)

View file

@ -5,6 +5,6 @@ idf_component_register(
"csi.c"
"ble.c"
"provision.c"
INCLUDE_DIRS "."
REQUIRES esp_wifi esp_netif nvs_flash mdns esp_http_client esp_timer bt driver log esp_http_server
INCLUDE_DIRS "." "${CMAKE_BINARY_DIR}/spaxel-firmware/main"
REQUIRES esp_wifi esp_netif nvs_flash mdns esp_http_client esp_timer bt driver log esp_http_server mbedtls
)

View file

@ -0,0 +1,4 @@
// Auto-generated at build time - do not edit
#pragma once
#define SPAXEL_FIRMWARE_VERSION "@PROJECT_VERSION@"

View file

@ -7,11 +7,13 @@
#include "esp_system.h"
#include "esp_ota_ops.h"
#include "esp_http_client.h"
#include "mbedtls/sha256.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semaphore.h"
#include "cJSON.h"
#include <string.h>
#include <strings.h>
// ESP-IDF WebSocket client
#include "esp_websocket_client.h"
@ -633,15 +635,29 @@ static void ota_task(void *arg) {
return;
}
// Initialize SHA-256 for verification
mbedtls_sha256_context sha_ctx;
bool do_sha_verify = (strlen(s_ota_sha256) == 64);
if (do_sha_verify) {
mbedtls_sha256_init(&sha_ctx);
mbedtls_sha256_starts(&sha_ctx, 0); // 0 = SHA-256
}
// Download and write
char *buf = malloc(4096);
int total_read = 0;
int read;
while ((read = esp_http_client_read(http, buf, 4096)) > 0) {
// Update SHA-256 hash if verifying
if (do_sha_verify) {
mbedtls_sha256_update(&sha_ctx, (unsigned char *)buf, read);
}
err = esp_ota_write(ota_handle, buf, read);
if (err != ESP_OK) {
free(buf);
if (do_sha_verify) mbedtls_sha256_free(&sha_ctx);
esp_ota_abort(ota_handle);
esp_http_client_cleanup(http);
websocket_send_ota_status("failed", 0, "write_failed");
@ -660,7 +676,29 @@ static void ota_task(void *arg) {
// Verify and complete
websocket_send_ota_status("verifying", 100, NULL);
// SHA256 verification would go here if s_ota_sha256 is set
// SHA-256 verification
if (do_sha_verify) {
unsigned char hash[32];
mbedtls_sha256_finish(&sha_ctx, hash);
mbedtls_sha256_free(&sha_ctx);
// Convert binary hash to hex string
char hash_hex[65];
for (int i = 0; i < 32; i++) {
sprintf(hash_hex + (i * 2), "%02x", hash[i]);
}
hash_hex[64] = '\0';
// Compare with expected hash (case-insensitive)
if (strcasecmp(hash_hex, s_ota_sha256) != 0) {
ESP_LOGE(TAG, "SHA-256 mismatch: expected %s, got %s", s_ota_sha256, hash_hex);
esp_ota_abort(ota_handle);
websocket_send_ota_status("failed", 0, "sha256_mismatch");
vTaskDelete(NULL);
return;
}
ESP_LOGI(TAG, "SHA-256 verified: %s", hash_hex);
}
err = esp_ota_end(ota_handle);
if (err != ESP_OK) {

View file

@ -10,6 +10,8 @@
#include "lwip/sockets.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>
#include <ctype.h>
static const char *TAG = "wifi";
@ -297,6 +299,26 @@ static void captive_dns_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p,
pbuf_free(p);
}
// URL decode helper for captive portal form parsing
static void url_decode(char *dst, const char *src, size_t dst_size) {
size_t i = 0;
size_t j = 0;
while (src[i] && j < dst_size - 1) {
if (src[i] == '+') {
dst[j++] = ' ';
i++;
} else if (src[i] == '%' && isxdigit(src[i+1]) && isxdigit(src[i+2])) {
char hex[3] = {src[i+1], src[i+2], 0};
dst[j++] = (char)strtol(hex, NULL, 16);
i += 3;
} else {
dst[j++] = src[i++];
}
}
dst[j] = '\0';
}
static esp_err_t captive_root_handler(httpd_req_t *req) {
const char *html =
"<!DOCTYPE html>"
@ -338,17 +360,20 @@ static esp_err_t captive_save_handler(httpd_req_t *req) {
char ssid[33] = {0};
char password[65] = {0};
char ms_ip[47] = {0};
char decoded[128];
// Simple parsing (url-encoded)
// Parse URL-encoded form data
char *p = strtok(buf, "&");
while (p) {
if (strncmp(p, "ssid=", 5) == 0) {
// URL decode would go here, simplified for now
strncpy(ssid, p + 5, sizeof(ssid) - 1);
url_decode(decoded, p + 5, sizeof(decoded));
strncpy(ssid, decoded, sizeof(ssid) - 1);
} else if (strncmp(p, "password=", 9) == 0) {
strncpy(password, p + 9, sizeof(password) - 1);
url_decode(decoded, p + 9, sizeof(decoded));
strncpy(password, decoded, sizeof(password) - 1);
} else if (strncmp(p, "ms_ip=", 6) == 0) {
strncpy(ms_ip, p + 6, sizeof(ms_ip) - 1);
url_decode(decoded, p + 6, sizeof(decoded));
strncpy(ms_ip, decoded, sizeof(ms_ip) - 1);
}
p = strtok(NULL, "&");
}