# MLS-Lite: Lightweight Crypto for Constrained Mesh Links > **Goal:** Define a symmetric encryption mode that works on LoRa SF12 (51-byte MTU) > while preserving as much MLS security as possible and enabling upgrade to full MLS > when faster transports are available. > > Created: 2026-03-30 | Status: Design Draft --- ## Problem Statement Full MLS is impractical on constrained links: | MLS Operation | Size (bytes) | SF12 Fragments | TX Time (1% duty) | |---------------|--------------|----------------|-------------------| | KeyPackage | 500-800 | 10-16 | 10-16 hours | | Welcome | 1000-2000 | 20-40 | 20-40 hours | | Commit | 200-500 | 4-10 | 4-10 hours | | AppMessage | 100-200 | 2-4 | 2-4 hours | **Result:** Group setup over LoRa takes days. Messages take hours. Unusable. --- ## Design Goals 1. **Short message overhead:** <50 bytes for a "hello" message (fits SF12 MTU unfragmented) 2. **Group encryption:** Shared symmetric key, not just link encryption 3. **Sender authentication:** Ed25519 signature (64 bytes, fragmentable) 4. **Upgrade path:** Seamless transition to full MLS when faster link available 5. **No KeyPackage exchange:** Use pre-shared secrets or out-of-band key exchange --- ## MLS-Lite Protocol ### Mode Selection ``` ┌─────────────────────────────────────────────────────────────┐ │ TransportManager │ ├─────────────────────────────────────────────────────────────┤ │ On send(destination, payload): │ │ │ │ 1. Check best route to destination │ │ 2. Get transport bitrate: │ │ - QUIC/TCP (>10 kbps) → full MLS │ │ - LoRa SF7-9 (1-10 kbps) → MLS-Lite + signatures │ │ - LoRa SF10-12 (<1 kbps) → MLS-Lite, no signatures │ │ │ │ 3. Wrap payload in appropriate envelope │ │ 4. Fragment if needed for transport MTU │ │ │ └─────────────────────────────────────────────────────────────┘ ``` ### MLS-Lite Envelope (Minimal Mode) For SF12 LoRa where every byte counts: ```rust pub struct MlsLiteEnvelope { // Header: 25 bytes pub version: u8, // 1 byte: 0x02 = MLS-Lite pub flags: u8, // 1 byte: [has_sig, priority(2), reserved(5)] pub group_id: [u8; 8], // 8 bytes: truncated group identifier pub sender_addr: [u8; 4], // 4 bytes: truncated sender address pub seq: u32, // 4 bytes: sequence number (replay protection) pub epoch: u16, // 2 bytes: key epoch (for rotation) pub nonce: [u8; 5], // 5 bytes: ChaCha20 nonce suffix (epoch is prefix) // Payload: variable pub ciphertext: Vec, // ChaCha20-Poly1305 encrypted // includes 16-byte auth tag // Optional signature: 64 bytes (if has_sig flag set) pub signature: Option<[u8; 64]>, } // Minimal overhead: 25 bytes header + 16 bytes tag = 41 bytes // With signature: 105 bytes total overhead ``` ### Encryption Details ``` Key derivation: group_secret = HKDF-SHA256( ikm = pre_shared_key || group_id, salt = "quicprochat-mls-lite-v1", info = epoch.to_be_bytes() ) encryption_key = group_secret[0..32] // ChaCha20 key nonce_prefix = group_secret[32..39] // 7 bytes Full nonce (12 bytes): nonce = nonce_prefix || envelope.nonce Encrypt: ciphertext = ChaCha20-Poly1305( key = encryption_key, nonce = nonce, plaintext = payload, aad = header_bytes // version, flags, group_id, sender_addr, seq, epoch ) ``` ### Key Exchange (Out-of-Band) MLS-Lite groups are established via: 1. **QR Code:** Scan to join group (contains group_secret + group_id) 2. **NFC Tap:** Bump phones to exchange group key 3. **Voice Readout:** 24-word mnemonic for group secret 4. **Faster Link:** Full MLS setup over QUIC, then extract epoch key for MLS-Lite ``` ┌─────────────────────────────────────────────────────────────┐ │ Key Exchange Flow │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Option A: QR Code (in-person) │ │ Alice generates: QR(group_id || group_secret) │ │ Bob scans → joins MLS-Lite group │ │ │ │ Option B: MLS Bootstrap (hybrid) │ │ 1. Alice & Bob establish full MLS group over Internet │ │ 2. Export current epoch key as MLS-Lite group_secret │ │ 3. Both can now communicate over LoRa using MLS-Lite │ │ 4. When Internet available, re-sync to full MLS │ │ │ │ Option C: Pre-Shared Key (deployment) │ │ Org distributes group_secret to all devices │ │ Like Meshtastic channel key, but with replay protection │ │ │ └─────────────────────────────────────────────────────────────┘ ``` ### Key Rotation MLS-Lite does NOT have automatic post-compromise security. Manual rotation: ``` Rotation trigger: - Periodic (e.g., weekly) - Member leaves group - Suspected compromise Rotation process: 1. New group_secret generated (QR code, or via full MLS if available) 2. epoch incremented 3. Old key deleted after grace period 4. Devices that miss rotation must re-join ``` ### Upgrade to Full MLS When faster transport becomes available: ``` ┌─────────────────────────────────────────────────────────────┐ │ MLS-Lite → MLS Upgrade │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. Device detects QUIC/TCP connectivity │ │ 2. Contacts server, fetches peer KeyPackages │ │ 3. Creates full MLS group with same group_id │ │ 4. Sends MLS Welcome to all known members │ │ 5. Members upgrade to full MLS │ │ 6. MLS-Lite continues in parallel for LoRa-only members │ │ │ │ Bridging: │ │ - Gateway nodes (CAP_GATEWAY) translate between modes │ │ - Full MLS message → re-encrypt as MLS-Lite for LoRa │ │ - MLS-Lite message → forward as MLS AppMessage │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Security Analysis ### What MLS-Lite Provides | Property | Full MLS | MLS-Lite | Notes | |----------|----------|----------|-------| | **Confidentiality** | ✓ | ✓ | ChaCha20-Poly1305 | | **Integrity** | ✓ | ✓ | Poly1305 MAC | | **Replay protection** | ✓ | ✓ | Sequence numbers | | **Sender auth (group)** | ✓ | ✓ | Only group members can encrypt | | **Sender auth (individual)** | ✓ | Optional | Ed25519 signature (64 bytes) | | **Forward secrecy** | ✓ | Partial | Only on manual epoch rotation | | **Post-compromise security** | ✓ | ✗ | No automatic healing | | **Transcript consistency** | ✓ | ✗ | No ratchet tree | | **Deniability** | ✗ | ✗ | Neither provides this | ### Threat Model **Protected against:** - Passive eavesdropping (even quantum with PQ group_secret) - Message replay (sequence numbers) - Message tampering (AEAD) - Outsider injection (need group_secret) **NOT protected against:** - Compromised group member reading all traffic (no PCS) - Long-term key compromise without manual rotation - Relay node with group_secret (but they're in the group anyway) ### Comparison to Meshtastic | Property | Meshtastic | MLS-Lite | |----------|------------|----------| | **Encryption** | AES-256-CTR | ChaCha20-Poly1305 | | **Authentication** | None (shared key) | Optional Ed25519 | | **Replay protection** | None | Sequence numbers | | **Key rotation** | Manual | Manual (epoch field) | | **Overhead** | 16 bytes (header) | 41 bytes (no sig), 105 bytes (with sig) | | **Upgrade path** | None | → Full MLS | MLS-Lite is strictly better than Meshtastic's crypto while fitting similar constraints. --- ## Wire Format ### MLS-Lite Envelope (CBOR) ``` MlsLiteEnvelope = { 0: uint, ; version (0x02) 1: uint, ; flags 2: bytes .size 8, ; group_id 3: bytes .size 4, ; sender_addr 4: uint, ; seq 5: uint, ; epoch 6: bytes .size 5, ; nonce 7: bytes, ; ciphertext (includes 16-byte tag) ? 8: bytes .size 64 ; signature (optional) } ``` Estimated sizes: - Minimal (1-byte payload): ~50 bytes (fits SF12 unfragmented!) - Short message (20 bytes): ~70 bytes (2 fragments on SF12) - With signature: add 64 bytes ### MeshEnvelope Mode Flag Extend MeshEnvelope to indicate crypto mode: ```rust pub struct MeshEnvelope { // ... existing fields ... /// Crypto mode: 0x00 = full MLS, 0x02 = MLS-Lite pub crypto_mode: u8, } ``` --- ## Implementation Plan ### Phase 1: Core MLS-Lite 1. [ ] Define `MlsLiteEnvelope` struct 2. [ ] Implement key derivation (HKDF) 3. [ ] Implement encrypt/decrypt (ChaCha20-Poly1305) 4. [ ] Add sequence number tracking (replay window) 5. [ ] Add CBOR serialization 6. [ ] Unit tests ### Phase 2: Integration 1. [ ] Add `crypto_mode` to TransportManager routing decisions 2. [ ] Implement QR code key exchange (generate/scan) 3. [ ] Add `/mesh lite-create ` REPL command 4. [ ] Add `/mesh lite-join ` REPL command 5. [ ] Integration tests with LoRaMockMedium ### Phase 3: Gateway/Bridge 1. [ ] Implement MLS → MLS-Lite translation in gateway nodes 2. [ ] Add CAP_GATEWAY capability flag 3. [ ] Handle epoch sync between modes 4. [ ] End-to-end test: QUIC client → gateway → LoRa client --- ## Open Questions 1. **Signature vs. no signature?** - Signatures add 64 bytes (1-2 extra fragments on SF12) - Without signatures, any group member can spoof any sender - Proposal: configurable, default to signatures on SF7-9, skip on SF10-12 2. **Epoch sync without server?** - How do LoRa-only nodes learn about epoch changes? - Proposal: Include epoch in announce, peers relay epoch updates 3. **Post-quantum group_secret?** - MLS-Lite uses symmetric crypto (quantum-safe for confidentiality) - Key exchange is vulnerable if using X25519 - Proposal: QR code includes ML-KEM-768 encapsulation for PQ key exchange 4. **Compatibility with Reticulum/LXMF?** - Should we use msgpack instead of CBOR for LXMF compat? - Should we implement LXMF as an additional mode? --- ## References - [MLS RFC 9420](https://datatracker.ietf.org/doc/rfc9420/) — Full MLS spec - [ChaCha20-Poly1305 RFC 8439](https://datatracker.ietf.org/doc/rfc8439/) - [HKDF RFC 5869](https://datatracker.ietf.org/doc/rfc5869/) - [Meshtastic Encryption](https://meshtastic.org/docs/overview/encryption/) - [Reticulum LXMF](https://github.com/markqvist/LXMF) --- *Last updated: 2026-03-30*