Honest assessment of QuicProChat vs Reticulum/Meshtastic/Briar: - MLS overhead (500-800 byte KeyPackages) impractical for SF12 LoRa - KeyPackage distribution over mesh unsolved - No lightweight mode for constrained links MLS-Lite design proposes 41-byte overhead symmetric mode: - ChaCha20-Poly1305 with HKDF key derivation - Optional Ed25519 signatures - Upgrade path to full MLS when faster transport available - QR code / out-of-band key exchange
12 KiB
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
- Short message overhead: <50 bytes for a "hello" message (fits SF12 MTU unfragmented)
- Group encryption: Shared symmetric key, not just link encryption
- Sender authentication: Ed25519 signature (64 bytes, fragmentable)
- Upgrade path: Seamless transition to full MLS when faster link available
- 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:
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<u8>, // 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:
- QR Code: Scan to join group (contains group_secret + group_id)
- NFC Tap: Bump phones to exchange group key
- Voice Readout: 24-word mnemonic for group secret
- 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:
pub struct MeshEnvelope {
// ... existing fields ...
/// Crypto mode: 0x00 = full MLS, 0x02 = MLS-Lite
pub crypto_mode: u8,
}
Implementation Plan
Phase 1: Core MLS-Lite
- Define
MlsLiteEnvelopestruct - Implement key derivation (HKDF)
- Implement encrypt/decrypt (ChaCha20-Poly1305)
- Add sequence number tracking (replay window)
- Add CBOR serialization
- Unit tests
Phase 2: Integration
- Add
crypto_modeto TransportManager routing decisions - Implement QR code key exchange (generate/scan)
- Add
/mesh lite-create <name>REPL command - Add
/mesh lite-join <qr-data>REPL command - Integration tests with LoRaMockMedium
Phase 3: Gateway/Bridge
- Implement MLS → MLS-Lite translation in gateway nodes
- Add CAP_GATEWAY capability flag
- Handle epoch sync between modes
- End-to-end test: QUIC client → gateway → LoRa client
Open Questions
-
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
-
Epoch sync without server?
- How do LoRa-only nodes learn about epoch changes?
- Proposal: Include epoch in announce, peers relay epoch updates
-
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
-
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 — Full MLS spec
- ChaCha20-Poly1305 RFC 8439
- HKDF RFC 5869
- Meshtastic Encryption
- Reticulum LXMF
Last updated: 2026-03-30