Skip to content

Commit ae5ccf5

Browse files
committed
Add LockdownAuth and LockdownStatus messages for hardened firmware builds
Companion proto changes for meshtastic/firmware#10349 (MESHTASTIC_LOCKDOWN hardened build option). Replaces a previous "no schema change" hack that repurposed SecurityConfig.private_key as the passphrase byte transport. AdminMessage.lockdown_auth (= 104): LockdownAuth carries a passphrase plus optional boots/until-epoch overrides, and a lock_now sentinel. Used for first-time provisioning, unlock on subsequent reboots, re-verification on already-unlocked devices, and Lock Now. Firmware decides between provision and unlock based on its own state. FromRadio.lockdown_status (= 18): LockdownStatus reports lockdown state to the client (NEEDS_PROVISION / LOCKED / UNLOCKED / UNLOCK_FAILED) plus structured fields for lock reason, token TTL, and unlock-failure backoff. Sent post-config and in response to each LockdownAuth command. Replaces the earlier scheme of encoding state as magic-string prefixes inside ClientNotification. Both messages are documented inline. No existing fields are altered.
1 parent 1d6f1a7 commit ae5ccf5

4 files changed

Lines changed: 151 additions & 0 deletions

File tree

meshtastic/admin.options

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@
1919
*HamParameters.call_sign max_size:8
2020
*HamParameters.short_name max_size:5
2121
*NodeRemoteHardwarePinsResponse.node_remote_hardware_pins max_count:16
22+
23+
*LockdownAuth.passphrase max_size:32

meshtastic/admin.proto

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,9 +521,68 @@ message AdminMessage {
521521
* Parameters and sensor configuration
522522
*/
523523
SensorConfig sensor_config = 103;
524+
525+
/*
526+
* Lockdown passphrase delivery / unlock / lock-now command for hardened
527+
* firmware builds (see MESHTASTIC_LOCKDOWN). Used to provision the
528+
* passphrase on first boot, unlock encrypted storage on subsequent
529+
* reboots, re-verify on already-unlocked devices to authorize a new
530+
* client connection, or immediately re-lock the device.
531+
*
532+
* Replaces the earlier scheme that repurposed SecurityConfig.private_key
533+
* to carry passphrase bytes; that hack is retired.
534+
*/
535+
LockdownAuth lockdown_auth = 104;
524536
}
525537
}
526538

539+
/*
540+
* Lockdown passphrase delivery payload.
541+
*
542+
* One message handles three operations distinguished by content:
543+
* - Provision (first-time): passphrase set, lock_now=false. Firmware
544+
* generates DEK, wraps with passphrase-derived KEK, persists.
545+
* - Unlock: passphrase set, lock_now=false. Firmware verifies
546+
* passphrase against stored DEK, unlocks storage, authorizes the
547+
* connection that delivered this packet.
548+
* - Lock now: lock_now=true, passphrase ignored. Firmware revokes
549+
* all client auth and reboots into the locked state.
550+
*
551+
* Firmware decides between provision and unlock based on its own state
552+
* (whether a DEK file already exists). Clients do not need to track
553+
* which case applies.
554+
*/
555+
message LockdownAuth {
556+
/*
557+
* Passphrase bytes (1-32). Empty when lock_now is true.
558+
* Capped to 32 to match the proto cap on related security fields.
559+
*/
560+
bytes passphrase = 1;
561+
562+
/*
563+
* Optional override of the boot-count token TTL granted on success.
564+
* 0 = use firmware default (TOKEN_DEFAULT_BOOTS).
565+
* On reboot the firmware decrements this; when it reaches 0 the
566+
* device boots fully locked and requires a fresh passphrase.
567+
*/
568+
uint32 boots_remaining = 2;
569+
570+
/*
571+
* Optional wall-clock expiry for the unlock token, as absolute
572+
* Unix-epoch seconds. 0 = no time limit (only the boot-count TTL
573+
* applies). On boot, if the device RTC is set and now > this value,
574+
* the token is treated as expired.
575+
*/
576+
uint32 valid_until_epoch = 3;
577+
578+
/*
579+
* If true, ignore passphrase fields, immediately revoke all
580+
* connection-level admin authorization, and reboot the device into
581+
* the locked state. Always honoured regardless of current lock state.
582+
*/
583+
bool lock_now = 4;
584+
}
585+
527586
/*
528587
* Firmware update mode for OTA updates
529588
*/

meshtastic/mesh.options

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,7 @@
9898
*ChunkedPayload.chunk_count int_size:16
9999
*ChunkedPayload.chunk_index int_size:16
100100
*ChunkedPayload.payload_chunk max_size:228
101+
102+
# Longest documented value is "token_wrong_size" (16 chars); 32 leaves room
103+
# for future short reasons without forcing nanopb to use callbacks.
104+
*LockdownStatus.lock_reason max_size:32

meshtastic/mesh.proto

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2263,9 +2263,95 @@ message FromRadio {
22632263
* Persistent data for device-ui
22642264
*/
22652265
DeviceUIConfig deviceuiConfig = 17;
2266+
2267+
/*
2268+
* Lockdown state notification for hardened firmware builds.
2269+
* Sent post-config (so unauthorized clients learn they must
2270+
* provision/unlock) and after each LockdownAuth admin command
2271+
* to report success or failure. Replaces the earlier scheme of
2272+
* encoding state as magic-string prefixes inside ClientNotification.
2273+
*/
2274+
LockdownStatus lockdown_status = 18;
22662275
}
22672276
}
22682277

2278+
/*
2279+
* Lockdown state report from firmware to client (for hardened builds
2280+
* with MESHTASTIC_LOCKDOWN). Sent immediately after config_complete_id
2281+
* to inform a freshly-connected unauthorized client what it must do,
2282+
* and again in response to each LockdownAuth admin command.
2283+
*/
2284+
message LockdownStatus {
2285+
enum State {
2286+
/* Default; should not be sent. */
2287+
STATE_UNSPECIFIED = 0;
2288+
2289+
/*
2290+
* No passphrase has ever been provisioned on this device.
2291+
* Client should prompt the operator to set one.
2292+
*/
2293+
NEEDS_PROVISION = 1;
2294+
2295+
/*
2296+
* Storage is locked or this client has not authenticated yet.
2297+
* lock_reason carries a machine-readable detail string.
2298+
* Client should present (or auto-replay) a passphrase via
2299+
* AdminMessage.lockdown_auth.
2300+
*/
2301+
LOCKED = 2;
2302+
2303+
/*
2304+
* Passphrase accepted; client is now authorized for this connection.
2305+
* boots_remaining and valid_until_epoch describe the active session
2306+
* token's TTL.
2307+
*/
2308+
UNLOCKED = 3;
2309+
2310+
/*
2311+
* Passphrase rejected. backoff_seconds is non-zero when rate-limited.
2312+
*/
2313+
UNLOCK_FAILED = 4;
2314+
}
2315+
2316+
/* Current lockdown state being reported. */
2317+
State state = 1;
2318+
2319+
/*
2320+
* For LOCKED: machine-readable reason. Known values:
2321+
* "needs_auth" — storage already unlocked, client must auth
2322+
* "token_missing" — no boot token on flash
2323+
* "token_expired" — boot token wall-clock TTL elapsed
2324+
* "token_boots_zero" — boot token boot-count TTL exhausted
2325+
* "token_hmac_fail" — token tampered or wrong device
2326+
* "token_dek_fail" — token DEK decrypt failed
2327+
* "token_wrong_size" — token file corrupted
2328+
* "token_bad_magic" — token file corrupted
2329+
* "not_provisioned" — should generally use NEEDS_PROVISION state instead
2330+
* Other values may be added; clients should treat unknown values as
2331+
* "locked, ask for passphrase".
2332+
*/
2333+
string lock_reason = 2;
2334+
2335+
/*
2336+
* For UNLOCKED: remaining boots on the issued session token.
2337+
* Decrements by 1 on each subsequent boot.
2338+
*/
2339+
uint32 boots_remaining = 3;
2340+
2341+
/*
2342+
* For UNLOCKED: wall-clock expiry of the issued session token,
2343+
* absolute Unix-epoch seconds. 0 = no time limit.
2344+
*/
2345+
uint32 valid_until_epoch = 4;
2346+
2347+
/*
2348+
* For UNLOCK_FAILED: seconds the client must wait before another
2349+
* passphrase attempt will be accepted. 0 = wrong passphrase, no
2350+
* backoff (immediate retry allowed but advisable to prompt user).
2351+
*/
2352+
uint32 backoff_seconds = 5;
2353+
}
2354+
22692355
/*
22702356
* A notification message from the device to the client
22712357
* To be used for important messages that should to be displayed to the user

0 commit comments

Comments
 (0)