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
326 lines
12 KiB
Markdown
326 lines
12 KiB
Markdown
# 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<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:
|
|
|
|
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 <name>` REPL command
|
|
4. [ ] Add `/mesh lite-join <qr-data>` 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*
|