Skip to main content

MLS (Messaging Layer Security) Layer: The Meeting Room

Mental Model: The Meeting Room​

Imagine you and 5 friends want to have a private conversation in a restaurant. You need to:

  1. Find a private room that sits all 6 people
  2. Ensure everyone can hear everyone else
  3. Handle people arriving/leaving mid-conversation
  4. Keep the room secure even with changing membership

MLS is like that meeting room for secure group messaging while maintaining all the security properties of Signal!

The Group Ratchet​

Unlike Signal (for 1-on-1), MLS handles groups by using a tree-based ratchet:

  • Imagine a binary tree where each leaf is a group member
  • The root of the tree encrypts for everyone
  • When someone joins/leaves, you ratchet the tree (change the keys)
  • Everyone gets new keys but they all derive the same root key

Result: Group messaging with perfect forward secrecy.

Why Use MLS in Cascading Cipher?​

🎯 The Problem​

In Signal, we have perfect 1-on-1 messaging. But for groups:

  1. Naive approach: Broadcast DH handshake for each new member (slow!)
  2. Naive approach: Everyone runs separate Signal sessions (doesn't handle joins/leaves)
  3. Naive approach: Use a secret and share it (breaks forward secrecy when someone leaves)

We need a way to:

  • Send messages to many people with encryption
  • Handle joins and leaves with forward secrecy
  • Maintain perfect PFS (compromise reveals nothing but current message)
  • Keep the ratcheting property (no going back)

πŸ›‘οΈ The MLS Solution​

MLS uses the Ratchet Tree to solve this:

Ratchet Tree Structure​

                        Root (Group Key)
/ \
Node A Node B
/ \ / \
Charlie Dana Eve Frank Grace
  • Root: Holds the group encryption key
  • Internal nodes: Hold temporary keys for subsets
  • Leaves: Each member's position in the tree

How It Works​

  1. Alice sends a message:

    • Alice knows the Root Key (from the tree)
    • She encrypts with it
    • Everyone decrypts with the same Root Key
  2. Bob joins the group:

    • Add Bob as a new leaf
    • Update the Ratchet Tree along Bob's path (leaf β†’ root)
    • All new keys along that path
    • Everyone receives new Root Key
  3. Charlie leaves:

    • Remove Charlie's leaf
    • Update the Ratchet Tree (path from Charlie's leaf)
    • New keys for affected nodes and Root
    • Charlie can no longer decrypt future messages!

Where It Fits in the Stack​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ πŸ›οΈ MLS Layer (Group Ratchet) β”‚ ← Tree-based, handles joins/leaves
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ πŸ“ž Signal Layer (Pair Ratchet) β”‚ ← Per-message keys (1-on-1 only)
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 🀝 DH Layer (Key Exchange) β”‚ ← Root key for tree
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ AES Layer (Foundation) β”‚ ← Encrypts with group key
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

MLS takes group membership changes and creates a fresh group key for everyone, maintaining perfect PFS for groups.


How MLS Works​

The Ratchet Tree​

Initial Group Setup​

// Initialize a MLS group with 3 members
const group = await MLS.createGroup({
members: [
{ name: 'Alice', publicKey: alicePublicKey },
{ name: 'Bob', publicKey: bobPublicKey },
{ name: 'Charlie', publicKey: charliePublicKey }
],
epoch: 0 // Version of the group
})

// Create the Ratchet Tree
const ratchetTree = await MLS.createRatchetTree({
members: group.members
})

// Each member gets their leaf position and path secrets
const aliceState = await group.getMemberState('Alice')
const bobState = await group.getMemberState('Bob')
const charlieState = await group.getMemberState('Charlie')

Sending a Message​

// Alice wants to send a message
const aliceMessage = 'Hello everyone!'

// Alice derives the Root Key from her position in the tree
const rootKey = await MLS.deriveRootKey(aliceState, ratchetTree)

// Encrypt with the Root Key (same for everyone!)
const encrypted = await AES_GCMEncrypt(aliceMessage, rootKey)
console.log('Encrypted group message:', encrypted)

Receiving a Message​

// Bob receives the message

// Bob also derives the same Root Key (he knows his path secrets!)
const bobRootKey = await MLS.deriveRootKey(bobState, ratchetTree)

// Decrypt using the same Root Key
const decrypted = await AES_GCMDecrypt(encrypted, bobRootKey)
console.log('Decrypted:', decrypted)
// "Hello everyone!"

A Member Joins (Bob)​

// Add Bob to the group
const newGroup = await group.addMember({
name: 'Bob',
publicKey: bobPublicKey
})

// Create a Welcome message with Bob's path secrets
const welcomeMessage = await MLS.createWelcomeMessage({
newMemberState: await newGroup.getMemberState('Bob'),
ratchetTree,
groupInfo: newGroup.info
})

// Ratchet the tree: update path from Bob's leaf to root
const newRatchetTree = await MLS.ratchetTree({
ratchetTree,
memberIndex: 3, // Bob's position
newGroupInfo: newGroup.info
})

// Bob receives Welcome, learns his path secrets
await Bob.processWelcome(welcomeMessage)
// Now Bob can derive the Root Key and decrypt future messages!

// Alice and Charlie receive the new Root Key and ratchet
await Alice.updateGroupState(newRatchetTree)
await Charlie.updateGroupState(newRatchetTree)

A Member Leaves (Charlie)​

// Remove Charlie from the group
const newGroup = await group.removeMember('Charlie')

// Ratchet the tree: update path from Charlie's leaf to root
const newRatchetTree = await MLS.ratchetTree({
ratchetTree,
memberIndex: 2, // Charlie's position
newGroupInfo: newGroup.info
})

// Create a Commit message with the new Root Key
const commitMessage = await MLS.createCommitMessage({
newRatchetTree,
groupInfo: newGroup.info
})

// Send Commit to remaining members
await Alice.processCommit(commitMessage) // Updates to new keys
await Bob.processCommit(commitMessage) // Updates to new keys

// Charlie, who left, can't decrypt anymore!
// The Root Key has changed, and Charlie doesn't have the new path secrets

Why Can't a Leaver Decrypt Future Messages?​

The Cryptographic Guarantee​

When Charlie leaves, MLS:

  1. Creates a new Ratchet Tree along Charlie's path
  2. Generates new secrets for all nodes on that path
  3. Derives a new Root Key at the top

Charlie only knows:

  • His old path secrets (from when he was in the group)
  • The old Root Key

But now:

  • They use a new Root Key
  • Charlie's old path secrets can't derive it (one-way KDF!)

It's like changing all the locks in a hotel room after someone checks out, except you don't even need to change the physical locksβ€”just the combination, which you keep secret.

If Eve Compromises Charlie After He Leaves​

Eve gets Charlie's ratchet tree state (his path secrets and keys):

Eve decrypts past messages? Yes (Charlie was there)
Eve decrypts future messages? NO (Root Key changed!)

MLS ensures compromise reveals nothing about future messages.


Using the MLS Layer​

Basic Group Setup​

import { MLSLayer } from '@cascading-cipher/mls-layer'

// Create an MLS group
const mlsLayer = new MLSLayer({
groupName: 'Our Secure Group',
members: [
{ name: 'Alice', publicKey: alicePublicKey },
{ name: 'Bob', publicKey: bobPublicKey },
{ name: 'Charlie', publicKey: charliePublicKey }
]
})

Sending a Message to the Group​

// Alice sends a message
const plaintext = 'Group message from Alice!'

const encrypted = await mlsLayer.encrypt(
new TextEncoder().encode(plaintext)
)

// Output includes:
// - ciphertext (encrypted with group Root Key)
// - epoch (version of the group)
// - sender info (Alice's position)
// - signature (for authenticity)
console.log('Encrypted group message:', encrypted)

Receiving and Decrypting​

// Bob receives the message

// Decrypt using the group Root Key
const decrypted = await mlsLayer.decrypt(encrypted)

console.log('Decrypted:', new TextDecoder().decode(decrypted))
// "Group message from Alice!"

Adding a Member​

// Add Dana to the group
const newGroup = await mlsLayer.addMember({
member: { name: 'Dana', publicKey: danaPublicKey }
})

// Create Welcome message
const welcomeMsg = await mlsLayer.createWelcomeMessage(newGroup)

// Dana receives Welcome and joins
const newMemberState = await Dana.processWelcome(welcomeMsg)
console.log('Dana joined the group!')

Removing a Member​

// Remove Charlie (he might have been compromised!)
const newGroup = await mlsLayer.removeMember('Charlie')

// Create Commit message
const commitMsg = await mlsLayer.createCommitMessage(newGroup)

// Remaining members (Alice, Bob, Dana) update
await aliceMlsLayer.processCommit(commitMsg)
await bobMlsLayer.processCommit(commitMsg)
await danaMlsLayer.processCommit(commitMsg)
// Charlie gets Commit but can't decrypt (he was removed)

Integration with CascadingCipherManager​

import { CascadingCipherManager } from '@cascading-cipher/manager'
import { AESCipherLayer } from '@cascading-cipher/aes-layer'
import { DHCipherLayer } from '@cascading-cipher/dh-layer'
import { MLSLayer } from '@cascading-cipher/mls-layer'

// Add all layers for groups
const manager = new CascadingCipherManager()

// 1. DH for initial root key
await manager.addLayer(new DHCipherLayer({
myPrivateKey: myPrivateKey,
theirPublicKey: groupPublicKey
}))

// 2. MLS for group tree and key ratcheting
await manager.addLayer(new MLSLayer({
groupName: 'Secure Development Team',
members: [
{ name: 'Alice', publicKey: alicePublicKey },
{ name: 'Bob', publicKey: bobPublicKey },
{ name: 'Charlie', publicKey: charliePublicKey }
]
}))

// 3. AES for encryption
await manager.addLayer(new AESCipherLayer())

// Encrypt: DH root β†’ MLS tree β†’ AES with group key
const encrypted = await manager.encrypt(
'Team, deploy to production at midnight!'
)

// Everyone decrypts with the same group Root Key
const decrypted = await manager.decrypt(encrypted)
console.log('Group message:', decrypted)

Complete Example: Secure Group Chat​

async function secureGroupChat() {
// Group members
const members = {
alice: await generateKeyPair(),
bob: await generateKeyPair(),
charlie: await generateKeyPair()
}

// Create Alice's manager (she's the admin)
const aliceManager = new CascadingCipherManager()
await aliceManager.addLayer(new DHCipherLayer({
myPrivateKey: members.alice.privateKey,
theirPublicKey: members.bob.publicKey // Init with Bob
}))
await aliceManager.addLayer(new MLSLayer({
groupName: 'Dev Team',
members: [
{ name: 'Alice', publicKey: members.alice.publicKey },
{ name: 'Bob', publicKey: members.bob.publicKey }
]
}))
await aliceManager.addLayer(new AESCipherLayer())

// Alice sends the first message
const encrypted1 = await aliceManager.encrypt(
'Welcome to our secure group chat!'
)
console.log('Alice sent encrypted message')

// Bob creates his manager (mirrored)
const bobManager = new CascadingCipherManager()
await bobManager.addLayer(new DHCipherLayer({
myPrivateKey: members.bob.privateKey,
theirPublicKey: members.alice.publicKey
}))
await bobManager.addLayer(new MLSLayer({
groupName: 'Dev Team',
members: [
{ name: 'Alice', publicKey: members.alice.publicKey },
{ name: 'Bob', publicKey: members.bob.publicKey }
]
}))
await bobManager.addLayer(new AESCipherLayer())

// Bob decrypts
const decrypted1 = await bobManager.decrypt(encrypted1)
console.log('Bob received:', new TextDecoder().decode(decrypted1))

// Add Charlie to the group
const charlieAdded = await (aliceManager.layers[1] as MLSLayer).addMember({
member: { name: 'Charlie', publicKey: members.charlie.publicKey }
})
const welcomeMsg = await (aliceManager.layers[1] as MLSLayer).createWelcomeMessage(charlieAdded)
console.log('\nCharlie joining group...')

// Charlie sets up with Welcome
const charlieManager = new CascadingCipherManager()
await charlieManager.addLayer(new DHCipherLayer({
myPrivateKey: members.charlie.privateKey,
theirPublicKey: members.alice.publicKey
}))
await charlieManager.addLayer(new MLSLayer({
groupName: 'Dev Team',
members: [
{ name: 'Alice', publicKey: members.alice.publicKey },
{ name: 'Bob', publicKey: members.bob.publicKey },
{ name: 'Charlie', publicKey: members.charlie.publicKey }
]
}))
await charlieManager.addLayer(new AESCipherLayer())

// Charlie processes Welcome
await charlieManager.layers[1].processWelcome(welcomeMsg)

// Alice and Bob update
const commitMsg = await (aliceManager.layers[1] as MLSLayer).createCommitMessage(charlieAdded)
await (bobManager.layers[1] as MLSLayer).processCommit(commitMsg)

// Now Charlie can receive messages
const encrypted2 = await aliceManager.encrypt(
'Hi Charlie, welcome to the team!'
)
const decrypted2 = await charlieManager.decrypt(encrypted2)
console.log('Charlie received:', new TextDecoder().decode(decrypted2))

console.log('\nβœ… MLS handles group joins with perfect forward secrecy!')
}

// Helper
async function generateKeyPair() {
return await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveKey", "deriveBits"]
)
}

Security Properties​

βœ… Perfect Forward Secrecy (for Groups)​

  • Compromise of a member's state reveals only current message
  • Past messages: old Root Key can't be derived (ratcheted away)
  • Future messages: group ratchets on any membership change

βœ… Post-Compromise Security​

If Eve compromises Alice:

  • Eve can decrypt the current message Alice is sending
  • But Alice can ratchet her leaf and root
  • Future messages: Eve locked out

βœ… Group Forward Secrecy​

When Charlie leaves:

  • Charlie can't decrypt future messages (Root Key changed)
  • Alice and Bob can (they get new Root Key from tree ratchet)
  • Charlie's old path secrets are useless now

βœ… Message Authentication​

MLS signs every message:

  • Verification it's from the claimed sender
  • Can't be forged by Eve
  • Deniable: both parties could have signed (unlike digital signatures)

Quiz Time!​

🧠 How does MLS handle a member leaving the group?

MLS removes the member's leaf from the Ratchet Tree and ratchets all nodes along that member's path to the root. This creates a new Root Key for the remaining group. The leaver no longer has the new path secrets needed to derive it, so they're locked out of future messages.

🧠 What's the difference between Signal and MLS?
  • Signal: 1-on-1 messaging, double ratchet
  • MLS: group messaging, tree ratchet

Signal doesn't handle groups efficientlyβ€”you'd need separate sessions for each person. MLS handles groups natively with a single tree that ratchets on membership changes.

🧠 Why does MLS need a Ratchet Tree instead of a single key?

To handle membership changes efficiently! With a single key, if someone leaves, you'd need to send a new key to everyone (and not the leaker). With a Ratchet Tree, you only update nodes along the path from the leaver to the root, creating a new Root Key for the group. The leaver's old path secrets can't derive it!

🧠 What if Eve compromises Charlie and he's still in the group?

Eve can decrypt only the current message Charlie might send (if any). To read past or future messages, Eve would need:

  • Old Root Keys (gone, ratcheted away)
  • Charlie's other path secrets (deleted after use)

So compromise limited to the current message only!

🧠 How does MLS ensure new members can decrypt past messages?

It doesn't! MLS doesn't provide backward secrecy for new members. New members only get encryption for future messages. To give them past messages, you'd need to re-encrypt them, which breaks the forward secrecy guarantee.


Can You Explain to a 5-Year-Old?​

Imagine a secret meeting in a magical room:

  1. The room has invisible keys that magically change
  2. Everyone in the room knows the secret key
  3. When someone leaves, the key completely changes
  4. When someone new enters, they learn the new key from someone inside
  5. If you leave, the room's lock changesβ€”you can't get back in!
  6. If you're inside, you always have the right key

It's like a dynamic club where:

  • You're always let in if you're a member
  • When someone leaves, the secret club password changes
  • New members learn the new password from inside
  • Leavers are locked out forever (password changed!)

Key Takeaways​

βœ… MLS = Messaging Layer Security: Groups, not just 1-on-1

βœ… Ratchet Tree: Binary tree where leaves = group members, root = group key

βœ… Join/leave: Ratchet along the path β†’ new Root Key

βœ… Perfect PFS for groups: Past and future messages safe on compromise

βœ… No backward secrecy: New members can't decrypt past messages

βœ… One tree β†’ many keys: Efficient key ratcheting for groups

βœ… Used by: Apple iMessage (iCloud Keychain), WhatsApp (optionally), Matrix