Documentation Index
Fetch the complete documentation index at: https://mintlify.com/openmls/openmls/llms.txt
Use this file to discover all available pages before exploring further.
OpenMLS provides flexible APIs for managing group membership. You can add or remove members immediately with a commit, or propose changes for later approval.
Adding members
Members are added to a group by providing their key packages. The key packages should be fetched from the delivery service.
The add_members() function adds members atomically and includes a path update:
use openmls::prelude::*;
let bob_key_package = // ... fetch from delivery service
let (commit_message, welcome_message, group_info) = alice_group.add_members(
provider,
&alice_signature_keys,
&[bob_key_package],
)?;
// Send commit to existing members
delivery_service.send_to_members(commit_message)?;
// Send welcome to new members
delivery_service.send_to_new_members(welcome_message)?;
// Optional: send group_info if ratchet tree extension is enabled
if let Some(group_info) = group_info {
delivery_service.send_group_info(group_info)?;
}
The function returns:
MlsMessageOut - Commit message for existing members
MlsMessageOut - Welcome message for new members
Option<GroupInfo> - Group info if ratchet tree extension is enabled
Adding members without update
For better performance, use add_members_without_update() which only includes a path if required by pending proposals:
let (commit_message, welcome_message, group_info) = alice_group.add_members_without_update(
provider,
&alice_signature_keys,
&[bob_key_package],
)?;
Not sending an update means the sender won’t achieve post-compromise security with this commit. However, it saves computation and bandwidth, especially with large public keys.
Using the commit builder
For more control, use the CommitBuilder API:
use openmls::prelude::*;
let bundle = alice_group
.commit_builder()
.propose_adds([bob_key_package].into_iter())
.load_psks(provider.storage())?
.build(provider.rand(), provider.crypto(), signer, |_| true)?
.stage_commit(provider)?;
let (commit, _, group_info) = bundle.into_contents();
let welcome = bundle.to_welcome_msg().ok_or("No welcome generated")?;
The KeyPackage is wrapped in Some because Option<KeyPackage> implements IntoIterator<Item = KeyPackage>. This means the function also works with any iterator over KeyPackage items or a Vec<KeyPackage>.
Proposing to add members
Create an add proposal without immediately committing:
let proposal_message = alice_group.propose_add_member(
provider,
&alice_signature_keys,
&bob_key_package,
)?;
// Send proposal to group members
delivery_service.send_to_members(proposal_message)?;
External add proposals
Outside parties can propose adding themselves or others if they’re registered in the ExternalSendersExtension.
External join proposal
A party can propose adding themselves:
use openmls::prelude::*;
let proposal = JoinProposal::new(
bob_key_package,
group_info,
&bob_signature_keys,
)?;
let proposal_message = proposal.into_plaintext_message(
provider.crypto(),
group_info.group_context(),
)?;
// Send to group members for validation
delivery_service.send_to_members(proposal_message)?;
Processing external proposals
Group members receive and validate external proposals:
let processed_message = alice_group.process_message(
provider,
protocol_message,
)?;
if let ProcessedMessageContent::ExternalJoinProposalMessage(proposal) = processed_message.into_content() {
// Application validates and decides whether to accept
alice_group.store_pending_proposal(provider.storage(), *proposal)?;
}
Removing members
Members can be removed by providing their leaf index.
Remove members atomically:
use openmls::prelude::*;
// Get the member's key package reference
let bob_index = alice_group
.members()
.find(|m| m.credential == bob_credential)
.map(|m| m.index)
.ok_or("Member not found")?;
let (commit_message, optional_welcome, group_info) = alice_group.remove_members(
provider,
&alice_signature_keys,
&[bob_index],
)?;
// Send commit to remaining members
delivery_service.send_to_members(commit_message)?;
// If there were pending add proposals, send welcome to new members
if let Some(welcome) = optional_welcome {
delivery_service.send_to_new_members(welcome)?;
}
Even though members were removed, the commit might also cover pending add proposals. That’s why an optional Welcome message is returned.
Proposing to remove members
Create a remove proposal:
let proposal_message = alice_group.propose_remove_member(
provider,
&alice_signature_keys,
bob_index,
)?;
delivery_service.send_to_members(proposal_message)?;
Leaving a group
Members can leave by creating a remove proposal for themselves:
let leave_proposal = bob_group.leave_group(
provider,
&bob_signature_keys,
)?;
delivery_service.send_to_members(leave_proposal)?;
Self-remove proposal
Alternatively, use a self-remove proposal (must be sent as PublicMessage):
let self_remove_proposal = bob_group.leave_group_via_self_remove(
provider,
&bob_signature_keys,
)?;
delivery_service.send_to_members(self_remove_proposal)?;
leave_group_via_self_remove() can only be used if the group’s wire format policy allows PublicMessages. It will fail if the policy is AlwaysCiphertext.
Being removed from a group
When you’re removed, you can still process the final commit:
let processed_message = bob_group.process_message(provider, message)?;
if let ProcessedMessageContent::StagedCommitMessage(staged_commit) = processed_message.into_content() {
if staged_commit.self_removed() {
println!("We were removed from the group");
}
// Merge the commit - group will become inactive
bob_group.merge_staged_commit(provider, *staged_commit)?;
// Group is now inactive, can only be used for inspection
assert!(!bob_group.is_active());
}
After merging a commit that removes you, the group becomes inactive. You can still examine the membership list, but you cannot create or process new messages.
Interpreting remove operations
Use the RemoveOperation enum to understand the context of a removal:
use openmls::prelude::*;
let processed_message = alice_group.process_message(provider, message)?;
if let ProcessedMessageContent::StagedCommitMessage(staged_commit) = processed_message.into_content() {
for remove_proposal in staged_commit.remove_proposals() {
let operation = RemoveOperation::new(remove_proposal.clone(), &alice_group)?;
match operation {
RemoveOperation::WeLeft => println!("We left the group"),
RemoveOperation::WeWereRemovedBy(sender) => println!("We were removed by {:?}", sender),
RemoveOperation::TheyLeft(index) => println!("Member {} left", index),
RemoveOperation::TheyWereRemovedBy((index, sender)) => {
println!("Member {} was removed by {:?}", index, sender)
}
RemoveOperation::WeRemovedThem(index) => println!("We removed member {}", index),
}
}
}
Swapping members
Replace members that are out of sync with new members:
use openmls::prelude::*;
let out_of_sync_indices = vec![member1_index, member2_index];
let replacement_key_packages = vec![new_kp1, new_kp2];
let bundle = alice_group.swap_members(
provider,
&alice_signature_keys,
&out_of_sync_indices,
&replacement_key_packages,
)?;
The swap_members() function does not enforce that removed members and new members correspond. It’s up to the application to ensure the correct members are being replaced.
MlsGroup::add_members() - Add members with path update
MlsGroup::add_members_without_update() - Add members without forced path
MlsGroup::remove_members() - Remove members from group
MlsGroup::leave_group() - Leave the group
RemoveOperation - Classifies the type of remove operation
Next steps
Sending messages
Send application messages to group members
Processing messages
Handle incoming proposals and commits