Processing incoming messages

Processing incoming messages happens in different phases:

Deserializing messages

Incoming messages can be deserialized from byte slices into an MlsMessageIn:

    let mls_message_in =
        MlsMessageIn::try_from_bytes(&bytes).expect("Could not deserialize message.");

If the message is malformed, the function will fail with an error.

Parsing messages

In the next step, the incoming message needs to be parsed. If the message was encrypted, it will be decrypted automatically:

    let unverified_message = bob_group
        .parse_message(mls_message_in, backend)
        .expect("Could not parse message.");

Parsing can fail, if e.g. dercrypting the message fails. The exact reason for failure is returned in the error.

Processing messages

In the next step the unverified message needs to be processed. This step performs all remaining validity checks and also verifies the message's signature. Optionally, a signature key can be provided to verify the message's signature. This can be used when processing external messages. By default, the sender's credential is used to verify the signature.

    let processed_message = bob_group
        .process_unverified_message(
            unverified_message,
            None, // No external signature key
            backend,
        )
        .expect("Could not process unverified message.");

Interpreting the processed message

In the last step, the message is ready for inspection. There are 3 different kinds of messages:

Application messages

Application messages simply return the original byte slice:

    if let ProcessedMessage::ApplicationMessage(application_message) = processed_message {
        // Check the message
        assert_eq!(application_message.into_bytes(), b"Hi, I'm Alice!");
    }

Proposals

Standalone proposals are returned as a QueuedProposal, indicating the fact that they are pending proposals. The actual proposal can be inspected through the .proposal() function. After inspection, applications should store the pending proposal in the proposal store of the group:

    if let ProcessedMessage::ProposalMessage(staged_proposal) = charlie_processed_message {
        // In the case we received an Add Proposal
        if let Proposal::Add(add_proposal) = staged_proposal.proposal() {
            // Check that Bob was added
            assert_eq!(add_proposal.key_package().credential(), &bob_credential);
        } else {
            panic!("Expected an AddProposal.");
        }

        // Check that Alice added Bob
        assert!(matches!(
            staged_proposal.sender(),
            Sender::Member(member) if member == alice_group
            .key_package_ref()
            .expect("An unexpected error occurred.")
        ));
        // Store proposal
        charlie_group.store_pending_proposal(*staged_proposal);
    }

Commit messages

Commit messages are returned as StagedCommit objects. The proposals they cover can be inspected through different functions, depending on the proposal type. For more details see the StagedCommit documentation. After the application has inspected the StagedCommit and approved all the proposals it covers, the StagedCommit can be merged in the current group state by calling the .merge_staged_commit() function.

    if let ProcessedMessage::StagedCommitMessage(staged_commit) = alice_processed_message {
        // We expect a remove proposal
        let remove = staged_commit
            .remove_proposals()
            .next()
            .expect("Expected a proposal.");
        // Check that Bob was removed
        assert_eq!(
            remove.remove_proposal().removed(),
            bob_group
                .key_package_ref()
                .expect("An unexpected error occurred.")
        );
        // Check that Charlie removed Bob
        assert!(matches!(
            remove.sender(),
            Sender::Member(member) if member == &charlies_old_kpr
        ));
        // Merge staged commit
        alice_group
            .merge_staged_commit(*staged_commit)
            .expect("Could not merge Commit.");
    }

Interpreting remove operations

Remove operations can have different meanings, such as:

  • We left the group (by our own wish)
  • We were removed from the group (by another member or a pre-configured sender)
  • We removed another member from the group
  • Another member left the group (by its own wish)
  • Another member was removed from the group (by a member or a pre-configured sender, but not by us)

Since all remove operations only appear as a QueuedRemoveProposal, the RemoveOperation enum can be constructed from the remove proposal and the current group state to refelect the different scenarios listed above.

    if let ProcessedMessage::StagedCommitMessage(staged_commit) = bob_processed_message {
        let remove_proposal = staged_commit
            .remove_proposals()
            .next()
            .expect("An unexpected error occurred.");

        // We construct a RemoveOperation enum to help us interpret the remove operation
        let remove_operation = RemoveOperation::new(remove_proposal, &bob_group)
            .expect("An unexpected Error occurred.");

        match remove_operation {
            RemoveOperation::WeLeft => unreachable!(),
            // We expect this variant, since Bob was removed by Charlie
            RemoveOperation::WeWereRemovedBy(member) => {
                assert!(matches!(member, Sender::Member(member) if member == charlies_old_kpr));
            }
            RemoveOperation::TheyLeft(_) => unreachable!(),
            RemoveOperation::TheyWereRemovedBy(_) => unreachable!(),
            RemoveOperation::WeRemovedThem(_) => unreachable!(),
        }

        // Merge staged Commit
        bob_group
            .merge_staged_commit(*staged_commit)
            .expect("Could not merge Commit.");
    } else {
        unreachable!("Expected a StagedCommit.");
    }