Adding members to a group

Immediate operation

Members can be added to the group atomically with the .add_members() function. The application needs to fetch the corresponding key packages from every new member from the Delivery Service first.

    let (mls_message_out, welcome, group_info) = alice_group
        .add_members(
            provider,
            &alice_signature_keys,
            &[bob_key_package.key_package().clone()],
        )
        .expect("Could not add members.");

The function returns the tuple (MlsMessageOut, Welcome). The MlsMessageOut contains a Commit message that needs to be fanned out to existing group members. The Welcome message must be sent to the newly added members.

Adding members without update

The .add_members_without_update() function functions the same as the .add_members() function, except that it will only include an update to the sender's key material if the sender's proposal store includes a proposal that requires a path. For a list of proposals and an indication whether they require a path (i.e. a key material update) see Section 17.4 of RFC 9420.

Not sending an update means that the sender will not achieve post-compromise security with this particular commit. However, not sending an update saves on performance both in terms of computation and bandwidth. Using .add_members_without_update() can thus be a useful option if the ciphersuite of the group features large public keys and/or expensive encryption operations.

Proposal

Members can also be added as a proposal (without the corresponding Commit message) by using the .propose_add_member() function:

    let (mls_message_out, _proposal_ref) = alice_group
        .propose_add_member(
            provider,
            &alice_signature_keys,
            bob_key_package.key_package(),
        )
        .expect("Could not create proposal to add Bob");

In this case, the function returns an MlsMessageOut that needs to be fanned out to existing group members.

External proposal

Parties outside the group can also make proposals to add themselves to the group with an external proposal. Since those proposals are crafted by outsiders, they are always plaintext messages.

    let proposal =
        JoinProposal::new::<<Provider as openmls_traits::OpenMlsProvider>::StorageProvider>(
            bob_key_package.key_package().clone(),
            alice_group.group_id().clone(),
            alice_group.epoch(),
            &bob_signature_keys,
        )
        .expect("Could not create external Add proposal");

It is then up to the group members to validate the proposal and commit it. Note that in this scenario it is up to the application to define a proper authorization policy to grant the sender.

    let alice_processed_message = alice_group
        .process_message(
            provider,
            proposal
                .into_protocol_message()
                .expect("Unexpected message type."),
        )
        .expect("Could not process message.");
    match alice_processed_message.into_content() {
        ProcessedMessageContent::ExternalJoinProposalMessage(proposal) => {
            alice_group
                .store_pending_proposal(provider.storage(), *proposal)
                .unwrap();
            let (_commit, welcome, _group_info) = alice_group
                .commit_to_pending_proposals(provider, &alice_signature_keys)
                .expect("Could not commit");
            assert_eq!(alice_group.members().count(), 1);
            alice_group
                .merge_pending_commit(provider)
                .expect("Could not merge commit");
            assert_eq!(alice_group.members().count(), 2);

            let welcome: MlsMessageIn = welcome.expect("Welcome was not returned").into();
            let welcome = welcome
                .into_welcome()
                .expect("expected the message to be a welcome message");

            let bob_group = StagedWelcome::new_from_welcome(
                provider,
                mls_group_create_config.join_config(),
                welcome,
                None,
            )
            .expect("Bob could not stage the the group join")
            .into_group(provider)
            .expect("Bob could not join the group");
            assert_eq!(bob_group.members().count(), 2);
        }
        _ => unreachable!(),
    }