Skip to content

fix(companion): gate private-key export and factory-reset on WiFi builds#2527

Open
swaits wants to merge 1 commit into
meshcore-dev:mainfrom
swaits:fix/wifi-companion-gate-dangerous-cmds
Open

fix(companion): gate private-key export and factory-reset on WiFi builds#2527
swaits wants to merge 1 commit into
meshcore-dev:mainfrom
swaits:fix/wifi-companion-gate-dangerous-cmds

Conversation

@swaits
Copy link
Copy Markdown

@swaits swaits commented May 12, 2026

Summary

On companion_radio builds compiled with WIFI_SSID, the host frame-protocol surface is exposed over plain TCP port 5000 with no authentication. Anyone on the same WiFi network can issue any of the ~60 CMD_* codes, including CMD_EXPORT_PRIVATE_KEY (cmd 23), CMD_IMPORT_PRIVATE_KEY (cmd 24), and CMD_FACTORY_RESET (cmd 51). This change disables those three commands by default on WIFI_SSID builds and gates them behind a new opt-in build flag ALLOW_UNAUTHENTICATED_TCP_PRIVATE_KEY. Serial and BLE builds are unchanged (serial is local-only; BLE uses PIN-bonded characteristics).

Background

The TCP frame protocol lives in src/helpers/esp32/SerialWifiInterface.cpp. checkRecvFrame() accepts a TCP accept(), reads a 3-byte header + body, and dispatches the body to MyMesh::handleCmdFrame(). There is no handshake / PIN / nonce / challenge at the framework or application layer.

The most damaging unauth capabilities:

  • CMD_EXPORT_PRIVATE_KEY (examples/companion_radio/MyMesh.cpp) — writes the 64-byte Ed25519 private key to the TCP socket.
  • CMD_IMPORT_PRIVATE_KEY (examples/companion_radio/MyMesh.cpp) — overwrites the device identity with attacker-supplied keypair.
  • CMD_FACTORY_RESET (examples/companion_radio/MyMesh.cpp) — formats the FS and reboots.

The base platformio.ini enables ENABLE_PRIVATE_KEY_EXPORT=1 and ENABLE_PRIVATE_KEY_IMPORT=1 by default with a comment "NOTE: comment these out for more secure firmware", but in practice every shipped variant has them on, and the flags aren't aware that WiFi changes the trust model.

PoC against an unpatched build:

printf '<\x01\x00\x17' | nc victim.local 5000 | xxd
# returns: >  41 00  0e  <prv_key:64>

Change

examples/companion_radio/MyMesh.cpp only — three command handlers gain a compile-time guard:

#if ENABLE_PRIVATE_KEY_EXPORT && (!defined(WIFI_SSID) || defined(ALLOW_UNAUTHENTICATED_TCP_PRIVATE_KEY))

(and the same for the IMPORT path and FACTORY_RESET, which gets a new #if/#else writeDisabledFrame();#endif block).

When the guard fails, the handler responds with the existing RESP_CODE_DISABLED frame, matching the pre-existing "command disabled at build time" pattern.

Why this is the minimal fix

The proper long-term fix is an out-of-band PIN handshake on the TCP socket — re-using the BLE PIN mechanism — so that no command works until the client authenticates. That requires host-app changes and a new wire-protocol message; it's a larger coordinated release.

This patch is the minimum-viable defense: remove the three most-dangerous capabilities from the unauthenticated TCP surface, leave the rest. Even after this lands, an attacker on the LAN can still observe traffic and issue benign commands; the PIN handshake remains worth doing. We don't change platformio.ini defaults — the existing ENABLE_PRIVATE_KEY_* flags continue to mean what they meant, just composed with the WiFi gate.

Risk / compatibility

  • Wire format / frame protocol: unchanged. Clients that ask for these commands on a WiFi build now see RESP_CODE_DISABLED, which is already in the protocol and is what they see when the feature is built-out.
  • For trusted-network use cases where the operator wants TCP key import/export (e.g. provisioning fleets), define -D ALLOW_UNAUTHENTICATED_TCP_PRIVATE_KEY=1 at build time. The flag name is intentionally accusatory.
  • Serial and BLE builds: zero behaviour change.

References

  • CWE-306 — Missing Authentication for Critical Function.
  • CWE-269 — Improper Privilege Management.
  • OWASP IoT Top 10 (2018) num. 2 — "Insecure Network Services."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant