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.
Welcome messages are sent to new members when they are added to a group via a commit.
Overview
A Welcome message contains all the information a new member needs to initialize their group state and join the group.
Welcome Structure
// RFC 9420
struct {
CipherSuite cipher_suite;
EncryptedGroupSecrets secrets<V>;
opaque encrypted_group_info<V>;
} Welcome;
The ciphersuite used by the group
secrets
Vec<EncryptedGroupSecrets>
Encrypted secrets for each new member (one per added KeyPackage)
Encrypted GroupInfo containing group state
Creating Welcome Messages
Welcome messages are automatically generated when adding members.
use openmls::prelude::*;
// Add members to group
let (commit, welcome, group_info) = group.add_members(
provider,
&signer,
&[key_package1, key_package2]
)?;
if let Some(welcome) = welcome {
// Welcome generated for new members
let bytes = welcome.tls_serialize_detached()?;
// Send to new members
}
Welcome is None if the commit didn’t add any members
EncryptedGroupSecrets
Each new member gets their own encrypted secrets.
struct {
KeyPackageRef new_member;
HpkeCiphertext encrypted_group_secrets;
} EncryptedGroupSecrets;
Hash reference identifying which KeyPackage these secrets are for
HPKE-encrypted GroupSecrets for this member
Finding Your Secrets
The new member finds their secrets using their KeyPackage hash:
impl Welcome {
pub(crate) fn find_encrypted_group_secret(
&self,
hash_ref: HashReference,
) -> Option<&EncryptedGroupSecrets> {
self.secrets()
.iter()
.find(|egs| hash_ref == egs.new_member())
}
}
GroupSecrets
After decryption, the new member obtains GroupSecrets.
// RFC 9420
struct {
opaque joiner_secret<V>;
optional<PathSecret> path_secret;
PreSharedKeyID psks<V>;
} GroupSecrets;
Secret used to initialize the key schedule
Path secret if the commit included an update path
psks
Vec<PreSharedKeyId>
required
Pre-shared keys injected in this epoch (usually empty)
Decrypting GroupSecrets
let group_secrets = GroupSecrets::try_from_ciphertext(
&hpke_private_key,
&ciphertext,
&context,
ciphersuite,
crypto
)?;
The new member’s HPKE private key from their KeyPackage
The encrypted group secrets from the Welcome
Context bytes (group ID) for HPKE decryption
GroupInfo
The encrypted GroupInfo contains the group state.
struct {
GroupContext group_context;
Extension extensions<V>;
MAC confirmation_tag;
uint32 signer;
opaque signature<V>;
} GroupInfo;
Complete group context (group ID, epoch, tree hash, etc.)
GroupInfo-specific extensions (e.g., RatchetTree)
Confirmation tag from the commit that added this member
Leaf index of the member who created this GroupInfo
Signature over the GroupInfo
RatchetTree Extension
GroupInfo typically includes the ratchet tree:
use openmls::extensions::*;
// Extract ratchet tree from GroupInfo
if let Some(tree_ext) = group_info.extensions()
.ratchet_tree()
{
// Ratchet tree is available
}
The RatchetTree extension allows new members to reconstruct the group tree without downloading it separately.
Processing Welcome
New members process Welcome to join the group.
use openmls::prelude::*;
// Receive Welcome message
let message_in = MlsMessageIn::tls_deserialize(&mut bytes.as_slice())?;
let welcome = message_in.into_welcome()
.ok_or("Not a Welcome message")?;
// Join the group using Welcome
let group_config = MlsGroupJoinConfig::default();
let mut group = StagedWelcome::new_from_welcome(
provider,
&group_config,
welcome,
Some(ratchet_tree), // Optional: provide if not in GroupInfo
)?;
// Group is ready to use
println!("Joined group: {:?}", group.group_id());
println!("Current epoch: {}", group.epoch());
StagedWelcome
Welcomes are first staged for inspection.
pub struct StagedWelcome {
// Validated welcome ready to finalize
}
Consume the staged welcome and create the group
Welcome Validation
Processing a Welcome involves several validation steps:
- Find encrypted secrets: Locate secrets for our KeyPackage
- Decrypt secrets: Use HPKE private key to decrypt GroupSecrets
- Decrypt GroupInfo: Decrypt using joiner secret
- Verify signature: Validate GroupInfo signature
- Verify confirmation tag: Ensure group state is correct
- Reconstruct tree: Build ratchet tree from extension or separate input
- Initialize key schedule: Derive epoch secrets
// OpenMLS handles all validation internally
let staged = StagedWelcome::new_from_welcome(
provider,
&config,
welcome,
ratchet_tree,
)?; // Returns error if validation fails
Welcome Errors
Common Errors
pub enum WelcomeError {
// Could not find encrypted secrets for our KeyPackage
NoMatchingKeyPackage,
// HPKE decryption failed
DecryptionFailed,
// GroupInfo decryption failed
GroupInfoDecryptionFailed,
// Signature validation failed
InvalidSignature,
// Confirmation tag doesn't match
InvalidConfirmationTag,
// Missing required RatchetTree
MissingRatchetTree,
// Other errors
LibraryError(LibraryError),
}
Encryption Details
GroupSecrets Encryption
GroupSecrets are encrypted using HPKE:
encrypted_group_secrets = HPKE.Seal(
pk = new_member.init_key,
label = "Welcome",
context = group_id,
plaintext = GroupSecrets
)
The init_key from the new member’s KeyPackage
GroupInfo Encryption
GroupInfo is encrypted using the joiner secret:
encrypted_group_info = AEAD.Seal(
key = welcome_key,
nonce = welcome_nonce,
aad = "",
plaintext = GroupInfo
)
welcome_key = DeriveSecret(joiner_secret, "welcome")
welcome_nonce = DeriveSecret(joiner_secret, "nonce")
PathSecret
If the commit included an update path, GroupSecrets contains a path secret.
pub struct PathSecret {
path_secret: Secret,
}
Deriving Keys
The new member uses the path secret to derive their encryption keys:
impl PathSecret {
pub(crate) fn derive_key_pair(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<EncryptionKeyPair, LibraryError> {
// Derive node secret
let node_secret = self.path_secret
.kdf_expand_label(crypto, ciphersuite, "node", &[], hash_len)?;
// Derive HPKE key pair
crypto.derive_hpke_keypair(ciphersuite.hpke_config(), &node_secret)
}
}
Ratchet Tree Delivery
The ratchet tree can be delivered in two ways:
1. RatchetTree Extension
Included in GroupInfo extensions (recommended):
// Tree included in GroupInfo
let staged = StagedWelcome::new_from_welcome(
provider,
&config,
welcome,
None, // Tree is in GroupInfo
)?;
2. External Delivery
Provided separately:
// Fetch tree externally
let ratchet_tree = fetch_ratchet_tree();
let staged = StagedWelcome::new_from_welcome(
provider,
&config,
welcome,
Some(ratchet_tree),
)?;
Using the RatchetTree extension is more convenient and ensures tree authenticity.
Examples
Send Welcome
use openmls::prelude::*;
// Add members and get Welcome
let (commit, welcome, _) = group.add_members(
provider,
&signer,
&[alice_kp, bob_kp]
)?;
if let Some(welcome) = welcome {
// Serialize Welcome
let welcome_bytes = welcome.tls_serialize_detached()?;
// Send to new members
send_to_alice(welcome_bytes.clone());
send_to_bob(welcome_bytes);
}
// Send commit to existing members
let commit_bytes = commit.tls_serialize_detached()?;
send_to_group(commit_bytes);
Process Welcome
use openmls::prelude::*;
// Receive Welcome bytes
let welcome_bytes = receive_welcome();
// Deserialize
let message_in = MlsMessageIn::tls_deserialize(
&mut welcome_bytes.as_slice()
)?;
let welcome = match message_in.extract() {
MlsMessageBodyIn::Welcome(w) => w,
_ => return Err("Expected Welcome"),
};
// Join group
let config = MlsGroupJoinConfig::builder()
.build();
let mut group = StagedWelcome::new_from_welcome(
provider,
&config,
welcome,
None, // Tree in GroupInfo
)?.into_group(provider)?;
println!("Successfully joined group {}", group.group_id());
println!("Members: {}", group.members().count());
With External Tree
// Group created without RatchetTree extension
let group_config = MlsGroupCreateConfig::builder()
.with_group_context_extensions(
Extensions::empty() // No RatchetTree extension
)?
.build();
let mut group = MlsGroup::new(
provider,
&signer,
&group_config,
credential_with_key,
)?;
// Add member (GroupInfo won't include tree)
let (commit, welcome, _) = group.add_members(
provider,
&signer,
&[key_package]
)?;
// New member must get tree separately
let ratchet_tree = export_ratchet_tree(&group);
// Send both to new member
send_welcome_and_tree(welcome, ratchet_tree);
// New member processes
let group = StagedWelcome::new_from_welcome(
provider,
&config,
welcome,
Some(ratchet_tree), // Required
)?.into_group(provider)?;
Inspect Before Joining
let staged = StagedWelcome::new_from_welcome(
provider,
&config,
welcome,
None,
)?;
// Inspect group info before joining
let group_info = staged.group_info();
println!("Group ID: {:?}", group_info.group_context().group_id());
println!("Epoch: {}", group_info.group_context().epoch());
println!("Ciphersuite: {:?}", group_info.group_context().ciphersuite());
// Decide whether to join
if should_join(group_info) {
let group = staged.into_group(provider)?;
// Joined
} else {
// Reject welcome
}
Security Considerations
Forward Secrecy
Welcome messages provide forward secrecy:
- Init keys are single-use (from KeyPackage)
- GroupSecrets are ephemeral
- Compromising current state doesn’t compromise past Welcomes
Authentication
Welcome authenticity is ensured through:
- GroupInfo signature (proves it came from group member)
- Confirmation tag (proves correct group state)
- HPKE authenticated encryption
Replay Protection
KeyPackages should be single-use:
- Each KeyPackage used once
- Prevents Welcome replay attacks
- Hash reference ensures correct secrets