Appearance
Channels: hashes, names, PSKs, AES-256-CCM, and pairwise DMs
LowMesh does not carry a small numeric channelId on the wire. Every Mesh PDU includes a channelHash (2 bytes, LE uint16_t) that selects the crypto domain for the inner AES-CCM application tail.
Truncation (normative on this wire revision): compute the full 32-bit digest per the recipes below, then use the lower 16 bits as uint16_t (digest[0] | (digest[1] << 8) in LE order) for channelHash on-air. (Host tools MAY still store/compare 32-bit digests internally; the mesh header carries 16 bits.)
There are three normative ways a channelHash selects keys:
- Default community (
0x0000) — fixed AES-256 keydefaultCommunityPsk32in ROM on every device (SHA256("LowMesh/v2/defaultCommunityPsk32"), first 32 bytes). The default community domain is encrypted/authenticated against accidental corruption and casual non-LowMesh decoding, but it is not private against other LowMesh devices because thedefaultCommunityPsk32is globally shared. Use named channels, DMs, or admin channels for private traffic. There is no unauthenticated cleartext inner payload on0x0000in the current specification. - Named channel — derived from
(channelNameUtf8, psk32)(shared group key). - Pairwise DM — derived from HKDF over the X25519 ECDH output — see Network key & channel keys.
What receivers do (normative intent)
- Parse the dynamic base header (see Packet layout); preserve
ttlRawat wire offset0x04inside the CCM AAD copy with byte-wide zero-masking at that offset (see below). - Read
channelHash(16-bit). - Build the candidate list of local
psk32entries whose on-wirechannelHashequals the header value (community0x0000, matching DM sessions, matching named channels, admin channel if applicable). - If more than one candidate maps to the same 16-bit
channelHash, try candidates in a deterministic order (document product policy — e.g. DM before named before community) and accept only one successful MIC verification. - If more than one candidate verifies, the frame is ambiguous — MUST drop (log
DROP_MIC_AMBIGUOUSor equivalent). - If no candidate verifies: treat as unknown hash for relay policy (see Embedded storage
DeviceRole::relayCommunityTraffic) — end nodes typically drop; repeaters MAY applyshouldForwardAsThirdPartyRelay.
DM receiver caching
Devices SHOULD maintain a RAM table of DmPairwiseSession entries (see Embedded storage): at minimum peerDeviceId, channelHash, derived psk32, trustState, and the peer’s ed25519PublicKey for re-derivation and UI.
When NODE_ADVERT / NODE_INFO_* delivers a new pkEd_peer, firmware SHOULD precompute (or refresh) the pairwise psk32 and channelHashDM for that peer off the radio hot path where possible.
channelHash definitions (normative digest math)
A) Named channels (group / admin)
SHA-256 digest over UTF8("LMCH/v1") || uint16_le(N) || channelNameUtf8 || psk, then:
channelHash_onWire = uint16_le( digest[0] | (digest[1] << 8) ) // 2-byte LE in mesh headerPractically: channelHash_onWire = digest[0] | (digest[1] << 8) as an unsigned 16-bit value. Host tools MAY cache read_u32_le(digest[0..3]) for UI and collision analysis; the mesh PDU carries 16 bits only.
channelHash MUST NOT be 0 for named channels (the recipe is expected to yield non-zero lower 16 bits; treat accidental 0 as a deployment error).
B) Pairwise direct messages (DM)
Let psk32 be the HKDF-derived AES key for the DM session (see Network key & channel keys §5).
digestDM[0..31] = SHA256( psk32[0..31] )
channelHashDM_onWire = uint16_le( digestDM[0] | (digestDM[1] << 8) ) // 2-byte LE in mesh header(Provisioning tools MAY compare read_u32_le(digestDM[0..3]) internally; only the lower 16 bits are transmitted on-air.)
16-bit collision policy: Truncation to 16 bits can cause unrelated keys to share the same on-air channelHash. Receivers MUST use the deterministic multi-candidate MIC procedure above. Provisioning tools SHOULD warn when any configured named channel or DM maps to the same 16-bit channelHash as another entry. Devices SHOULD expose a local diagnostic warning when such a collision exists among configured named channels, pairwise DM sessions, or admin channels. MUST NOT expand the mesh header beyond 16-bit channelHash on this wire revision.
C) Default community reserved hash
channelHash = 0x0000 is reserved for the default community key above — not cleartext. A separate true cleartext “Ham operator” profile is deferred to a future wire revision.
AES-256-CCM (all channels)
For every resolved psk32 (0x0000, named, or DM), the MIC length is selected by MIC_MODE in verFlags bits 3–2 (see Packet layout):
MIC_MODE | Mode | micLen |
|---|---|---|
00 | AES-256-CCM-4 (required, default) | 4 |
01 | AES-256-CCM-8 (optional) | 8 |
10 / 11 | Reserved | receivers MUST drop (DROP_UNSUPPORTED_MIC_MODE) |
verFlags bits 1–0 remain reserved and MUST be 0 on TX. Stacks that require all reserved bits to be zero will safely drop MIC_MODE != 00 frames rather than misparse them. This does not mean CCM-4-only devices can decrypt CCM-8 traffic — they cannot.
Receivers MUST drop frames with unsupported MIC_MODE values.
Ciphertext + MIC length (parsing)
Let prefixBytes be the size of the base header plus optional region, fragment, and routing blocks. Let rxPhyLen be the Mesh PDU length (excluding PHY CRC if captured separately).
prefixBytes = baseSize + regionSize + fragSize + routeSize
appTailLen = rxPhyLen - prefixBytes
micLen = decodeMicLen(verFlags.MIC_MODE) // 4 or 8
if appTailLen < micLen:
drop DROP_TOO_SHORT
cipherLen = appTailLen - micLen
innerBodyLen = cipherLen
appTail = ciphertext || MICcipherLenequals the plaintext length of the inner message (innerBodyLenafter decrypt).MIC_MODEchanges only the MIC/tag length. It does not changechannelHash, nonce construction, AAD masking, routing-block behavior,payloadType, or the dynamic header layout.
Backward compatibility: CCM-8 is backward compatible by safe rejection. Devices that only support required CCM-4 will drop MIC_MODE = 01 frames; they MUST NOT claim to read CCM-8.
CCM-8 policy
- Normal mesh traffic SHOULD use
MIC_MODE = 00(CCM-4) by default for airtime efficiency. - Admin traffic SHOULD use
MIC_MODE = 01(CCM-8) when both sender and receiver advertiseSEC_CAP_CCM8(see Node identity). - Products MAY require CCM-8 for destructive or high-risk admin operations. If CCM-8 is required by policy and a device does not support it, the admin command MUST NOT be sent or MUST be rejected (
DROP_ADMIN_POLICY_REQUIRES_CCM8/ADMIN_STATUS_POLICY_REQUIRES_CCM8). - A sender SHOULD use CCM-8 only when the intended receiver is known to support
SEC_CAP_CCM8. For broadcast/flood traffic, use CCM-4 unless all intended receivers are known to support CCM-8 or product policy intentionally excludes older devices. - Admin tools MAY require CCM-8 even if that means older devices cannot be administered.
CCM-8 is an optional stronger MIC mode for admin, high-security, or product-specific deployments. Normal mesh traffic remains CCM-4 by default.
Additional Authenticated Data (AAD) — critical
AAD length is dynamic:
aadLen = baseSize + regionSize + fragSizeCopy those bytes from the received Mesh PDU into the AAD buffer in order:
- Base header bytes (
verFlags…payloadType, dynamic 16 or 20 bytes depending onHAS_DST_ID). - Spatial Region block (5 bytes:
spatialId+spatialMask) ifHAS_REGION. - Fragment block (2 bytes) if
HAS_FRAGMENT.
The routing block MUST NOT be included in AAD. This allows stateless repeaters to append path hashes and grow the routing block without invalidating the MIC over the encrypted inner message.
Strict source routes vs flood breadcrumbs: The mutable plaintext routing block is appropriate for flood breadcrumbs only. Strict source-route instructions that affect security, authorization, or delivery guarantees MUST be authenticated inside the encrypted application body or by a separate routeAuth mechanism. Strict source routes carried only in the plaintext routing block are advisory unless authenticated by a higher layer — MUST NOT be treated as trusted authorization by themselves. See Routing on wire.
TTL zero-masking: before CCM, set hopControl (wire name ttlRaw) to 0x00 in the AAD copy at the same relative offset as on the wire within the copied base header. For the documented base layout, this byte is always at Mesh PDU offset 0x04. Masking the whole byte (hop limit and HAS_ROUTE_BLOCK bit 7) lets relays set the route flag and decrement hops without mutating verFlags (which are authenticated in AAD).
Plaintext / ciphertext scope
- Encrypted/authenticated by CCM: the application tail bytes only (all
channelHashvalues including0x0000). - Not encrypted by the channel PSK: base header, optional 5-byte region block, optional fragment block, optional routing block (visible on the wire outside the ciphertext).
AES-256-CCM nonce construction (Base Profile)
AES-CCM nonce construction MUST be unique per psk32. messageId alone is not sufficient — it is only 16 bits. The on-wire sessionId (4 bytes LE in the base header) MUST participate in nonce and replay keying.
Base Profile default (12-byte CCM nonce)
Normative Base Profile layout (exactly 12 bytes):
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | srcId (LE nodeId32) |
| 4 | 4 | sessionId (LE uint32, from base header offset 0x09) |
| 8 | 2 | messageId (LE uint16) |
| 10 | 2 | fragmentWord — packed mesh fragment when HAS_FRAGMENT, else 0 |
Rules:
sessionIdis mandatory on the wire and MUST be included in the nonce.fragmentWordis0whenHAS_FRAGMENTis not set.fragmentWordis in the nonce. The 10-bitfragmentIndexMUST NOT be wrapped or reused to represent more than 1024 chunks — see Fragmentation.- The same nonce MUST NOT be reused with the same
psk32, regardless ofMIC_MODE/ MIC length. MIC_MODEMUST NOT be added to the nonce unless a future unreleased spec change explicitly does so.- Originators MUST persist
sessionIdin NVM and MUST change it after reboot (or when reuse would collide) before reusingmessageIdvalues under the samepsk32— see Embedded storage.
See Base interoperability profile and Golden test vectors.
Integrity expectations
- LoRa CRC16 provides PHY-level error detection for the whole over-the-air payload.
- CCM-4 / CCM-8 provides cryptographic integrity for the inner ciphertext under the AAD policy above (MIC length from
MIC_MODE).
Replay protection
AES-CCM authenticates payloads but does not by itself prevent replay. Receivers MUST use duplicate / replay suppression keyed by at least:
(channelHash, srcId, sessionId, messageId, fragmentWord)where fragmentWord is the packed fragment block when present, else 0 (same keying as the duplicate cache in Routing).
Admin / control payloads SHOULD include stronger replay protection inside the encrypted inner body — for example a monotonic command counter, timestamp, session counter, or challenge/response token. Receivers MUST reject stale or repeated admin commands when replay metadata indicates the command was already processed — see Mesh admin.
Admin / operator traffic
Named-channel channelHash with the fixed lowmesh/admin name + adminChannelPsk — see Mesh admin & operator channel.
Relationship to NVM / provisioning
Named channels: firmware stores (channelNameUtf8, psk) — see Network key & channel keys and Embedded storage.
DMs: firmware stores Ed25519 keys in NVM, sessionId, and HKDF-derived psk32 / channelHashDM in RAM after ECDH.
Hardware note (ESP32-S3, nRF52840)
SHA-256, X25519, Ed25519↔X25519 conversion, and AES-CCM SHOULD use optimized library implementations (mbedTLS, TweetNaCl, PSA). Avoid blocking radio tasks on curve math — use deferred workers and bounded session caches.