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.
Processing incoming messages from the delivery service is a multi-phase operation in OpenMLS. Messages are deserialized, decrypted, validated, and then inspected before being applied to the group state.
Message processing overview
Processing happens in these phases:
- Deserialize - Convert bytes into
MlsMessageIn
- Process - Decrypt and validate the message
- Inspect - Examine the message content
- Apply - Merge commits or store proposals
Deserializing messages
Incoming messages from the delivery service are deserialized into MlsMessageIn:
use openmls::prelude::*;
let serialized_message: Vec<u8> = delivery_service.receive()?;
let mls_message_in = MlsMessageIn::tls_deserialize(
&mut serialized_message.as_slice()
)?;
If the message is malformed, deserialization will fail with an error.
Processing messages
Use process_message() to decrypt and validate messages:
use openmls::prelude::*;
let protocol_message = match mls_message_in.try_into_protocol_message() {
Ok(msg) => msg,
Err(_) => {
// Not a protocol message (might be a Welcome)
return Ok(());
}
};
let processed_message = alice_group.process_message(
provider,
protocol_message,
)?;
This performs all syntactic and semantic validation checks and verifies the message’s signature. If successful, it returns a ProcessedMessage.
MlsMessageIn can carry all MLS message types, but only PrivateMessageIn and PublicMessageIn are processed in the context of a group. Welcome messages are handled separately during the join process.
Interpreting processed messages
The ProcessedMessage exposes header fields and content:
use openmls::prelude::*;
let group_id = processed_message.group_id();
let epoch = processed_message.epoch();
let sender = processed_message.sender();
let authenticated_data = processed_message.authenticated_data();
let credential = processed_message.credential();
match processed_message.into_content() {
ProcessedMessageContent::ApplicationMessage(app_msg) => {
// Handle application message
}
ProcessedMessageContent::ProposalMessage(proposal) => {
// Handle proposal
}
ProcessedMessageContent::StagedCommitMessage(staged_commit) => {
// Handle commit
}
ProcessedMessageContent::ExternalJoinProposalMessage(proposal) => {
// Handle external join proposal
}
}
Handling application messages
Application messages contain the plaintext payload:
use openmls::prelude::*;
if let ProcessedMessageContent::ApplicationMessage(app_msg) = processed_message.into_content() {
let message_bytes = app_msg.into_bytes();
let message_text = String::from_utf8(message_bytes)?;
println!("Received message: {}", message_text);
// Process the message in your application
app.handle_message(message_text);
}
Handling proposals
Standalone proposals should be stored in the group’s proposal store:
use openmls::prelude::*;
if let ProcessedMessageContent::ProposalMessage(proposal) = processed_message.into_content() {
// Inspect the proposal
match proposal.proposal() {
Proposal::Add(add_proposal) => {
println!("Add proposal received");
// Validate the key package
let key_package = add_proposal.key_package();
validate_key_package(key_package)?;
}
Proposal::Remove(remove_proposal) => {
println!("Remove proposal for index: {}", remove_proposal.removed());
}
Proposal::Update(update_proposal) => {
println!("Update proposal received");
}
_ => {}
}
// Store the proposal
alice_group.store_pending_proposal(provider.storage(), *proposal)?;
}
Rolling back proposals
You can remove proposals from the proposal store if needed:
use openmls::prelude::*;
// Store proposal and get reference
let proposal_ref = alice_group.store_pending_proposal(
provider.storage(),
proposal,
)?;
// Later, if validation fails, remove it
if !is_proposal_valid(&proposal) {
alice_group.remove_pending_proposal(
provider.storage(),
proposal_ref,
)?;
}
Handling commits
Commit messages are returned as StagedCommit objects that must be inspected before merging:
use openmls::prelude::*;
if let ProcessedMessageContent::StagedCommitMessage(staged_commit) = processed_message.into_content() {
// Inspect the commit
for add_proposal in staged_commit.add_proposals() {
println!("Adding member with credential: {:?}",
add_proposal.add_proposal().key_package().credential());
// Validate credentials here
validate_credential(add_proposal.add_proposal().key_package().credential())?;
}
for remove_proposal in staged_commit.remove_proposals() {
println!("Removing member at index: {}",
remove_proposal.remove_proposal().removed());
}
for update_proposal in staged_commit.update_proposals() {
println!("Member updating leaf node");
}
// Check if we were removed
if staged_commit.self_removed() {
println!("We were removed from the group");
}
// Approve and merge the commit
alice_group.merge_staged_commit(provider, *staged_commit)?;
}
Always validate credentials before merging commits. Inspect add and update proposals to ensure all new credentials are trusted. See the credential validation guide for details.
Staged commit inspection
StagedCommit provides methods to inspect different proposal types:
use openmls::prelude::*;
// Access proposals by type
let add_proposals = staged_commit.add_proposals();
let remove_proposals = staged_commit.remove_proposals();
let update_proposals = staged_commit.update_proposals();
let psk_proposals = staged_commit.psk_proposals();
// Check for path updates
if staged_commit.has_path() {
println!("Commit includes a path (key material update)");
}
// Get the committer's index
let committer = staged_commit.committer();
Understanding remove operations
Use RemoveOperation to interpret the semantic meaning of removals:
use openmls::prelude::*;
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 voluntarily");
}
RemoveOperation::WeWereRemovedBy(sender) => {
println!("We were removed by: {:?}", sender);
}
RemoveOperation::TheyLeft(index) => {
println!("Member at index {} left", index);
}
RemoveOperation::TheyWereRemovedBy((index, sender)) => {
println!("Member at index {} was removed by {:?}", index, sender);
}
RemoveOperation::WeRemovedThem(index) => {
println!("We removed member at index {}", index);
}
}
}
Merging commits
After inspection, merge the staged commit to advance the group state:
use openmls::prelude::*;
alice_group.merge_staged_commit(
provider,
staged_commit,
)?;
println!("Now in epoch: {}", alice_group.epoch());
Merging a commit advances the epoch, clears pending commits, and updates the group’s key material. After merging, resumption PSKs are automatically stored for the previous epoch.
Two-phase message processing
For advanced use cases, you can separate unprotection from verification:
use openmls::prelude::*;
// Phase 1: Unprotect (decrypt) the message
let unverified_message = alice_group.unprotect_message(
provider,
protocol_message,
)?;
// Inspect unverified content (use with caution!)
// ...
// Phase 2: Verify and process
let processed_message = alice_group.process_unverified_message(
provider,
unverified_message,
)?;
Data from UnverifiedMessage is not validated. Only use this for logging or debugging. Always call process_unverified_message() to verify before trusting the content.
Error handling
Process message errors indicate validation failures:
use openmls::prelude::*;
let result = alice_group.process_message(provider, protocol_message);
match result {
Ok(processed_message) => {
// Handle message
}
Err(ProcessMessageError::GroupStateError(MlsGroupStateError::UseAfterEviction)) => {
println!("Cannot process: we were removed from the group");
}
Err(ProcessMessageError::IncompatibleWireFormat) => {
println!("Message wire format violates group policy");
}
Err(ProcessMessageError::ValidationError(e)) => {
println!("Message failed validation: {:?}", e);
}
Err(e) => {
println!("Error processing message: {:?}", e);
}
}
Processing external proposals
External proposals from outside parties require special handling:
use openmls::prelude::*;
if let ProcessedMessageContent::ProposalMessage(proposal) = processed_message.into_content() {
if matches!(processed_message.sender(), Sender::External(_)) {
// Validate external sender authorization
let external_senders = alice_group
.group()
.group_context()
.extensions()
.external_senders()?;
// Check if sender is authorized
let is_authorized = external_senders
.iter()
.any(|es| es.credential() == processed_message.credential());
if is_authorized {
alice_group.store_pending_proposal(provider.storage(), *proposal)?;
} else {
println!("Unauthorized external sender");
}
}
}
MlsGroup::process_message() - Main processing function
ProcessedMessage - Validated message with metadata
ProcessedMessageContent - Enum of message content types
StagedCommit - Commit ready for inspection and merging
QueuedProposal - Proposal stored in proposal store
Next steps
Credential validation
Learn how to validate member credentials
Key updates
Update your encryption keys for forward secrecy