feat: secure remote command execution over MQTT (slot-based) - #37
Draft
agessaman wants to merge 6 commits into
Draft
feat: secure remote command execution over MQTT (slot-based)#37agessaman wants to merge 6 commits into
agessaman wants to merge 6 commits into
Conversation
Adds JWTHelper::verifyToken (signature check + claim extraction, hex or base64url signatures) and JWTHelper::base64UrlDecode, complementing the existing token-creation path. Firmware-only; used by remote command auth.
Pure, host-testable engine for JWT-authenticated remote serial commands: replay protection, per-key rate limiting, command blacklist, target filtering, and authorization ordering. All JSON/base64/crypto/clock I/O is behind injected seams so the full pipeline is unit-tested under env:native (23 googletest cases) without MQTT or Ed25519.
Adds mqtt_remote_enabled (global master), mqtt_use_acl, per-slot mqtt_slot_remote_enabled[], and mqtt_admin_public_key to the /mqtt_prefs tail. Introduces the kV1PreRemotePayloadSize (2864) decode checkpoint so a pre-remote payload still loads as Current with the remote fields defaulting (master off, ACL on, per-slot on). Payload version stays 1; a forward file (2940) is held, not discarded, by older firmware. New host codec tests cover the pre-remote migration and the full round-trip.
Global: set/get mqtt.remote (master kill switch), mqtt.useacl, mqtt.admin (admin key readable over serial only). Per-slot: set/get mqttN.remote, following the existing mqttN.* slot-command convention. Setters only persist; the bridge reconciles command subscriptions live, so no WSS restart is needed.
Binds the RemoteControl policy engine to the slot clients: each slot registers an onMessage callback that claims a single lock-free pending slot; the bridge task verifies + executes it and publishes the signed response to the originating slot. Subscriptions are reconciled live against the global master (mqtt.remote) and per-slot (mqttN.remote) flags, so the kill switch unsubscribes every slot and drops any in-flight command without a WSS restart. The bridge implements the RemoteControl crypto/authorizer/executor/clock seams privately (JWTHelper + LocalIdentity, MQTTPrefs + ACL callback, CLI callback, millis/time). Remote commands run with a non-zero sentinel sender_timestamp so serial-only CLI gates (prv.key, freq, erase) still refuse them.
Add a shared MQTTRemoteCallbacks.h (ACL admin authorizer + CLI executor adapters) and register them on the bridge in the repeater and room-server examples right after construction, at every bridge-creation site.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Re-implements the JWT-authenticated remote serial command execution over MQTT
feature (originally a single commit on the long-stale
mqtt-bridge-implementation-remote,~1223 commits / 6 months behind) onto the current slot-based observer MQTT
architecture. This is a re-implementation, not a cherry-pick: the old feature was built
around a fixed two-analyzer (US/EU) client model and
NodePrefs-resident MQTT config,both of which no longer exist.
An admin holding an authorized Ed25519 key can publish a signed command to
meshcore/{IATA}/{DEVICE}/serial/commands; the device verifies it, runs it through theexisting CLI, and publishes a signed reply to
.../serial/responses.Design decisions (from the reviewed plan)
limit, command blacklist, target filtering, authorization ordering) lives in a new pure
RemoteControlhelper with the JSON/base64/Ed25519/clock behind injected seams, so it isfully host-tested without MQTT or crypto. The bridge implements the seams (JWTHelper +
LocalIdentity, MQTTPrefs + ACL callback, CLI callback, millis/time).
set mqtt.remote on|offis the master(default off);
set mqttN.remote on|offgates each slot (default on). Effective =master AND slot. Subscriptions are reconciled live by the bridge task, so the kill switch
unsubscribes every slot and drops any in-flight command without a WSS restart.
two-analyzer model; slots already bound memory via PSRAM active-slot caps + circuit
breakers), and the
-native-observer-{hash}version-string change (collides with theobserver release-channel versioning).
are preserved.
Security posture
sender_timestamp, so the serial-onlyCLI gates (
get prv.key,set freq,erase, …) treat them as not local console andrefuse — a remote admin gets mesh-admin-equivalent access, never console-only access.
(The old feature passed
0, i.e. full serial access.)mqtt.useacl on, default) or an explicitmqtt.adminkey; command blacklist (get wifi.pwd,set mqtt.admin);rebootrefused;nonce replay + per-key rate limiting run before the expensive signature verify.
get mqtt.adminreveals the key over serial only.Preferences / fleet migration
New fields are appended to the
MQTTPrefstail (mqtt_remote_enabled,mqtt_use_acl,mqtt_slot_remote_enabled[],mqtt_admin_public_key); payload version stays 1. A newkV1PreRemotePayloadSize(2864) decode checkpoint means a/mqtt_prefswritten by currentfield firmware still loads as
Current, with the remote fields defaulting (master off, ACLon, per-slot on). A forward (2940-byte) file is held, not discarded by older firmware.
static_asserts lock the offsets.CLI
set/get mqtt.remote,mqtt.useacl,mqtt.admin, and per-slotset/get mqttN.remote.Testing
pio test -e native: 238/238 pass, including a new 23-casetest_remote_controlsuite (replay, rate-limit, blacklist, target filtering, ACL-vs-key authz, malformed JWT,
timeout, response construction) and 2 new codec migration tests (pre-remote load + full
round-trip).
Heltec_v3_repeater_observer_mqttandHeltec_v3_room_server_observer_mqtt.Not included (possible follow-ups)
that brokers are preset-based.
Commits
jwtverify →RemoteControlengine + tests →MQTTPrefsfields + codec → CLI → bridgeglue → example wiring.