fix: Address P2 security audit findings

- VULN-012: Split STATUS into minimal (unauthed: hostname, uptime,
  rssi, version, motion, presence) and full (authed: all internals,
  build info, target IP, heap, NVS stats)
- VULN-011: Remove mDNS service advertisement and hardcoded "ESP32 CSI
  Sensor" instance name; use hostname only
- VULN-021: Increase HMAC tag from 64 bits (16 hex) to 128 bits
  (32 hex) — BREAKING: client scripts must update HMAC computation
- VULN-023: Enable PMF (802.11w) in sdkconfig.defaults to prevent
  deauth attacks at protocol level
This commit is contained in:
user
2026-02-14 20:10:14 +01:00
parent ed8669c0af
commit 57927c7c22
2 changed files with 47 additions and 29 deletions

View File

@@ -1455,8 +1455,8 @@ static struct {
static int s_auth_nonce_idx = 0; static int s_auth_nonce_idx = 0;
/** /**
* Verify HMAC-signed command. Format: "HMAC:<16hex>:<uptime_s>:<cmd>" * Verify HMAC-signed command. Format: "HMAC:<32hex>:<uptime_s>:<cmd>"
* HMAC = truncated SHA-256(secret, "<uptime_s>:<cmd>") * HMAC = first 16 bytes of SHA-256(secret, "<uptime_s>:<cmd>") as 32 hex chars
* Timestamp must be within 5s of device uptime (replay protection). * Timestamp must be within 5s of device uptime (replay protection).
* Recently used timestamp+HMAC pairs are cached to reject exact replays. * Recently used timestamp+HMAC pairs are cached to reject exact replays.
* Returns pointer to actual command on success, or NULL on failure * Returns pointer to actual command on success, or NULL on failure
@@ -1475,18 +1475,18 @@ static const char *auth_verify(const char *input, char *reply, size_t reply_size
return NULL; return NULL;
} }
/* Parse: HMAC:<16 hex chars>:<uptime_s>:<cmd> */ /* Parse: HMAC:<32 hex chars>:<uptime_s>:<cmd> */
if (strlen(input) < 5 + 16 + 1) { if (strlen(input) < 5 + 32 + 1) {
snprintf(reply, reply_size, "ERR AUTH malformed"); snprintf(reply, reply_size, "ERR AUTH malformed");
return NULL; return NULL;
} }
if (input[5 + 16] != ':') { if (input[5 + 32] != ':') {
snprintf(reply, reply_size, "ERR AUTH malformed"); snprintf(reply, reply_size, "ERR AUTH malformed");
return NULL; return NULL;
} }
/* payload = "<uptime_s>:<cmd>" */ /* payload = "<uptime_s>:<cmd>" */
const char *payload = input + 5 + 16 + 1; const char *payload = input + 5 + 32 + 1;
const char *cmd_sep = strchr(payload, ':'); const char *cmd_sep = strchr(payload, ':');
if (!cmd_sep) { if (!cmd_sep) {
snprintf(reply, reply_size, "ERR AUTH malformed (need timestamp:cmd)"); snprintf(reply, reply_size, "ERR AUTH malformed (need timestamp:cmd)");
@@ -1515,15 +1515,15 @@ static const char *auth_verify(const char *input, char *reply, size_t reply_size
mbedtls_md_hmac_finish(&ctx, hmac); mbedtls_md_hmac_finish(&ctx, hmac);
mbedtls_md_free(&ctx); mbedtls_md_free(&ctx);
/* Format first 8 bytes as 16 hex chars */ /* Format first 16 bytes as 32 hex chars (128-bit tag) */
char expected[17]; char expected[33];
for (int i = 0; i < 8; i++) { for (int i = 0; i < 16; i++) {
snprintf(expected + i * 2, 3, "%02x", hmac[i]); snprintf(expected + i * 2, 3, "%02x", hmac[i]);
} }
/* Constant-time comparison (prevents timing side-channel) */ /* Constant-time comparison (prevents timing side-channel) */
volatile uint8_t diff = 0; volatile uint8_t diff = 0;
for (int i = 0; i < 16; i++) { for (int i = 0; i < 32; i++) {
diff |= (uint8_t)input[5 + i] ^ (uint8_t)expected[i]; diff |= (uint8_t)input[5 + i] ^ (uint8_t)expected[i];
} }
if (diff != 0) { if (diff != 0) {
@@ -1703,7 +1703,7 @@ static void powertest_task(void *arg)
vTaskDelete(NULL); vTaskDelete(NULL);
} }
static int cmd_handle(const char *cmd, char *reply, size_t reply_size) static int cmd_handle(const char *cmd, char *reply, size_t reply_size, bool authed)
{ {
/* REBOOT */ /* REBOOT */
if (strncmp(cmd, "REBOOT", 6) == 0) { if (strncmp(cmd, "REBOOT", 6) == 0) {
@@ -1744,13 +1744,21 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
return strlen(reply); return strlen(reply);
} }
/* STATUS */ /* STATUS — minimal without auth, full with auth */
if (strncmp(cmd, "STATUS", 6) == 0) { if (strncmp(cmd, "STATUS", 6) == 0) {
int64_t up = esp_timer_get_time() / 1000000LL; int64_t up = esp_timer_get_time() / 1000000LL;
int days = (int)(up / 86400); int days = (int)(up / 86400);
int hours = (int)((up % 86400) / 3600); int hours = (int)((up % 86400) / 3600);
int mins = (int)((up % 3600) / 60); int mins = (int)((up % 3600) / 60);
uint32_t heap = esp_get_free_heap_size();
char uptime_str[32];
if (days > 0) {
snprintf(uptime_str, sizeof(uptime_str), "%dd%dh%dm", days, hours, mins);
} else if (hours > 0) {
snprintf(uptime_str, sizeof(uptime_str), "%dh%dm", hours, mins);
} else {
snprintf(uptime_str, sizeof(uptime_str), "%dm", mins);
}
wifi_ap_record_t ap; wifi_ap_record_t ap;
int rssi = 0; int rssi = 0;
@@ -1762,6 +1770,20 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
const esp_app_desc_t *app_desc = esp_app_get_description(); const esp_app_desc_t *app_desc = esp_app_get_description();
if (!authed) {
/* Minimal: no build info, no target, no internals */
snprintf(reply, reply_size,
"OK STATUS hostname=%s uptime=%s uptime_s=%lld rssi=%d channel=%d"
" version=%s motion=%d presence=%d",
s_hostname, uptime_str, (long long)up, rssi, channel,
app_desc->version, s_motion_detected ? 1 : 0,
s_presence_detected ? 1 : 0);
return strlen(reply);
}
/* Full status (authenticated) */
uint32_t heap = esp_get_free_heap_size();
float chip_temp = 0.0f; float chip_temp = 0.0f;
#if SOC_TEMP_SENSOR_SUPPORTED #if SOC_TEMP_SENSOR_SUPPORTED
if (s_temp_handle) { if (s_temp_handle) {
@@ -1771,15 +1793,6 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
int actual_rate = (up > 0) ? (int)((uint64_t)s_csi_count / (uint64_t)up) : 0; int actual_rate = (up > 0) ? (int)((uint64_t)s_csi_count / (uint64_t)up) : 0;
char uptime_str[32];
if (days > 0) {
snprintf(uptime_str, sizeof(uptime_str), "%dd%dh%dm", days, hours, mins);
} else if (hours > 0) {
snprintf(uptime_str, sizeof(uptime_str), "%dh%dm", hours, mins);
} else {
snprintf(uptime_str, sizeof(uptime_str), "%dm", mins);
}
const char *csi_mode_str = (s_csi_mode == CSI_MODE_COMPACT) ? "compact" : const char *csi_mode_str = (s_csi_mode == CSI_MODE_COMPACT) ? "compact" :
(s_csi_mode == CSI_MODE_HYBRID) ? "hybrid" : "raw"; (s_csi_mode == CSI_MODE_HYBRID) ? "hybrid" : "raw";
@@ -2596,8 +2609,8 @@ static void cmd_task(void *arg)
rx_buf[len] = '\0'; rx_buf[len] = '\0';
/* Log command (redact HMAC token) */ /* Log command (redact HMAC token) */
if (strncmp(rx_buf, "HMAC:", 5) == 0 && strlen(rx_buf) > 22) { if (strncmp(rx_buf, "HMAC:", 5) == 0 && strlen(rx_buf) > 38) {
ESP_LOGI(TAG, "CMD rx: HMAC:****:%s", rx_buf + 22); ESP_LOGI(TAG, "CMD rx: HMAC:****:%s", rx_buf + 38);
} else { } else {
ESP_LOGI(TAG, "CMD rx: \"%s\"", rx_buf); ESP_LOGI(TAG, "CMD rx: \"%s\"", rx_buf);
} }
@@ -2622,7 +2635,7 @@ static void cmd_task(void *arg)
} else if (!authed && cmd_requires_auth(cmd)) { } else if (!authed && cmd_requires_auth(cmd)) {
reply_len = snprintf(reply_buf, sizeof(reply_buf), "ERR AUTH required"); reply_len = snprintf(reply_buf, sizeof(reply_buf), "ERR AUTH required");
} else { } else {
reply_len = cmd_handle(cmd, reply_buf, sizeof(reply_buf)); reply_len = cmd_handle(cmd, reply_buf, sizeof(reply_buf), authed);
} }
sendto(sock, reply_buf, reply_len, 0, sendto(sock, reply_buf, reply_len, 0,
(struct sockaddr *)&src_addr, src_len); (struct sockaddr *)&src_addr, src_len);
@@ -2726,12 +2739,11 @@ void app_main()
} }
#endif #endif
/* mDNS: announce as <hostname>.local with _esp-csi._udp service */ /* mDNS: announce as <hostname>.local — generic service type to reduce fingerprinting */
ESP_ERROR_CHECK(mdns_init()); ESP_ERROR_CHECK(mdns_init());
mdns_hostname_set(s_hostname); mdns_hostname_set(s_hostname);
mdns_instance_name_set("ESP32 CSI Sensor"); mdns_instance_name_set(s_hostname);
mdns_service_add(NULL, "_esp-csi", "_udp", s_target_port, NULL, 0); ESP_LOGI(TAG, "mDNS hostname: %s.local", s_hostname);
ESP_LOGI(TAG, "mDNS hostname: %s.local (_esp-csi._udp:%d)", s_hostname, s_target_port);
/* Watchdog: 30s timeout, auto-reboot on hang */ /* Watchdog: 30s timeout, auto-reboot on hang */
esp_task_wdt_config_t wdt_cfg = { esp_task_wdt_config_t wdt_cfg = {

View File

@@ -87,3 +87,9 @@ CONFIG_FREERTOS_USE_TICKLESS_IDLE=y
# WiFi Authentication (reject open/WEP APs) # WiFi Authentication (reject open/WEP APs)
# #
CONFIG_EXAMPLE_WIFI_AUTH_WPA2_WPA3_PSK=y CONFIG_EXAMPLE_WIFI_AUTH_WPA2_WPA3_PSK=y
#
# Protected Management Frames (802.11w) — prevent deauth attacks
#
CONFIG_ESP_WIFI_PMF_ENABLED=y
CONFIG_ESP_WIFI_PMF_REQUIRED=y