Appearance
Mesh admin: operator channel and encrypted commands
Interactive maintenance (USB/BLE to a physically adjacent device) is Remote shell (0-hop). That path does not use mesh ttl.
This section defines operator actions over the mesh — e.g. delete or pin bulletin entries, change node parameters, MAC-layer blocklist, trigger config writes — in a way that is cryptographically separated from normal participants who only hold the network master key and everyday channel keys.
Threat model
- Holders of the community PSK (NMK-derived channel keys) MUST not be able to forge admin actions by default.
- Admin capabilities require possession of a separate admin channel encryption key (32-byte AES-256 secret).
- Default community frames (
channelHash == 0x0000) MUST NOT satisfy admin decryption: admin traffic MUST use the adminchannelHash+adminChannelPskonly, so category0x9cannot be acted on from the community key alone.
Admin channel (normative)
| Item | Specification |
|---|---|
channelHash | Admin frames use the channelHash computed from the fixed UTF-8 name lowmesh/admin + adminChannelPsk — see Channels & security. |
| Key material | adminChannelPsk — 32 bytes, AES-256, stored explicitly in NVM (see Embedded storage). |
| Encryption | AES-256-CCM on the application tail (same envelope as other mesh traffic): MIC_MODE in verFlags selects 4-byte (CCM-4, default) or 8-byte (CCM-8, optional) MIC. See Channels & security. |
| Payload types | Admin payloadType values use category 0x9 — see Payloads. Nodes MUST reject (drop) category 0x9 unless decrypted with adminChannelPsk (matching admin channelHash). Community 0x0000 decrypt MUST NOT unlock admin bodies. |
Routing: Admin frames use normal mesh headers (srcId, optional dstId, ttlRaw) and MAY be unicast to a specific infrastructure node or flood with tight TTL per policy.
ADMIN_CMD (0x91)
Opaque to non-admin decryptors. After MIC verification and decryption, the inner plaintext is:
| Field | Size | Description |
|---|---|---|
adminCounter | 8 | LE uint64 monotonic counter per admin source / signing key |
adminFlags | 1 | Bit 0 = signaturePresent; bits 1–7 reserved, MUST be 0 on TX |
targetNodeId32 | 4 | LE nodeId32 target, or 0xFFFFFFFF for any admin host in scope |
commandLen | 2 | LE length of commandChain |
commandChain | commandLen | Admin operation chain (see below) |
signature | 0 or 64 | Ed25519 signature when adminFlags & 0x01 |
There is one current ADMIN_CMD format (LowMesh is not publicly released — do not use ADMIN_CMD_V2 or adminBodyVersion naming).
Processing order (normative):
- MIC verification (CCM-4 or CCM-8 per
MIC_MODE) — fail → drop, apply nothing. - Product policy — e.g. CCM-8 required for destructive ops → fail →
DROP_ADMIN_POLICY_REQUIRES_CCM8. - Replay check on
adminCounter— fail →DROP_ADMIN_REPLAY. - Signature verify (if present or required) — fail →
DROP_ADMIN_SIGNATURE_BAD/DROP_ADMIN_SIGNATURE_REQUIRED. - Parse and apply
commandChain— any failure → apply nothing.
commandChain (normative)
commandChain is one or more concatenated operation records:
[OpCode (1)] [KeyId (2 LE)] [ValueLen (2 LE)] [Value (ValueLen bytes)]Rules:
- Operations are processed in order.
- If any record overruns
commandLen, abort the entire command and apply nothing. - Unknown
OpCoderejects the entireADMIN_CMDand applies nothing. - For destructive commands, product policy MAY require CCM-8, a valid inner admin signature, or both.
Destructive / high-risk examples: key changes, admin key changes, firmware update state changes, deleting bulletin/store-forward data, changing radio/regional policy, changing relay permissions, factory reset, changing device role/profile.
Admin signature
When adminFlags & 0x01 (signaturePresent), a 64-byte Ed25519 signature follows commandChain.
Signed message (domain separator + fields):
"LowMesh ADMIN_CMD" ||
srcId ||
targetNodeId32 ||
adminCounter ||
commandLen ||
commandChainsrcIdis the outer mesh header sourcenodeId32(4 bytes, LE).- Do not include the routing block (repeaters may mutate breadcrumbs).
- Do not include
hopControl/ttlRaw(relays may decrement hops).
Receiver MUST verify the signature before applying commandChain. Invalid or missing signature when required → reject entire command, apply nothing.
Admin replay protection
Each receiver stores the highest accepted adminCounter per admin source identity or admin signing key. If adminCounter <= lastAcceptedCounter, reject as replay (DROP_ADMIN_REPLAY).
Implementations MAY keep a bounded replay window when storage is limited, but destructive commands SHOULD require persistent replay protection.
CCM-8 policy (admin)
Normal mesh traffic SHOULD use CCM-4. Admin traffic SHOULD use CCM-8 when both ends support SEC_CAP_CCM8. Products MAY require CCM-8 for destructive admin operations. If CCM-8 is required and the device does not support it, the command MUST NOT be sent or MUST be rejected.
See Channels & security — CCM-8 policy.
Universal execution chain (normative)
The commandChain is an implicit, sequential array of one or more operations, concatenated without outer TLV framing. Each operation is exactly:
[OpCode (1)] [KeyId (2 LE)] [ValueLen (2 LE)] [Value (ValueLen bytes)]Records are processed in order. Let offset index the start of the current record in commandChain, valueLen the ValueLen field (2 LE), and commandLen the total commandChain length. If (offset + 5 + valueLen) > commandLen, the record overruns the buffer: the parser MUST abort the entire ADMIN_CMD (MUST NOT apply this record or any later record).
Valid OpCode values (only):
OpCode | Name |
|---|---|
0x10 | KV_SET (write / action intercept) |
0x12 | KV_GET (read; response via ADMIN_RSP) |
Any other OpCode is invalid; the receiver MUST reject the ADMIN_CMD and MUST NOT apply any operation from this message.
KV_GET request: ValueLen on the wire MUST be 0 (there is no request Value payload). Semantics are selected solely by KeyId.
Parsing / execution (normative, firmware): Let off = 0. While off < commandLen:
- If
off + 5 > commandLen, abort entire command — apply nothing. - Read
OpCode = commandChain[off],KeyId(2 LE atoff+1),ValueLen(2 LE atoff+3). - If
(off + 5 + ValueLen) > commandLen, abort entire command (overrun). - Let
Value = commandChain + off + 5(lengthValueLen). - Dispatch
KV_SET/KV_GETforKeyId; on any failure, abort entire command (MUST NOT apply later operations). off += 5 + ValueLen.
An empty commandChain (commandLen = 0) is valid (no-op) after replay/signature/policy checks pass.
Atomic two-phase commandChain commit (normative)
commandChain execution is atomic: receivers MUST use a two-phase apply model:
- Validate phase — Parse the full
commandChain, verify every record fits withincommandLen, resolveKeyId/Valuebounds, and run policy pre-checks (CCM-8, signature, unicastdstId, dangerous-op rules below). No NVM or RAM mutation in this phase. - Commit phase — Only if validation succeeds, apply all operations in order. If any operation fails during commit, the receiver MUST roll back to the pre-command state where feasible and MUST NOT leave a partially applied chain.
If validation fails, nothing from that ADMIN_CMD is applied — same as a mid-chain parse failure.
Dangerous operations — delivery and policy (normative)
Destructive or high-risk KV_SET operations (key changes, admin key changes, firmware update state, bulletin/store-forward deletes, radio/regional policy, relay permissions, factory reset, role/profile changes) MUST satisfy all of the following unless a documented product exemption exists:
| Requirement | Rule |
|---|---|
Unicast dstId | Outer mesh header HAS_DST_ID MUST be set; dstId MUST equal the intended target nodeId32 (not flood, not 0xFFFFFFFF wildcard) |
| Replay protection | adminCounter MUST pass the monotonic replay check |
| Signature policy | Product policy MAY require adminFlags & signaturePresent and a valid Ed25519 signature over the signed message domain |
| CCM-8 policy | Product policy MAY require MIC_MODE = 01 when SEC_CAP_CCM8 is advertised on the target |
Flood-delivered admin frames MAY be accepted for non-destructive reads (KV_GET) per product policy, but MUST NOT satisfy dangerous-op delivery rules above.
Vendor or product-specific extensions SHOULD use dedicated KeyId blocks (e.g. high 0xExxx ranges) and MUST document wire Value layouts.
Virtual register table (KeyId LE)
Normative registry, listed by command name. KeyId values are grouped by high byte (0x01xx standard config, 0x02xx bulletin host intercepts, 0x03xx RAM MAC blocklist). KV_SET carries Value as below; KV_GET responses use ADMIN_RSP echo blocks (KeyId (2 LE) || valueLen (2 LE) || value) as in Getter responses under ADMIN_RSP below.
| Command name | KeyId (LE) | KV_SET Value | Notes |
|---|---|---|---|
MAC_RELAY_COMMUNITY | 0x0101 | 1 byte: 0x00 = Restricted (Fortress: drop unknown channelHash without local key); 0x01 = Community (forward unknown ciphertext per Channels & security). | Updates NVM / RAM policy; maps to nvm::DeviceRole::relayCommunityTraffic. KV_GET: current 1 byte. |
MAC_PRIMARY_ROLE | 0x0102 | 1 byte: 0x00 = Gateway, 0x01 = Repeater, 0x02 = Client (legacy alias: Edge) | Updates NVM role; nvm::DeviceRole::primaryRole. KV_GET: 1 byte. |
BULLETIN_CAP_TOTAL | 0x0111 | 2 bytes LE (nMaxTotal) | Updates NVM bulletin host capacity; BulletinHostPrefs (see Bulletin board). KV_GET: 2 bytes LE. |
BULLETIN_DELETE | 0x0201 | 6 bytes: channelHash (2 LE) ` | |
BULLETIN_PIN | 0x0202 | 7 bytes: channelHash (2 LE) ` | |
BLOCKLIST_ADD | 0x0301 | srcId (4 LE) | RAM blocklist add (see Embedded storage). |
BLOCKLIST_REMOVE | 0x0302 | srcId (4 LE) | RAM blocklist remove. |
KV_GET (0x12) on KeyId = 0x0301 (BLOCKLIST_ADD): returns the current blocklist array in the echoed value: count (1 byte) then count × 4 bytes of srcId (LE), insertion order, implementation cap (e.g. 16 entries). The response echo’s KeyId field is still 0x0301.
Persistence (normative): On successful KV_SET for 0x0101, 0x0102, or 0x0111, firmware MUST persist the corresponding NVM fields (lowmesh::nvm::DeviceStoreV1, BulletinHostPrefs, or equivalent) immediately before reporting success to the operator path, so reboot and power loss do not revert policy. RAM-only updates are not sufficient for those keys.
C++: lowmesh::runtime::ADMIN_OP_KV_SET, ADMIN_OP_KV_GET, ADMIN_KV_* — see firmware/include/lowmesh/runtime/admin_cmd.hpp.
Example: instant relay lockdown (Restricted)
Single KV_SET as the entire commandChain (commandLen = 6, no signature):
| Byte (hex) | Meaning |
|---|---|
10 | OpCode = KV_SET |
01 01 | KeyId = 0x0101 (MAC_RELAY_COMMUNITY, LE) |
01 00 | ValueLen = 1, LE |
00 | Value = 0x00 → Restricted |
Full commandChain (6 bytes): 10 01 01 01 00 00.
ADMIN_RSP (0x92)
Unicast response from target to operator originator (when applicable):
| Field | Size | Description |
|---|---|---|
forMessageId | 2 | Correlates to admin request mesh messageId (16-bit) |
status | 1 | Status code — see table below |
bodyLen | 2 | LE |
body | variable | Structured payload per status / command |
status values (normative):
| Value | Name |
|---|---|
0x00 | ADMIN_STATUS_OK |
0x01 | ADMIN_STATUS_BAD_FORMAT |
0x02 | ADMIN_STATUS_REPLAY |
0x03 | ADMIN_STATUS_SIGNATURE_REQUIRED |
0x04 | ADMIN_STATUS_SIGNATURE_BAD |
0x05 | ADMIN_STATUS_POLICY_REQUIRES_CCM8 |
0x06 | ADMIN_STATUS_UNSUPPORTED_MIC_MODE |
ADMIN_RSP SHOULD use the same admin channelHash / domain and SHOULD use the same or stronger MIC_MODE as the request when possible.
Getter responses (KV_GET → ADMIN_RSP)
For each KV_GET operation successfully executed in order, the responder SHOULD append one block to ADMIN_RSP body with the same echo shape:
KeyId (2 LE) || valueLen (2 LE) || value
- Example:
KV_GETonBLOCKLIST_ADD(0x0301) yieldsvalue=count(1)|| count × srcId(4 LE each) inside that echo block (see virtual register table). - Multiple **
KV_GET**s in oneADMIN_CMDchain → concatenate echo blocks in execution order (firmware:State::adminChainRspfilled bydispatchSecureAdminCmdPlaintexton success).
Errors: non-zero status; optional UTF-8 / structured error in body. On chain parse/execute failure, the requester MUST treat the whole ADMIN_CMD as failed; ADMIN_RSP payload from a partial chain is not normative.
Bulletin board interaction
Public BULLETIN_POST on normal channels remains for authors. Moderation (delete, pin, capacity) over RF uses ADMIN_CMD on the admin channelHash with the admin key — see Bulletin board.
Relationship to USB/BLE provisioning
Provisioning still uses USB & BLE hex API for initial key install. The admin mesh key SHOULD be distributed only to operators (QR, secure BLE, or out-of-band), not embedded in general community QR codes.
Embedded storage
See adminChannelPsk — stored in lowmesh::nvm::Crypto::adminChannelPsk in Embedded storage — and Crypto keys.