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.

This guide walks you through creating a basic two-party MLS group using OpenMLS. You’ll learn the fundamental concepts by building a working example.

What you’ll build

In this quickstart, you’ll create:
  1. Two participants (Sasha and Maxim) with credentials and key packages
  2. An MLS group created by Sasha
  3. An invitation for Maxim to join the group
  4. Maxim’s acceptance and joining of the group

Prerequisites

Make sure you have OpenMLS installed. See Installation for details. For this quickstart, add these dependencies to your Cargo.toml:
Cargo.toml
[dependencies]
openmls = "0.8.1"
openmls_rust_crypto = "0.5"
openmls_basic_credential = "0.5"
tls_codec = "0.4"

Step 1: Set up the environment

First, import the necessary modules and set up the ciphersuite and crypto providers:
use openmls::prelude::{*, tls_codec::*};
use openmls_rust_crypto::OpenMlsRustCrypto;
use openmls_basic_credential::SignatureKeyPair;

// Define the ciphersuite (using the mandatory-to-implement suite)
let ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;

// Create providers for both parties
let sasha_provider = &OpenMlsRustCrypto::default();
let maxim_provider = &OpenMlsRustCrypto::default();
In this example, we use separate provider instances for each participant to simulate two independent clients. In a real application, each client would run in its own process or on different devices.

Step 2: Create helper functions

Define helper functions to generate credentials and key packages:

Credential generation helper

fn generate_credential_with_key(
    identity: Vec<u8>,
    credential_type: CredentialType,
    signature_algorithm: SignatureScheme,
    provider: &impl OpenMlsProvider,
) -> (CredentialWithKey, SignatureKeyPair) {
    let credential = BasicCredential::new(identity);
    let signature_keys = SignatureKeyPair::new(signature_algorithm)
        .expect("Error generating a signature key pair.");

    // Store the signature key into the key store so OpenMLS has access to it
    signature_keys
        .store(provider.storage())
        .expect("Error storing signature keys in key store.");

    (
        CredentialWithKey {
            credential: credential.into(),
            signature_key: signature_keys.public().into(),
        },
        signature_keys,
    )
}

Key package generation helper

fn generate_key_package(
    ciphersuite: Ciphersuite,
    provider: &impl OpenMlsProvider,
    signer: &SignatureKeyPair,
    credential_with_key: CredentialWithKey,
) -> KeyPackageBundle {
    KeyPackage::builder()
        .build(
            ciphersuite,
            provider,
            signer,
            credential_with_key,
        )
        .unwrap()
}
Credentials identify participants in an MLS group. OpenMLS uses BasicCredential, which contains an identity as a byte vector.Key packages contain the public key material needed to add a client to a group asynchronously. They include:
  • A public HPKE encryption key
  • The client’s credential and signature
  • Information about supported features and extensions
  • A lifetime during which the key package is valid

Step 3: Create participants

Generate credentials and key packages for both participants:
// Create credentials for both participants
let (sasha_credential_with_key, sasha_signer) = generate_credential_with_key(
    "Sasha".into(),
    CredentialType::Basic,
    ciphersuite.signature_algorithm(),
    sasha_provider,
);

let (maxim_credential_with_key, maxim_signer) = generate_credential_with_key(
    "Maxim".into(),
    CredentialType::Basic,
    ciphersuite.signature_algorithm(),
    maxim_provider,
);

// Generate Maxim's key package
let maxim_key_package = generate_key_package(
    ciphersuite,
    maxim_provider,
    &maxim_signer,
    maxim_credential_with_key
);
In a real application, Maxim would upload their key package to a server, and Sasha would retrieve it from there. This enables asynchronous group creation.

Step 4: Create the group

Sasha creates a new MLS group:
let mut sasha_group = MlsGroup::new(
    sasha_provider,
    &sasha_signer,
    &MlsGroupCreateConfig::default(),
    sasha_credential_with_key,
)
.expect("An unexpected error occurred.");
At this point, Sasha has created a group containing only themselves.

Step 5: Add Maxim to the group

Sasha invites Maxim by adding their key package to the group:
// Add Maxim to the group
let (mls_message_out, welcome_out, group_info) = sasha_group
    .add_members(
        sasha_provider,
        &sasha_signer,
        core::slice::from_ref(maxim_key_package.key_package())
    )
    .expect("Could not add members.");

// Sasha merges the pending commit that adds Maxim
sasha_group
    .merge_pending_commit(sasha_provider)
    .expect("error merging pending commit");
When adding members:
  1. OpenMLS creates a Commit message that proposes adding Maxim to the group
  2. It generates a Welcome message containing the group secrets for Maxim
  3. It produces GroupInfo that describes the group state
The commit is initially “pending” and must be merged to update Sasha’s local group state.

Step 6: Serialize the Welcome message

Serialize the Welcome message to send it to Maxim:
// Serialize the Welcome message
let serialized_welcome = welcome_out
    .tls_serialize_detached()
    .expect("Error serializing welcome");
In a real application, this serialized message would be sent to Maxim through your application’s transport layer (e.g., via a server).

Step 7: Maxim receives and processes the Welcome

Maxim deserializes the Welcome message and joins the group:
// Maxim deserializes the message
let mls_message_in = MlsMessageIn::tls_deserialize(&mut serialized_welcome.as_slice())
    .expect("An unexpected error occurred.");

// Extract the Welcome message
let welcome = match mls_message_in.extract() {
    MlsMessageBodyIn::Welcome(welcome) => welcome,
    _ => unreachable!("Unexpected message type."),
};

// Create a staged join to inspect the Welcome
let maxim_staged_join = StagedWelcome::new_from_welcome(
    maxim_provider,
    &MlsGroupJoinConfig::default(),
    welcome,
    // The public tree is transferred out of band
    Some(sasha_group.export_ratchet_tree().into()),
)
.expect("Error creating a staged join from Welcome");

// Finally, Maxim creates the group
let mut maxim_group = maxim_staged_join
    .into_group(maxim_provider)
    .expect("Error creating the group from the staged join");
Staging allows you to inspect the Welcome message before committing to join the group. This enables validation of:
  • Group membership
  • Group parameters and policies
  • Extensions and capabilities
Once validated, you can commit by calling into_group().

Complete example

Here’s the complete working example you can run:
src/main.rs
use openmls::prelude::{*, tls_codec::*};
use openmls_rust_crypto::OpenMlsRustCrypto;
use openmls_basic_credential::SignatureKeyPair;

fn main() {
    // Define ciphersuite and providers
    let ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
    let sasha_provider = &OpenMlsRustCrypto::default();
    let maxim_provider = &OpenMlsRustCrypto::default();

    // Create credentials
    let (sasha_credential_with_key, sasha_signer) = generate_credential_with_key(
        "Sasha".into(),
        CredentialType::Basic,
        ciphersuite.signature_algorithm(),
        sasha_provider,
    );

    let (maxim_credential_with_key, maxim_signer) = generate_credential_with_key(
        "Maxim".into(),
        CredentialType::Basic,
        ciphersuite.signature_algorithm(),
        maxim_provider,
    );

    // Generate Maxim's key package
    let maxim_key_package = generate_key_package(
        ciphersuite,
        maxim_provider,
        &maxim_signer,
        maxim_credential_with_key
    );

    // Sasha creates a group
    let mut sasha_group = MlsGroup::new(
        sasha_provider,
        &sasha_signer,
        &MlsGroupCreateConfig::default(),
        sasha_credential_with_key,
    )
    .expect("An unexpected error occurred.");

    // Sasha invites Maxim
    let (mls_message_out, welcome_out, group_info) = sasha_group
        .add_members(
            sasha_provider,
            &sasha_signer,
            core::slice::from_ref(maxim_key_package.key_package())
        )
        .expect("Could not add members.");

    // Sasha merges the pending commit
    sasha_group
        .merge_pending_commit(sasha_provider)
        .expect("error merging pending commit");

    // Serialize the Welcome
    let serialized_welcome = welcome_out
        .tls_serialize_detached()
        .expect("Error serializing welcome");

    // Maxim receives and processes the Welcome
    let mls_message_in = MlsMessageIn::tls_deserialize(&mut serialized_welcome.as_slice())
        .expect("An unexpected error occurred.");

    let welcome = match mls_message_in.extract() {
        MlsMessageBodyIn::Welcome(welcome) => welcome,
        _ => unreachable!("Unexpected message type."),
    };

    let maxim_staged_join = StagedWelcome::new_from_welcome(
        maxim_provider,
        &MlsGroupJoinConfig::default(),
        welcome,
        Some(sasha_group.export_ratchet_tree().into()),
    )
    .expect("Error creating a staged join from Welcome");

    let mut maxim_group = maxim_staged_join
        .into_group(maxim_provider)
        .expect("Error creating the group from the staged join");

    println!("✅ Successfully created a two-party MLS group!");
    println!("   Sasha's group ID: {:?}", sasha_group.group_id());
    println!("   Maxim's group ID: {:?}", maxim_group.group_id());
}

// Helper functions
fn generate_credential_with_key(
    identity: Vec<u8>,
    credential_type: CredentialType,
    signature_algorithm: SignatureScheme,
    provider: &impl OpenMlsProvider,
) -> (CredentialWithKey, SignatureKeyPair) {
    let credential = BasicCredential::new(identity);
    let signature_keys = SignatureKeyPair::new(signature_algorithm)
        .expect("Error generating a signature key pair.");

    signature_keys
        .store(provider.storage())
        .expect("Error storing signature keys in key store.");

    (
        CredentialWithKey {
            credential: credential.into(),
            signature_key: signature_keys.public().into(),
        },
        signature_keys,
    )
}

fn generate_key_package(
    ciphersuite: Ciphersuite,
    provider: &impl OpenMlsProvider,
    signer: &SignatureKeyPair,
    credential_with_key: CredentialWithKey,
) -> KeyPackageBundle {
    KeyPackage::builder()
        .build(
            ciphersuite,
            provider,
            signer,
            credential_with_key,
        )
        .unwrap()
}
Run the example:
cargo run
You should see:
✅ Successfully created a two-party MLS group!
   Sasha's group ID: ...
   Maxim's group ID: ...

Key concepts recap

1

Credentials identify participants

Each participant needs a credential (identity) and signing keys to authenticate their actions in the group.
2

Key packages enable async joins

Key packages allow group creators to add members without requiring them to be online. Members publish key packages to a server in advance.
3

Welcome messages contain secrets

When adding a member, the group creator generates a Welcome message containing all the cryptographic secrets the new member needs to join.
4

Commits update group state

All group changes (adding/removing members, updates) are done via Commit messages that must be merged to take effect.

Next steps

Send messages

Learn how to send encrypted application messages in your group

Managing members

Add and remove members from groups

Group configuration

Customize group parameters and policies

Process messages

Handle incoming messages and group updates