Files
quicproquo/docs/plans/mls-lite-design.md
Christian Nennemann 01bc2a4273 docs: add mesh protocol gap analysis and MLS-Lite design
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
2026-03-30 23:29:44 +02:00

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

  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:

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:

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


Last updated: 2026-03-30