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.
Commit messages apply a set of proposals atomically to transition the group to a new epoch.
Overview
A Commit is the mechanism by which group state changes. It references or includes proposals and advances the group’s epoch.
Commit Structure
// RFC 9420
struct {
ProposalOrRef proposals<V>;
optional<UpdatePath> path;
} Commit;
proposals
Vec<ProposalOrRef>
required
Collection of proposals to apply (by value or reference)
Update path to refresh encryption keys (required in some cases)
Commit Fields
Proposals
The proposals included in a commit can be specified by value or by reference.
pub enum ProposalOrRef {
Proposal(Box<Proposal>),
Reference(Box<ProposalRef>),
}
By Value: Full proposal is included in the commit
- Used for proposals created in the same commit
- Larger message size
By Reference: Only hash reference is included
- References previously sent proposal messages
- Smaller message size
- Proposal must be in the proposal queue
Update Path
The update path refreshes the encryption keys along the sender’s direct path in the ratchet tree.
struct {
LeafNode leaf_node;
HPKECiphertext encrypted_path_secret<V>;
} UpdatePath;
The committer’s new leaf node
encrypted_path_secret
Vec<HpkeCiphertext>
required
Path secrets encrypted to each node’s resolution
Creating Commits
Commit Builder
use openmls::prelude::*;
// Commit pending proposals from the queue
let (commit, welcome, group_info) = group.commit_to_pending_proposals(
provider,
&signer
)?;
// commit: MlsMessageOut containing the commit
// welcome: Option<MlsMessageOut> if members were added
// group_info: Option<GroupInfo> for new members
Self-Update Commit
// Create commit with inline self-update
let (commit, welcome, group_info) = group.self_update(
provider,
&signer,
)?;
External Commit
Join a group using an external commit.
use openmls::prelude::*;
// Join via external commit
let (group, commit, group_info) = StagedWelcome::new_from_external_commit(
provider,
&join_config,
verifiable_group_info,
&credential_with_key,
&signer,
)?;
Path Requirements
Certain commits MUST include an update path:
Commit contains at least one Update proposal
Commit contains at least one Remove proposal
Commit is an external commit (joining the group)
Commit updates group context extensions
Commit contains a SelfRemove proposal
// Check if path is required
let path_required = commit.proposals.iter()
.any(|p| p.proposal_type().is_path_required());
if path_required && commit.path.is_none() {
// Invalid commit: path required but missing
}
Confirmation Tag
Commits include a confirmation tag to authenticate the new group state.
pub struct ConfirmationTag(pub(crate) Mac);
The confirmation tag is a MAC over the confirmed transcript hash:
confirmation_tag = MAC(confirmation_key, confirmed_transcript_hash)
Derived from the new epoch’s key schedule
confirmed_transcript_hash
Hash of interim transcript + commit content + signature
Verification
Receivers verify the confirmation tag to ensure:
- The commit is authentic
- All members have the same view of group state
- The committer has the correct key schedule
Processing Commits
Validation
When processing a commit, OpenMLS validates:
- Proposal validation: All proposals are well-formed and authorized
- Path validation: Update path is present when required and valid
- Signature validation: Commit signature is valid
- Confirmation tag: MAC verifies correctly
- Semantic validation: Proposals are compatible and applicable
let processed = group.process_message(provider, protocol_message)?;
match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(staged_commit) => {
// Inspect before merging
for proposal in staged_commit.queued_proposals() {
// Review proposals
}
// Merge the commit
group.merge_staged_commit(provider, *staged_commit)?;
}
_ => {}
}
Staged Commit
Commits are first staged for inspection before merging.
pub struct StagedCommit {
// Validated commit ready to merge
}
Returns the proposals included in this commit
add_proposals()
impl Iterator<Item = QueuedProposal>
Returns an iterator over Add proposals
remove_proposals()
impl Iterator<Item = QueuedProposal>
Returns an iterator over Remove proposals
update_proposals()
impl Iterator<Item = QueuedProposal>
Returns an iterator over Update proposals
Epoch Transition
When a commit is merged, the group transitions to a new epoch.
State Changes
- Epoch incremented:
epoch = old_epoch + 1
- Proposals applied: All proposals in commit are applied to tree
- Key schedule advanced: New secrets derived for the epoch
- Transcript updated: Commit added to transcript hash
// Before merge
let old_epoch = group.epoch();
// Merge commit
group.merge_staged_commit(provider, staged_commit)?;
// After merge
let new_epoch = group.epoch();
assert_eq!(new_epoch, old_epoch + 1);
Welcome Messages
When a commit adds new members, a Welcome message is generated.
let (commit, welcome, _group_info) = group.add_members(
provider,
&signer,
&[key_package]
)?;
if let Some(welcome) = welcome {
// Send welcome to new members
send_to_new_members(welcome);
}
See Welcome Messages for details.
Commit Types
Member Commit
Standard commit from an existing group member.
let sender = Sender::Member(leaf_index);
External Commit
Commit from a new member joining via external commit.
let sender = Sender::NewMemberCommit;
Requirements:
- Must include ExternalInit proposal
- Must include update path
- Uses external public key from GroupInfo
Transcript Hashes
Commits update two transcript hashes:
Confirmed Transcript Hash
Hash including the commit and signature:
confirmed_transcript_hash = Hash(
interim_transcript_hash ||
WireFormat || FramedContent || signature
)
Interim Transcript Hash
Hash including the confirmation tag:
interim_transcript_hash = Hash(
confirmed_transcript_hash ||
confirmation_tag
)
These ensure all members agree on group history.
Empty Commits
Commits with no proposals are allowed:
// Create empty commit (advances epoch only)
let (commit, welcome, group_info) = group.commit_to_pending_proposals(
provider,
&signer
)?;
// If no proposals queued, this is an empty commit
Use cases:
- Force epoch advancement
- Refresh keys periodically
- Recover from key compromise
Error Handling
Commit Creation Errors
pub enum CreateCommitError {
LibraryError(LibraryError),
OwnKeyNotFound,
MissingOwnLeaf,
// ...
}
Commit Processing Errors
pub enum ValidationError {
InvalidProposal,
InvalidPath,
InvalidSignature,
InvalidConfirmationTag,
ProposalNotFound,
UnauthorizedProposal,
// ...
}
Examples
Simple Commit
use openmls::prelude::*;
// Propose changes
group.propose_add_member(provider, &signer, &key_package)?;
group.propose_remove_member(provider, &signer, target_index)?;
// Commit all pending proposals
let (commit, welcome, group_info) = group.commit_to_pending_proposals(
provider,
&signer
)?;
// Send commit to group
send_to_group(commit);
// Send welcome to new members if present
if let Some(welcome) = welcome {
send_to_new_members(welcome);
}
// Merge own commit
group.merge_pending_commit(provider)?;
Process Received Commit
// Receive commit message
let message_in = MlsMessageIn::tls_deserialize(&mut bytes.as_slice())?;
let protocol_message = message_in.try_into_protocol_message()?;
// Process
let processed = group.process_message(provider, protocol_message)?;
match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(staged_commit) => {
// Inspect proposals
println!("Commit contains {} proposals",
staged_commit.queued_proposals().len());
// Merge to apply changes
group.merge_staged_commit(provider, *staged_commit)?;
println!("New epoch: {}", group.epoch());
}
_ => {}
}
External Commit
// Fetch group info from existing member
let verifiable_group_info = fetch_group_info();
// Create external commit to join
let (mut group, commit, _ratchet_tree) =
StagedWelcome::new_from_external_commit(
provider,
&join_config,
verifiable_group_info,
&credential_with_key,
&signer,
)?;
// Send commit to group
send_to_group(commit);
// Now a member of the group
println!("Joined group: {:?}", group.group_id());