Skip to content

Group Management and Federation

fn create_group(
founder: &DeviceIdentity,
initial_members: &[LocalContact],
provider: &impl OpenMlsProvider,
) -> Result<MultiUserGroup> {
// 1. Generate random group ID
let group_id = generate_random_bytes(32);
// 2. Create MLS group with founder's credential and RatchetTreeExtension
let config = MlsGroupCreateConfig::builder()
.with_ratchet_tree_extension(true) // Include tree in Welcome
.build();
let mut mls_group = MlsGroup::new(
provider,
&founder.keypair,
&config,
founder.create_mls_credential(),
)?;
// 3. Fetch KeyPackages and add initial members to MLS group
let mut key_packages = Vec::new();
for contact in initial_members {
// Fetch fresh KeyPackage from contact's server
let key_package = fetch_keypackage(
&contact.keypackage_server,
&contact.device_id,
)?;
key_packages.push(key_package);
}
// Add all members in one operation (OpenMLS API)
let (commit, welcome, _) = mls_group.add_members(
provider,
&founder.keypair,
&key_packages,
)?;
// Commit the changes
mls_group.merge_pending_commit(provider)?;
// 4. Send welcome messages to all added members
for contact in initial_members {
// Welcome includes ratchet tree (RatchetTreeExtension)
send_federated_message(
welcome.clone(),
&contact.delivery_addresses[0], // Use first delivery address
)?;
}
// 5. Send commit to existing members (none in this case, group just created)
Ok(MultiUserGroup {
group_id,
founder_device: founder.device_id,
members: build_member_list_from_mls(&mls_group),
mls_state: mls_group,
admin_devices: vec![founder.device_id],
created_at: current_unix_timestamp(),
})
}

Members can add people they have as contacts. When adding a member, their KeyPackage must be fetched from their designated server.

KeyPackage Requirement:

Each contact stores a keypackage_server field (from their ShareableIdentityBundle) that specifies where to fetch KeyPackages. The adding member:

  1. Fetches a fresh KeyPackage from the contact’s server
  2. Uses it to add the contact to the MLS group
  3. Sends the Welcome message to the contact’s delivery address

See Contact Exchange and Trust for details on KeyPackage management.

fn add_member_to_group(
group: &mut MultiUserGroup,
new_member_contact: &LocalContact,
adding_member: &DeviceIdentity,
provider: &impl OpenMlsProvider,
) -> Result<()> {
// 1. Check permissions
if !group.admin_devices.contains(&adding_member.device_id) {
return Err("Only admins can add members");
}
// 2. Fetch fresh KeyPackage from new member's server
let key_package = fetch_keypackage(
&new_member_contact.keypackage_server,
&new_member_contact.device_id,
)?;
// 3. Add member to MLS group (creates commit + welcome)
let (commit, welcome, _) = group.mls_state.add_members(
provider,
&adding_member.keypair,
&[key_package],
)?;
// 4. Merge pending commit to update local group state
group.mls_state.merge_pending_commit(provider)?;
// 5. Serialize commit for distribution
let commit_message = commit.tls_serialize_detached()?;
// 6. Send commit to all current members (updates their group state)
for member in &group.members {
let secure_message = SecureMessage {
message_id: generate_random_bytes(32),
group_id: group.group_id,
mls_ciphertext: commit_message.clone(),
sender_signature: adding_member.sign(&commit_message),
timestamp: current_unix_timestamp(),
message_type: MessageType::SystemOperation,
};
send_federated_message(
secure_message,
&member.delivery_addresses[0], // Use first delivery address
)?;
}
// 7. Serialize and send Welcome to new member
let welcome_message = welcome.tls_serialize_detached()?;
send_federated_message(
welcome_message,
&new_member_contact.delivery_addresses[0],
)?;
// 8. Update local group state
group.members.push(GroupMember {
device_id: new_member_contact.device_id,
public_key: new_member_contact.public_key,
delivery_addresses: new_member_contact.delivery_addresses.clone(),
added_by: adding_member.device_id,
added_at: current_unix_timestamp(),
participation_status: ParticipationStatus::PendingWelcome,
});
Ok(())
}

When a group member rotates their delivery addresses, they notify the group via a system message.

fn rotate_address_in_group(
group: &mut MultiUserGroup,
device: &DeviceIdentity,
new_address: String,
provider: &impl OpenMlsProvider,
) -> Result<()> {
// 1. Create system message announcing address change
let address_rotation = SystemOperation::AddressRotation {
device_id: device.device_id,
new_delivery_address: new_address.clone(),
timestamp: current_unix_timestamp(),
};
// 2. Serialize and sign the message
let message_bytes = serialize(&address_rotation)?;
let signature = device.sign(&message_bytes);
// 3. Encrypt with MLS
let encrypted = group.mls_state.encrypt_application_message(
provider,
&device.keypair,
&message_bytes,
)?;
// 4. Send to all group members
for member in &group.members {
let secure_message = SecureMessage {
message_id: generate_random_bytes(32),
group_id: group.group_id,
mls_ciphertext: encrypted.clone(),
sender_signature: signature.clone(),
timestamp: current_unix_timestamp(),
message_type: MessageType::SystemOperation,
};
send_federated_message(
secure_message,
&member.delivery_addresses,
)?;
}
// 5. Update local group state
if let Some(member) = group.members.iter_mut()
.find(|m| m.device_id == device.device_id) {
member.delivery_addresses.push(new_address);
}
Ok(())
}

When members receive an address rotation message:

fn handle_address_rotation(
group: &mut MultiUserGroup,
rotation: &SystemOperation::AddressRotation,
) -> Result<()> {
// 1. Find the member
let member = group.members.iter_mut()
.find(|m| m.device_id == rotation.device_id)
.ok_or("Unknown member")?;
// 2. Add new address to their list
if !member.delivery_addresses.contains(&rotation.new_delivery_address) {
member.delivery_addresses.push(rotation.new_delivery_address.clone());
}
// 3. Optional: Remove old addresses after grace period
// (Keep old addresses for a few days to handle in-flight messages)
Ok(())
}

Address rotation properties:

  • Members maintain multiple addresses during transition
  • Old addresses remain valid for a grace period (e.g., 7 days)
  • Group members automatically learn new addresses
  • No manual contact updates needed
GET /.well-known/cryptid
Host: server.example.com
{
"protocol_version": "1.0",
"server_name": "server.example.com",
"federation_enabled": true,
"supported_features": [
"mls_messaging"
],
"rate_limits": {
"federated_messages_per_minute": 1000,
"max_message_size": 10485760
},
"certificate_fingerprint": "sha256:a1b2c3d4..."
}
PUT /federation/v1/deliver/{transaction_id}
Content-Type: application/json
Authorization: Bearer <federation_token>
{
"origin_server": "alice.server1.com",
"transaction_id": "unique_transaction_id",
"messages": [
{
"message_id": "message_id_32_bytes",
"recipient_addresses": ["bob@server2.com"],
"group_id": "group_id_32_bytes",
"mls_ciphertext": "base64_of_encrypted_content",
"sender_signature": "ed25519_signature",
"timestamp": 1758999498
}
]
}

The recipient_addresses field contains delivery addresses (not device_ids). If a device has multiple delivery addresses on different servers, the message may be sent to any active address. The receiving server routes to the device regardless of which address was used.

Example:

{
"origin_server": "server1.com",
"messages": [
{
"recipient_addresses": [
"bob-work@server2.com", // Bob's work address
"bob-personal@server2.com" // Bob's personal address (same device)
],
// Server2 delivers to Bob's device via either address
}
]
}