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.
Message framing provides a unified wrapper for all MLS message types with serialization and wire format handling.
Overview
OpenMLS uses two framing types:
- MlsMessageOut: Messages being sent (serialized to wire)
- MlsMessageIn: Messages being received (deserialized from wire)
This separation ensures type safety and proper validation flow.
MLSMessage Structure
// RFC 9420
struct {
ProtocolVersion version = mls10;
WireFormat wire_format;
select (MLSMessage.wire_format) {
case mls_plaintext:
PublicMessage plaintext;
case mls_ciphertext:
PrivateMessage ciphertext;
case mls_welcome:
Welcome welcome;
case mls_group_info:
GroupInfo group_info;
case mls_key_package:
KeyPackage key_package;
}
} MLSMessage;
MLS protocol version (currently mls10)
Discriminator for message type
Message content (type determined by wire_format)
The wire format enum identifies the message type.
pub enum WireFormat {
PublicMessage = 1,
PrivateMessage = 2,
Welcome = 3,
GroupInfo = 4,
KeyPackage = 5,
}
Unencrypted message (proposals, commits from external senders)
Encrypted message (application data, member proposals/commits)
Welcome message for new members
Key package for inviting users
MlsMessageOut
Messages created by OpenMLS for sending.
pub struct MlsMessageOut {
pub(crate) version: ProtocolVersion,
pub(crate) body: MlsMessageBodyOut,
}
MlsMessageBodyOut
pub enum MlsMessageBodyOut {
PublicMessage(PublicMessage),
PrivateMessage(PrivateMessage),
Welcome(Welcome),
GroupInfo(GroupInfo),
KeyPackage(KeyPackage),
}
Creating Messages
use openmls::prelude::*;
// From group operations
let message = group.create_message(provider, &signer, b"data")?;
// message is MlsMessageOut containing PrivateMessage
let (commit, welcome, _) = group.commit_to_pending_proposals(
provider,
&signer
)?;
// commit is MlsMessageOut containing PublicMessage or PrivateMessage
// welcome is Option<MlsMessageOut> containing Welcome
Serialization
// Serialize to bytes
let bytes = message.tls_serialize_detached()?;
// Or use convenience method
let bytes = message.to_bytes()?;
// Send over network
send_to_server(bytes);
Conversion
Create MlsMessageOut from specific message types:
// From PrivateMessage
let msg_out = MlsMessageOut::from(private_message);
// From PublicMessage
let msg_out = MlsMessageOut::from(public_message);
// From Welcome with version
let msg_out = MlsMessageOut::from_welcome(
welcome,
ProtocolVersion::default()
);
MlsMessageIn
Messages received and deserialized from the wire.
pub struct MlsMessageIn {
pub(crate) version: ProtocolVersion,
pub(crate) body: MlsMessageBodyIn,
}
MlsMessageBodyIn
pub enum MlsMessageBodyIn {
PublicMessage(PublicMessageIn),
PrivateMessage(PrivateMessageIn),
Welcome(Welcome),
GroupInfo(VerifiableGroupInfo),
KeyPackage(KeyPackageIn),
}
Input types (PublicMessageIn, PrivateMessageIn, etc.) require validation before use.
Deserialization
use openmls::prelude::*;
// Deserialize from bytes
let message_in = MlsMessageIn::tls_deserialize(
&mut bytes.as_slice()
)?;
// Check wire format
match message_in.wire_format() {
WireFormat::PrivateMessage => { /* encrypted */ }
WireFormat::Welcome => { /* welcome */ }
_ => {}
}
// Extract body
let body = message_in.extract();
match body {
MlsMessageBodyIn::PrivateMessage(pm) => {
// Process encrypted message
}
MlsMessageBodyIn::Welcome(welcome) => {
// Process welcome
}
MlsMessageBodyIn::GroupInfo(group_info) => {
// Process group info
}
_ => {}
}
ProtocolMessage
Convenience type for processing protocol messages (Public/Private).
pub enum ProtocolMessage {
PrivateMessage(PrivateMessageIn),
PublicMessage(Box<PublicMessageIn>),
}
Returns the wire format of this message
Returns the group ID this message belongs to
Returns the epoch this message was sent in
Returns the content type (Application/Proposal/Commit)
Returns true for Proposal and Commit messages
Returns true for external proposals and commits
Converting to ProtocolMessage
// From MlsMessageIn
let protocol_message = message_in.try_into_protocol_message()?;
// Process in group
let processed = group.process_message(provider, protocol_message)?;
PublicMessage
Unencrypted framing for proposals and commits.
// RFC 9420
struct {
FramedContent content;
FramedContentAuthData auth;
optional<MAC> membership_tag;
} PublicMessage;
The message content (group_id, epoch, sender, body)
Authentication data (signature, optional confirmation_tag)
Membership tag for member authentication
When Used
- External proposals (NewMemberProposal sender)
- External commits (NewMemberCommit sender)
- Proposals/commits when configured for public messaging
Authentication
PublicMessages include:
- Signature: All PublicMessages are signed
- Membership Tag: Members include MAC for authentication
- Confirmation Tag: Commits include confirmation tag
// Signature verification
public_message.verify_signature(crypto, ciphersuite, &public_key)?;
// Membership tag verification
public_message.verify_membership_tag(
crypto,
ciphersuite,
&membership_key,
&serialized_context
)?;
PrivateMessage
Encrypted framing for confidential messages.
// RFC 9420
struct {
opaque group_id<V>;
uint64 epoch;
ContentType content_type;
opaque authenticated_data<V>;
opaque encrypted_sender_data<V>;
opaque ciphertext<V>;
} PrivateMessage;
Group identifier (plaintext for routing)
Epoch number (plaintext for key selection)
Type of content (Application/Proposal/Commit)
Additional authenticated data
Encrypted sender information (leaf_index, generation)
Encrypted message content
Decryption Flow
- Decrypt sender data to identify sender
- Get decryption key from secret tree (sender, generation)
- Decrypt ciphertext to recover content
- Verify signature in decrypted auth data
// Process private message
let decrypted = group.process_message(
provider,
ProtocolMessage::PrivateMessage(private_message)
)?;
ContentType
Identifies the type of content in a message.
pub enum ContentType {
Application = 1,
Proposal = 2,
Commit = 3,
}
Methods
impl ContentType {
pub fn is_handshake_message(&self) -> bool {
self == &ContentType::Proposal || self == &ContentType::Commit
}
}
FramedContent
Core content structure used in both Public and Private messages.
struct {
opaque group_id<V>;
uint64 epoch;
Sender sender;
opaque authenticated_data<V>;
ContentType content_type;
select (FramedContent.content_type) {
case application:
opaque application_data<V>;
case proposal:
Proposal proposal;
case commit:
Commit commit;
}
} FramedContent;
Additional authenticated data
body
FramedContentBody
required
Message content (Application/Proposal/Commit)
Sender
Identifies who sent the message.
pub enum Sender {
Member(LeafNodeIndex),
External(u32),
NewMemberProposal,
NewMemberCommit,
}
Message from existing group member
Message from external sender (signature key index)
External proposal from prospective member
External commit from joining member
Examples
Send Application Message
use openmls::prelude::*;
// Create message
let message_out = group.create_message(
provider,
&signer,
b"Hello, world!"
)?;
// Serialize and send
let bytes = message_out.tls_serialize_detached()?;
send_to_delivery_service(bytes);
Receive and Process
// Receive bytes
let bytes = receive_from_delivery_service();
// Deserialize
let message_in = MlsMessageIn::tls_deserialize(
&mut bytes.as_slice()
)?;
// Convert to protocol message
let protocol_message = message_in.try_into_protocol_message()?;
// Process
let processed = group.process_message(provider, protocol_message)?;
match processed.into_content() {
ProcessedMessageContent::ApplicationMessage(app_msg) => {
println!("Application: {:?}", app_msg.into_bytes());
}
ProcessedMessageContent::ProposalMessage(proposal) => {
println!("Proposal: {:?}", proposal);
}
ProcessedMessageContent::StagedCommitMessage(commit) => {
group.merge_staged_commit(provider, *commit)?;
println!("Processed commit");
}
_ => {}
}
Handle Welcome
// Receive welcome
let bytes = receive_welcome();
let message_in = MlsMessageIn::tls_deserialize(&mut bytes.as_slice())?;
if let MlsMessageBodyIn::Welcome(welcome) = message_in.extract() {
// Join group
let group = StagedWelcome::new_from_welcome(
provider,
&config,
welcome,
None,
)?.into_group(provider)?;
println!("Joined group: {:?}", group.group_id());
}
let message_in = MlsMessageIn::tls_deserialize(&mut bytes.as_slice())?;
match message_in.wire_format() {
WireFormat::PrivateMessage | WireFormat::PublicMessage => {
// Protocol message - process in group
let protocol_msg = message_in.try_into_protocol_message()?;
group.process_message(provider, protocol_msg)?;
}
WireFormat::Welcome => {
// Welcome - join new group
let welcome = message_in.into_welcome().unwrap();
handle_welcome(welcome)?;
}
WireFormat::GroupInfo => {
// Group info - for external commits
let group_info = message_in.into_verifiable_group_info().unwrap();
handle_group_info(group_info)?;
}
WireFormat::KeyPackage => {
// Key package - store for inviting user
let key_package = message_in.into_keypackage().unwrap();
store_key_package(key_package)?;
}
}