We use the MLS Protocol for message encryption, specified by the RFC-9420 spec document. If you want to learn more about MLS, the architecture documentation (RFC-9750) is a good place to start.
This chapter explains how MLS is applied within Rhizo: how group membership works, how messages are distributed and protected, and what happens when you remove someone from a group.
Core MLS concepts
Readers familiar with MLS may skip this section.
About MLS and open-mls
MLS is, as the name suggests, a security layer for end-to-end encrypted group messaging, maintained by the MLS working group at the IETF, an Internet standards organization. It’s designed to scale efficiently to groups as large as 50,000 members, and to support users who access chat from multiple devices.
The open-mls library we use in Rhizo, implements the spec in a way that makes it easy to use in applications in a safe manner.
When we say “MLS requires” or “MLS needs”, it refers to a requirement coming directly from the protocol specification.
Post-quantum note
As of August 2026, open-mls does not yet support post-quantum key exchange. We do hope to land post-quantum support before we get to some beta versions of the application. We talk about it more in the Open Questions chapter.
Groups, group state and epochs
Groups are fundamental to MLS, and carry specific meaning.
Note: italics denote MLS-specific terms.
An MLS group is a collection of authenticated clients where each member holds their own keys and they share a common secret value at any given time. In other words, they hold a shared cryptographic state, and groups effectively stay together as long as they share state.
Group state moves forward linearly in epochs: a point in time where all members agree on the same set of keys and the same view of group state (e.g., with regards to who belongs in the group etc.).
Message types
There are four kinds of MLS messages relevant to us:
| Type | Sent to | Purpose |
|---|---|---|
| Proposal | Group | Proposes a group state change (e.g. add/remove a member) |
| Commit | Group | Applies proposals and advances the epoch. |
| Application | Group | Carries app-level content: chat messages, document patches (in CRDT documents), etc. |
| Welcome | Individual | Sent to a newly invited member; contains the initial group state they use to bootstrap into the group |
Proposal/Commit type messages are MLS’s own group-control messages.
Adding a member, removing a member, and rotating keys are all done the same way: via a Commit message that advances the epoch. The commit instructs group members to implement a collection of Proposals, for example whom to include in the group.
Abstract Services: Authentication and Delivery
For the MLS protocol to function correctly application needs to define two abstract services, an Authentication Service and Delivery Service.
This is also where a common misreading of the protocol falls down: the assumption that security depends on “some external service you trust”. That’s why it’s worth highlighting the fact that the specification treats them as abstract services.
It is important to note that the AS can be completely abstract in the case of a service provider which allows MLS clients to generate, distribute, and validate credentials themselves. As with the AS, the DS can be completely abstract if users are able to distribute credentials and messages without relying on a central DS (as in a peer-to-peer system).
The Messaging Layer Security (MLS) Architecture (RFC-9750)
What performs the role of Authentication Service?
In Rhizo the chains themselves are the authentication service — there’s no separate service to trust. What you are trusting is the cryptographic construction of the chains.
For MLS to function Authentication Service needs to perform three functions:
- Issue credentials to clients that attest to bindings between identities and signature key pairs. — In Rhizo credential is simply the pairing of client’s identity chain id, and the identity key, which exists on the chain.
- Enable a client to verify that a credential presented by another client is valid with respect to a reference identifier. — Any client is able to verify that credential is valid and exists on space chain, as well as perform check that referenced identity chain id contains the identity key. This also allows client to fetch currently valid MlsCredentialKey which is used by the client to sign the MLS message.
- Enable a group member to verify that a credential represents the same client as another credential. — Identity chain is what binds the all the identity keys to a single user.
How messages are distributed
To perform the role of Delivery Service we use a server that holds messages in anonymous mailboxes. The server and mailboxes are explained in more detail in their own chapters.
We do two things worth noting from the perspective of MLS: strict commit ordering through a server-side group mailbox, and client-side fan-out of application messages.
Strictly ordered commits
If two devices commit at the same time, and group members disagree about which commit “won” the group forks — different sets of members end up with different views of state. This can look like messages failing to decrypt or people getting silently removed/desynced from the group until the conflict is resolved.
To keep groups in sync, every member must converge on the same, single ordering of commits within group history.
In Rhizo, we solve this through strict ordering of commits. A server-side group mailbox (described later in more detail) decides the order of messages — it assigns sequence number to all messages by arrival order.
The first valid commit the mailbox accepts for an epoch is treated as authoritative; later ones that lose the race are rejected and the sender must (rebase and) retry against the new epoch.
Client-side message fan-out
There are two ways to send messages in Rhizo:
- Server-side fan-out, where you send message to a shared group mailbox to which all the group members have access to.
- Client-side fan-out, where client prepares the message and sends it to personal mailbox of each group participant.
We use client-side fan-out for application messages in smaller groups because it minimizes the time the server stores messages, as the server can delete them as soon as the client downloads them and confirms receiving them.
In general, the server never holds onto messages any longer than necessary. Less data resting on a server means less exposure if that server is ever compromised, largely used as a precaution against harvest now, decrypt later attacks.
Harvest now, decrypt later is a surveillance strategy that relies on acquiring and storing currently unreadable encrypted data while waiting for possible breakthroughs in decryption technology that would make it readable in the future.
The downside is that the larger the group, the more messages the client needs to send, which means more network traffic and opportunities for things to go wrong. That’s why for larger groups (and groups that require strict ordering in the application layer) we send all messages through group mailboxes.
Becoming a group member
There are two main ways to join groups.
Adding a member via key package
Adding someone to a group requires first fetching their current key package: a signed, published bundle a user generates so others can add them to a group.
It wraps a key that others use to encrypt a group key for that user. It’s signed with the user’s credential key, so its authenticity is verifiable.
Joining a group via external Commit
For groups designated as “public” within a Space, it’s possible to join a group through an External Commit. Group members can share the GroupInfo required to join it in a Catalogue, which allows individuals to join the group independently without having to be added by an existing member.
Removing a member from a group
Removing a member from a group is done through a commit. This commit advances a group epoch, with the keys for that member removed from the ratchet tree.
This makes it impossible for removed member to derive the secrets used for encryption and mailbox access for future group epochs. In order for them to gain access to the group, they will need to be re-added.
Extending MLS: Group Context Extension
We use the MLS GroupContextExtensions extension to attach a group type to the group state (e.g. to answer, is this group a document, a channel, or something else?).
We use it because when a client receives a Welcome message for a new group, it needs to know what kind of group it’s bootstrapping into before it can render or handle it correctly. Without this extension, there’s no way to know that at Welcome time.
More extensions or extension-driven metadata may be added in the future.