Skip to content

MLS Integration and Message Security

MLS is the gold standard for group messaging security. It has been standardized as RFC 9420. A simple way to think about it is as “TLS for group chats”.

The protocol provides:

  • Forward Secrecy: Compromise of current keys doesn’t affect past messages.

  • Post-Compromise Security: New key material recovers security after a breach.

  • Group Key Agreement: Efficient key management for large groups.

  • Asynchronous Operation: Members can be added while offline.

  • Scalability: Supports groups consisting of thousands of members.

FeatureSignal ProtocolMLS
Group SizeEfficient for small groupsScales to 50,000+ members
Forward SecrecyYesYes
Post-Compromise SecurityYesYes
StandardizationSignal-specificIETF RFC 9420
Asynchronous OperationLimitedFull support
Future-ProofingSignal controlsIndustry standard

At the moment, Cryptid supports one MLS cipher suite:

  • MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519
    • Encryption: AES-128-GCM
    • Best for: Modern devices with hardware AES acceleration
    • Performance: Very fast with AES-NI or ARM Crypto Extensions

I’m exploring support for MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519 for devices without AES hardware (older hardware, embedded systems). I’ll update this section once I decide.

Common components:

  • Key Exchange: X25519 (DHKEM)
  • Hash: SHA-256
  • Signatures: Ed25519
  • Security Level: 128-bit

Both cipher suites are equally secure. Clients negotiate during group creation based on hardware capabilities.

All communication uses this unified message format:

struct SecureMessage {
// Cryptographically random
message_id: [u8; 32],
// MLS group identifier
group_id: [u8; 32],
// MLS-encrypted payload
mls_ciphertext: Vec<u8>,
// Device signature over ciphertext
sender_signature: Ed25519Signature,
// Creation timestamp
timestamp: u64,
// Type information
message_type: MessageType,
}
enum MessageType {
// UTF-8 text content
Text,
// File with metadata
Media { mime_type: String },
// Group management
SystemOperation,
// Initial contact establishment
ContactRequest,
}
struct DirectMessageGroup {
// SHA256 of sorted device IDs
group_id: [u8; 32],
// Exactly 2 devices
participants: [DeviceIdentity; 2],
// Current epoch and keys
mls_state: MLSGroupState,
created_at: u64,
}
// Group ID is deterministic: same for both participants
fn create_dm_group_id(device1: &[u8; 32], device2: &[u8; 32]) -> [u8; 32] {
let mut sorted = [*device1, *device2];
sorted.sort(); // Ensures same ID regardless of who creates the group
sha256(&sorted.concat())
}
struct MultiUserGroup {
// Cryptographically random
group_id: [u8; 32],
// Who created the group
founder_device: [u8, 32],
// Current members
members: Vec<GroupMember>,
// Current MLS epoch and keys
mls_status: MLSGroupState,
// Devices with admin privileges
admin_devices: HashSet<[u8; 32]>,
created_at: u64,
}
struct GroupMember {
device_id: [u8; 32],
// From MLS group context
public_key: Ed25519PublicKey,
// For message routing
delivery_addresses: Vec<String>,
// Which member added them
added_by: [u8; 32],
added_at: u64,
participation_status: ParticipationStatus,
}

KeyPackages are one-time-use cryptographic credentials that enable asynchronous group additions. They allow someone to add you to a group even when you’re offline.

KeyPackage structure:

struct KeyPackage {
protocol_version: ProtocolVersion,
cipher_suite: CipherSuite,
init_key: HPKEPublicKey, // Ephemeral public key for Welcome encryption
leaf_node: LeafNode {
encryption_key: HPKEPublicKey, // Ephemeral encryption key
signature_key: Ed25519PublicKey, // Device identity key
credential: BasicCredential {
identity: device_id, // Device identity
},
},
signature: Ed25519Signature, // Signed by device's private key
}

Key properties:

  • One-time use: Each KeyPackage can only be used to add to one group
  • Pre-generated: Devices upload 50-100 KeyPackages to servers
  • Ephemeral keys: Each KeyPackage has unique HPKE keys
  • Device identity: All KeyPackages share the same device credential

Devices generate KeyPackages containing:

  • Device credential (device_id, signature key)
  • Ephemeral HPKE keys (unique per KeyPackage)
  • Self-signature proving device ownership

Public KeyPackages are uploaded to the device’s primary server

//POST /api/v1/keypackage/upload
{
"device_id": "...",
"key_packages": [/* 100 public KeyPackages */],
"signature": "...",
"timestamp": 1758315663
}

When adding someone to a group, fetch their KeyPackage:

// GET /api/v1/keypackage/fetch?device_id={device_id}
// Server returns one KeyPackage and deletes it (one-time use)
{
"device_id": "...",
"key_package": "...",
"remaining": 99
}

Use the KeyPackage to add the member:

// Add Alice to group using her KeyPackage
let (commit, welcome, _) = group.add_members(
provider,
&adder_keypair,
&[alice_keypackage]
)?;
// Welcome is encrypted with Alice's KeyPackage public key
send_welcome(&alice.delivery_addresses, welcome)?;

Alice uses her stored private key to decrypt the Welcome message

// Alice has the private key from when she generated the KeyPackage
let staged_join = StagedWelcome::new_from_welcome(
provider,
&config,
welcome,
None, // Ratchet tree included in Welcome (RatchetTreeExtension)
)?;
let alice_group = staged_join.into_group(provider)?;

When a KeyPackage count drops below 20, upload more:

// Background task
if device.remaining_keypackages() < 20 {
let new_keypackage = device.generate_key_packages(100);
upload_keypackages(server, new_kps)?;
}

Why KeyPackages Are Separate from Contacts

Section titled “Why KeyPackages Are Separate from Contacts”

ShareableIdentityBundle (for contact exchange) does NOT include KeyPackages:

  • Bundles are long-lived (shared via QR codes)
  • KeyPackages are ephemeral (used once)
  • Bundles would need constant regeneration

Instead, bundles specify keypackage_server where KeyPackages can be fetched on-demand.

See Contact Exchange and Trust Establishment for complete KeyPackage management details.

MLS uses something called TreeKEM to continuously rotate encryption keys. What this means is that even if an attacker gets today’s keys, yesterday’s messages still remain secure. Keys are deleted after they’re used, not just replaced.

Let’s say Alice’s device gets compromised. Since keys are rotated, her security will be recovered during the next rotation. New members joining the group will trigger key rotation and active members regularly update keys to heal from potential compromises.

All Cryptid groups MUST enable the RatchetTreeExtension for simpler Welcome handling.

Why this matters:

When joining a group via Welcome, new members need:

  1. The Welcome message (encrypted with their KeyPackage)
  2. The ratchet tree (group’s current key state)

Without RatchetTreeExtension:

  • Ratchet tree must be sent out-of-band
  • Adds complexity and potential failure points

With RatchetTreeExtension:

  • Ratchet tree is included in the Welcome message
  • Single message contains everything needed to join
  • Simpler, more reliable

This is required in the Cryptid protocol to ensure reliable group joins.

Every MLS operation (adding members, sending messages, etc) is cryptographically authenticated. This makes it impossible to forge group membership or operations. All members can verify all group operations and history.

Important: MLS uses device_id (not delivery addresses) for authentication.

What MLS sees:

// MLS credential contains device identity
struct BasicCredential {
identity: device_id, // Permanent device identity (32 bytes)
}
// When encrypting a message
fn encrypt_message(plaintext: &[u8], group: &MlsGroup) -> Vec<u8> {
// MLS automatically includes:
// - Sender's device_id (from credential)
// - Message content
// - Authentication data
// - Epoch information
}

What MLS doesn’t see:

  • Delivery addresses (used for routing, not authentication)
  • Server domains
  • Routing metadata

Privacy implications:

  • Group members know each other’s device_ids (MLS requirement)
  • Recipients can block by device_id (survives address rotation)
  • Servers don’t learn device_ids from messages (addresses used for routing)

Separation of concerns:

ComponentIdentityRouting
MLSdevice_idNot involved
ServersHiddendelivery_address
Contactsdevice_iddelivery_addresses

This separation ensures:

  • MLS handles cryptographic identity
  • Servers handle message routing
  • Neither learns more than necessary

Cryptid uses and recommends OpenMLS for MLS protocol implementation.

OpenMLS features leveraged:

  • Automatic key rotation (TreeKEM)
  • RatchetTreeExtension support
  • Staged operations for validation
  • Multiple cipher suite support
  • TLS serialization for wire format

See OpenMLS documentation for implementation details.