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