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.
Key updates (also called self-updates) allow group members to refresh their encryption keys and achieve post-compromise security. OpenMLS provides both immediate commit operations and proposal-based workflows.
Why update keys?
Regular key updates provide important security benefits:
- Forward secrecy - Previous messages remain secure even if current keys are compromised
- Post-compromise security - Recover security after a potential compromise
- Key rotation - Meet compliance requirements for regular key rotation
- Credential updates - Update your credential, capabilities, or extensions
The self_update() function updates your leaf node and immediately commits:
use openmls::prelude::*;
let bundle = alice_group.self_update(
provider,
&alice_signature_keys,
LeafNodeParameters::default(),
)?;
let (commit, welcome, group_info) = bundle.into_contents();
// Send commit to existing members
delivery_service.send_to_members(commit)?;
// If there were pending add proposals, send welcome
if let Some(welcome_msg) = welcome {
delivery_service.send_to_new_members(welcome_msg)?;
}
By default, self_update() only updates the HPKE encryption key. You can also update credentials, capabilities, and extensions using LeafNodeParameters.
Customizing leaf node parameters
Update additional leaf node properties:
use openmls::prelude::*;
let leaf_node_parameters = LeafNodeParameters::builder()
.with_credential(new_credential_with_key)
.with_capabilities(new_capabilities)
.with_extensions(new_extensions)
.build();
let bundle = alice_group.self_update(
provider,
&alice_signature_keys,
leaf_node_parameters,
)?;
Updating the signature key
To rotate your signature key, use self_update_with_new_signer():
use openmls::prelude::*;
// Generate new signature keys
let new_signature_keys = SignatureKeyPair::new(ciphersuite.signature_algorithm())?;
new_signature_keys.store(provider.storage())?;
// Create credential with new public key
let new_credential_with_key = CredentialWithKey {
credential: alice_credential,
signature_key: new_signature_keys.to_public_vec().into(),
};
// Create the signer bundle
let new_signer_bundle = NewSignerBundle::new(
&new_signature_keys,
new_credential_with_key,
);
// Update with new signer
let bundle = alice_group.self_update_with_new_signer(
provider,
&alice_signature_keys, // Old signer signs the commit
new_signer_bundle, // New signer for future messages
LeafNodeParameters::default(),
)?;
When using self_update_with_new_signer(), the Signer in the NewSignerBundle must match the public key and credential in the CredentialWithKey. The LeafNodeParameters must NOT contain a CredentialWithKey as it’s provided in the signer bundle.
Proposal-based updates
Create an update proposal without immediately committing:
use openmls::prelude::*;
let (proposal_message, proposal_ref) = alice_group.propose_self_update(
provider,
&alice_signature_keys,
LeafNodeParameters::default(),
)?;
// Send proposal to group members
delivery_service.send_to_members(proposal_message)?;
// Later, commit to the proposal
let (commit, welcome, group_info) = alice_group.commit_to_pending_proposals(
provider,
&alice_signature_keys,
)?;
When using proposal-based updates, the new leaf node’s private key is stored separately and will be used after the proposal is committed. This happens automatically when you or another member commits to the update proposal.
Understanding commit paths
Updates include a “path” - an update to the sender’s key material in the ratchet tree:
use openmls::prelude::*;
// Self-update always includes a path
let bundle = alice_group.self_update(
provider,
&alice_signature_keys,
LeafNodeParameters::default(),
)?;
// The commit includes encrypted path secrets for other members
let (commit, _, _) = bundle.into_contents();
The path ensures that:
- All members derive new shared secrets
- The group achieves post-compromise security
- Forward secrecy is maintained
Covering pending proposals
Self-updates can cover pending add proposals:
use openmls::prelude::*;
// Bob proposes to add Charlie
let add_proposal = bob_group.propose_add_member(
provider,
&bob_signature_keys,
&charlie_key_package,
)?;
// Alice processes the proposal
let processed = alice_group.process_message(provider, add_proposal)?;
if let ProcessedMessageContent::ProposalMessage(proposal) = processed.into_content() {
alice_group.store_pending_proposal(provider.storage(), *proposal)?;
}
// Alice's self-update covers the add proposal
let bundle = alice_group.self_update(
provider,
&alice_signature_keys,
LeafNodeParameters::default(),
)?;
let (commit, welcome, group_info) = bundle.into_contents();
// Welcome is Some because add proposal was covered
let welcome_msg = welcome.expect("Welcome should be present");
delivery_service.send_to_new_members(welcome_msg)?;
Even though you’re only updating your own leaf node, the commit might cover pending add proposals from the proposal store. Always check if a Welcome message is returned.
Using the commit builder
For more control, use the commit builder directly:
use openmls::prelude::*;
let leaf_node_parameters = LeafNodeParameters::builder()
.with_capabilities(new_capabilities)
.build();
let bundle = alice_group
.commit_builder()
.leaf_node_parameters(leaf_node_parameters)
.force_self_update(true) // Always include path
.load_psks(provider.storage())?
.build(provider.rand(), provider.crypto(), signer, |_| true)?
.stage_commit(provider)?;
Forced vs optional paths
Some operations force a path, while others make it optional:
use openmls::prelude::*;
// self_update() always includes a path
let bundle = alice_group.self_update(
provider,
signer,
LeafNodeParameters::default(),
)?;
// add_members() includes a path
let (commit, welcome, group_info) = alice_group.add_members(
provider,
signer,
&[bob_key_package],
)?;
// add_members_without_update() only includes path if needed
let (commit, welcome, group_info) = alice_group.add_members_without_update(
provider,
signer,
&[charlie_key_package],
)?;
See RFC 9420 Section 17.4 for which proposals require a path.
Best practices
Schedule periodic key updates:
use std::time::Duration;
// Update keys every 7 days
let update_interval = Duration::from_secs(7 * 24 * 60 * 60);
if last_update.elapsed() >= update_interval {
alice_group.self_update(
provider,
signer,
LeafNodeParameters::default(),
)?;
}
Update after compromise suspicion
Immediately update if you suspect key compromise:
if detected_potential_compromise() {
// Urgent key update
alice_group.self_update(
provider,
signer,
LeafNodeParameters::default(),
)?;
}
When processing update proposals from others:
for update_proposal in staged_commit.update_proposals() {
let new_leaf = update_proposal.update_proposal().leaf_node();
// Validate the new leaf node
validate_leaf_node(new_leaf)?;
// Validate credential
validate_credential(new_leaf.credential())?;
}
Avoid multiple members updating simultaneously:
// Use a coordination mechanism
if can_update_now() {
alice_group.self_update(provider, signer, LeafNodeParameters::default())?;
} else {
// Queue update for later
schedule_update_when_available();
}
Encryption key vs signature key
Understand the difference between the two key types:
- Encryption key (HPKE) - Used for encrypting messages and path secrets. Updated with every
self_update().
- Signature key - Used for signing messages and proposals. Only updated with
self_update_with_new_signer().
// Updates only encryption key
let bundle = alice_group.self_update(
provider,
signer,
LeafNodeParameters::default(),
)?;
// Updates both encryption AND signature keys
let bundle = alice_group.self_update_with_new_signer(
provider,
old_signer,
new_signer_bundle,
LeafNodeParameters::default(),
)?;
Validation requirements
Updated leaf nodes must meet validation requirements:
use openmls::prelude::*;
let new_extensions = Extensions::single(
Extension::ApplicationId(ApplicationIdExtension::new(b"my-app")),
)?;
let leaf_node_parameters = LeafNodeParameters::builder()
.with_extensions(new_extensions)
.build();
// This will fail if capabilities don't support the extension
let result = alice_group.self_update(
provider,
signer,
leaf_node_parameters,
);
The updated leaf node must support all group context extensions. If your new capabilities don’t include support for required extensions, the update will fail with UnsupportedGroupContextExtensions.
MlsGroup::self_update() - Update leaf node with default signer
MlsGroup::self_update_with_new_signer() - Update leaf node and signature key
MlsGroup::propose_self_update() - Create update proposal
LeafNodeParameters - Configuration for leaf node updates
NewSignerBundle - Bundle of new signer and credential
Next steps
Processing messages
Handle incoming update proposals and commits
Group configuration
Learn about sender ratchet and other security settings