Skip to main content

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;
version
ProtocolVersion
MLS protocol version (currently mls10)
wire_format
WireFormat
Discriminator for message type
body
MlsMessageBody
Message content (type determined by wire_format)

WireFormat

The wire format enum identifies the message type.
pub enum WireFormat {
    PublicMessage = 1,
    PrivateMessage = 2,
    Welcome = 3,
    GroupInfo = 4,
    KeyPackage = 5,
}
PublicMessage
0x0001
Unencrypted message (proposals, commits from external senders)
PrivateMessage
0x0002
Encrypted message (application data, member proposals/commits)
Welcome
0x0003
Welcome message for new members
GroupInfo
0x0004
Group state information
KeyPackage
0x0005
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 */ }
    _ => {}
}

Extracting Content

// 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>),
}
wire_format()
WireFormat
Returns the wire format of this message
group_id()
&GroupId
Returns the group ID this message belongs to
epoch()
GroupEpoch
Returns the epoch this message was sent in
content_type()
ContentType
Returns the content type (Application/Proposal/Commit)
is_handshake_message()
bool
Returns true for Proposal and Commit messages
is_external()
bool
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;
content
FramedContent
The message content (group_id, epoch, sender, body)
auth
FramedContentAuthData
Authentication data (signature, optional confirmation_tag)
membership_tag
Option<Mac>
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:
  1. Signature: All PublicMessages are signed
  2. Membership Tag: Members include MAC for authentication
  3. 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_id
GroupId
Group identifier (plaintext for routing)
epoch
uint64
Epoch number (plaintext for key selection)
content_type
ContentType
Type of content (Application/Proposal/Commit)
authenticated_data
opaque<V>
Additional authenticated data
encrypted_sender_data
opaque<V>
Encrypted sender information (leaf_index, generation)
ciphertext
opaque<V>
Encrypted message content

Decryption Flow

  1. Decrypt sender data to identify sender
  2. Get decryption key from secret tree (sender, generation)
  3. Decrypt ciphertext to recover content
  4. 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,
}
Application
0x01
Application data message
Proposal
0x02
Proposal message
Commit
0x03
Commit message

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;
group_id
GroupId
required
Group identifier
epoch
GroupEpoch
required
Epoch number
sender
Sender
required
Message sender
authenticated_data
VLBytes
required
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,
}
Member
LeafNodeIndex
Message from existing group member
External
u32
Message from external sender (signature key index)
NewMemberProposal
External proposal from prospective member
NewMemberCommit
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());
}

Routing by Wire Format

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)?;
    }
}