# 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*
