// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // Copyright by contributors to this project. // SPDX-License-Identifier: (Apache-2.0 OR MIT)
use alloc::vec; use alloc::vec::Vec; use core::fmt::{self, Debug}; use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize}; use mls_rs_core::error::IntoAnyError; #[cfg(feature = "last_resort_key_package_ext")] use mls_rs_core::extension::MlsExtension; use mls_rs_core::identity::MemberValidationContext; use mls_rs_core::secret::Secret; use mls_rs_core::time::MlsTime; use snapshot::PendingCommitSnapshot; use zeroize::Zeroizing;
#[cfg(feature = "private_message")] use ciphertext_processor::*;
use component_operation::{ComponentID, ComponentOperationLabel}; use confirmation_tag::*; use framing::*; use key_schedule::*; use membership_tag::*; use message_signature::*; use message_verifier::*; use proposal::*; #[cfg(feature = "by_ref_proposal")] use proposal_cache::*; use transcript_hash::*;
#[cfg(feature = "private_message")] mod ciphertext_processor;
mod commit; pubmod component_operation; pub(crate) mod confirmation_tag; pub(crate) mod epoch; pub(crate) mod framing; mod group_info; pub(crate) mod key_schedule; mod membership_tag; pub(crate) mod message_hash; pub(crate) mod message_processor; pub(crate) mod message_signature; pub(crate) mod message_verifier; pubmod mls_rules; #[cfg(feature = "private_message")] pub(crate) mod padding; /// Proposals to evolve a MLS [`Group`] pubmod proposal; mod proposal_cache; pub(crate) mod proposal_filter; #[cfg(feature = "by_ref_proposal")] pub(crate) mod proposal_ref; #[cfg(feature = "psk")] mod resumption; mod roster; pub(crate) mod snapshot; pub(crate) mod state;
#[cfg(feature = "prior_epoch")] pub(crate) mod state_repo; #[cfg(not(feature = "prior_epoch"))] pub(crate) mod state_repo_light; #[cfg(not(feature = "prior_epoch"))] pub(crate) use state_repo_light as state_repo;
#[derive(Clone, Debug)] #[non_exhaustive] /// Information provided to new members upon joining a group. pubstruct NewMemberInfo { /// Group info extensions found within the Welcome message used to join /// the group. pub group_info_extensions: ExtensionList, /// The group member who generated the Commit adding the joiner to the /// group. This may not be the party who generated the corresponding /// add proposal pub sender: u32,
}
/// Group info extensions found within the Welcome message used to join /// the group. pubfn group_info_extensions(&self) -> &ExtensionList {
&self.group_info_extensions
}
}
/// An MLS end-to-end encrypted group. /// /// # Group Evolution /// /// MLS Groups are evolved via a propose-then-commit system. Each group state /// produced by a commit is called an epoch and can produce and consume /// application, proposal, and commit messages. A [commit](Group::commit) is used /// to advance to the next epoch by applying existing proposals sent in /// the current epoch by-reference along with an optional set of proposals /// that are included by-value using a [`CommitBuilder`]. #[derive(Clone)] pubstruct Group<C> where
C: ClientConfig,
{
config: C,
cipher_suite_provider: <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider,
state_repo: GroupStateRepository<C::GroupStateStorage, C::KeyPackageRepository>, pub(crate) state: GroupState,
epoch_secrets: EpochSecrets,
private_tree: TreeKemPrivate,
key_schedule: KeySchedule, #[cfg(feature = "by_ref_proposal")]
pending_updates: crate::map::SmallMap<HpkePublicKey, (HpkeSecretKey, Option<SignatureSecretKey>)>,
pending_commit: PendingCommitSnapshot, #[cfg(feature = "psk")]
previous_psk: Option<PskSecretInput>, #[cfg(test)] pub(crate) commit_modifiers: CommitModifiers, pub(crate) signer: SignatureSecretKey,
}
let key_package = key_package_generation.key_package;
// Identify a leaf in the tree array (any even-numbered node) whose leaf_node is identical // to the leaf_node field of the KeyPackage. If no such field exists, return an error. Let // index represent the index of this node among the leaves in the tree, namely the index of // the node in the tree array divided by two. let self_index = public_tree
.find_leaf_node(&key_package.leaf_node)
.ok_or(MlsError::WelcomeKeyPackageNotFound)?;
#[cfg(not(feature = "last_resort_key_package_ext"))] let is_last_resort = false; #[cfg(feature = "last_resort_key_package_ext")] let is_last_resort = key_package
.extensions
.has_extension(LastResortKeyPackageExt::extension_type()); // Delete the key just used if this is not a last-resort key package. let used_key_package_ref = (!is_last_resort).then_some(key_package_generation.reference);
// If the path_secret value is set in the GroupSecrets object iflet Some(path_secret) = group_secrets.path_secret {
private_tree
.update_secrets(
&cipher_suite_provider,
group_info.signer,
path_secret,
&public_tree,
)
.await?;
}
// Use the joiner_secret from the GroupSecrets object to generate the epoch secret and // other derived secrets for the current epoch. let key_schedule_result = KeySchedule::from_joiner(
&cipher_suite_provider,
&group_secrets.joiner_secret,
&group_info.group_context, #[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
public_tree.total_leaf_count(),
&psk_secret,
)
.await?;
// Verify the confirmation tag in the GroupInfo using the derived confirmation key and the // confirmed_transcript_hash from the GroupInfo. if !group_info
.confirmation_tag
.matches(
&key_schedule_result.confirmation_key,
&group_info.group_context.confirmed_transcript_hash,
&cipher_suite_provider,
)
.await?
{ return Err(MlsError::InvalidConfirmationTag);
}
let cs = config
.crypto_provider()
.cipher_suite_provider(cs)
.ok_or(MlsError::UnsupportedCipherSuite(cs))?;
// Use the confirmed transcript hash and confirmation tag to compute the interim transcript // hash in the new state. let interim_transcript_hash = InterimTranscriptHash::create(
&cs,
&group_info.group_context.confirmed_transcript_hash,
&group_info.confirmation_tag,
)
.await?;
/// The current epoch of the group. This value is incremented each /// time a [`Group::commit`] message is processed. #[inline(always)] pubfn current_epoch(&self) -> u64 { self.context().epoch
}
/// HPKE encrypts a message to the member at the specified `recipient_index` in the group. /// /// Takes `context_info`, `associated_data`, and `plaintext`. /// Returns `ciphertext` and `kem_output` inside `HpkeCiphertext`. /// /// WARNING: The message sender is not authenticated. #[cfg(feature = "non_domain_separated_hpke_encrypt_decrypt")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn hpke_encrypt_to_recipient(
&self,
recipient_index: u32,
context_info: &[u8],
associated_data: Option<&[u8]>,
plaintext: &[u8],
) -> Result<HpkeCiphertext, MlsError> { self.hpke_encrypt_to_recipient_with_generic_context(
recipient_index,
context_info,
associated_data,
plaintext,
)
.await
}
/// HPKE encrypts a message to the member at the specified `recipient_index` in the group. /// /// Takes a `component_id` and `context` to construct a `ComponentOperationLabel`, to be used as /// the HPKE seal context, to ensure domain separation. /// Also takes an `associated_data` and `plaintext`. /// Returns `ciphertext` and `kem_output` inside `HpkeCiphertext`. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn safe_encrypt_with_context_to_recipient(
&self,
recipient_index: u32,
component_id: ComponentID,
context: &[u8],
associated_data: Option<&[u8]>,
plaintext: &[u8],
) -> Result<HpkeCiphertext, MlsError> { let component_operation_label = ComponentOperationLabel::new(component_id, context); self.hpke_encrypt_to_recipient_with_generic_context(
recipient_index,
&component_operation_label.get_bytes()?,
associated_data,
plaintext,
)
.await
}
/// HPKE decrypts a message sent to the current member. /// /// Takes `HpkeCiphertext` generated by `hpke_encrypt_to_recipient` intended for the /// current member. /// /// WARNING: The message sender is not authenticated. #[cfg(feature = "non_domain_separated_hpke_encrypt_decrypt")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn hpke_decrypt_for_current_member(
&self,
context_info: &[u8],
associated_data: Option<&[u8]>,
hpke_ciphertext: HpkeCiphertext,
) -> Result<Zeroizing<Vec<u8>>, MlsError> { self.hpke_decrypt_for_current_member_with_generic_context(
context_info,
associated_data,
hpke_ciphertext,
)
.await
}
/// HPKE decrypts a message sent to the current member. /// /// Takes `HpkeCiphertext` generated by `hpke_encrypt_to_recipient` intended for the /// current member. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn safe_decrypt_with_context_for_current_member(
&self,
component_id: ComponentID,
context: &[u8],
associated_data: Option<&[u8]>,
hpke_ciphertext: HpkeCiphertext,
) -> Result<Zeroizing<Vec<u8>>, MlsError> { let component_operation_label = ComponentOperationLabel::new(component_id, context); self.hpke_decrypt_for_current_member_with_generic_context(
&component_operation_label.get_bytes()?,
associated_data,
hpke_ciphertext,
)
.await
}
/// Index within the group's state for the local group instance. /// /// This index corresponds to indexes in content descriptions within /// [`ReceivedMessage`]. #[inline(always)] pubfn current_member_index(&self) -> u32 {
*self.current_member_leaf_index()
}
/// Signing identity currently in use by the local group instance. pubfn current_member_signing_identity(&self) -> Result<&SigningIdentity, MlsError> { self.current_user_leaf_node().map(|ln| &ln.signing_identity)
}
/// Member at a specific index in the group state. /// /// These indexes correspond to indexes in content descriptions within /// [`ReceivedMessage`]. pubfn member_at_index(&self, index: u32) -> Option<Member> { self.group_state().member_at_index(index)
}
#[cfg(feature = "by_ref_proposal")] for p in &provisional_state.applied_proposals.updates { if p.sender == Sender::Member(*self_index) { let leaf_pk = &p.proposal.leaf_node.public_key;
// Update the leaf in the private tree if this is our update #[cfg(feature = "std")] let new_leaf_sk_and_signer = self.pending_updates.get(leaf_pk);
/// Create a proposal message that adds a new member to the group. /// /// `authenticated_data` will be sent unencrypted along with the contents /// of the proposal message. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_add(
&mutself,
key_package: MlsMessage,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let proposal = self.add_proposal(key_package)?; self.proposal_message(proposal, authenticated_data).await
}
/// Create a proposal message that updates your own public keys. /// /// This proposal is useful for contributing additional forward secrecy /// and post-compromise security to the group without having to perform /// the necessary computation of a [`Group::commit`]. /// /// `authenticated_data` will be sent unencrypted along with the contents /// of the proposal message. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_update(
&mutself,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let proposal = self.update_proposal(None, None, None).await?; self.proposal_message(proposal, authenticated_data).await
}
/// Create a proposal message that updates your own public keys /// as well as your credential. /// /// This proposal is useful for contributing additional forward secrecy /// and post-compromise security to the group without having to perform /// the necessary computation of a [`Group::commit`]. /// /// Identity updates are allowed by the group by default assuming that the /// new identity provided is considered /// [valid](crate::IdentityProvider::validate_member) /// by and matches the output of the /// [identity](crate::IdentityProvider) /// function of the current /// [`IdentityProvider`](crate::IdentityProvider). /// /// `authenticated_data` will be sent unencrypted along with the contents /// of the proposal message. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_update_with_identity(
&mutself,
signer: SignatureSecretKey,
signing_identity: SigningIdentity,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let proposal = self
.update_proposal(Some(signer), Some(signing_identity), None)
.await?;
#[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] asyncfn update_proposal(
&mutself,
signer: Option<SignatureSecretKey>,
signing_identity: Option<SigningIdentity>,
leaf_node_extensions: Option<ExtensionList>,
) -> Result<Proposal, MlsError> { // Grab a copy of the current node and update it to have new key material letmut new_leaf_node: LeafNode = self.current_user_leaf_node()?.clone();
let new_leaf_node_extensions =
leaf_node_extensions.unwrap_or(new_leaf_node.ungreased_extensions()); let secret_key = new_leaf_node
.update(
&self.cipher_suite_provider, self.group_id(), self.current_member_index(),
Some(self.config.leaf_properties(new_leaf_node_extensions)),
signing_identity,
signer.as_ref().unwrap_or(&self.signer),
)
.await?;
// Store the secret key in the pending updates storage for later #[cfg(feature = "std")] self.pending_updates
.insert(new_leaf_node.public_key.clone(), (secret_key, signer));
/// Create a proposal message that removes an existing member from the /// group. /// /// `authenticated_data` will be sent unencrypted along with the contents /// of the proposal message. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_remove(
&mutself,
index: u32,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let proposal = self.remove_proposal(index)?; self.proposal_message(proposal, authenticated_data).await
}
/// Create a proposal message that adds an external pre shared key to the group. /// /// Each group member will need to have the PSK associated with /// [`ExternalPskId`](mls_rs_core::psk::ExternalPskId) installed within /// the [`PreSharedKeyStorage`](mls_rs_core::psk::PreSharedKeyStorage) /// in use by this group upon processing a [commit](Group::commit) that /// contains this proposal. /// /// `authenticated_data` will be sent unencrypted along with the contents /// of the proposal message. #[cfg(all(feature = "by_ref_proposal", feature = "psk"))] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_external_psk(
&mutself,
psk: ExternalPskId,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let proposal = self.psk_proposal(JustPreSharedKeyID::External(psk))?; self.proposal_message(proposal, authenticated_data).await
}
/// Create a proposal message that adds a pre shared key from a previous /// epoch to the current group state. /// /// Each group member will need to have the secret state from `psk_epoch`. /// In particular, the members who joined between `psk_epoch` and the /// current epoch cannot process a commit containing this proposal. /// /// `authenticated_data` will be sent unencrypted along with the contents /// of the proposal message. #[cfg(all(feature = "by_ref_proposal", feature = "psk"))] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_resumption_psk(
&mutself,
psk_epoch: u64,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let key_id = ResumptionPsk {
psk_epoch,
usage: ResumptionPSKUsage::Application,
psk_group_id: PskGroupId(self.group_id().to_vec()),
};
let proposal = self.psk_proposal(JustPreSharedKeyID::Resumption(key_id))?; self.proposal_message(proposal, authenticated_data).await
}
/// Create a proposal message that requests for this group to be /// reinitialized. /// /// Once a [`ReInitProposal`](proposal::ReInitProposal) /// has been sent, another group member can complete reinitialization of /// the group by calling [`Group::get_reinit_client`]. /// /// `authenticated_data` will be sent unencrypted along with the contents /// of the proposal message. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_reinit(
&mutself,
group_id: Option<Vec<u8>>,
version: ProtocolVersion,
cipher_suite: CipherSuite,
extensions: ExtensionList,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let proposal = self.reinit_proposal(group_id, version, cipher_suite, extensions)?; self.proposal_message(proposal, authenticated_data).await
}
/// Create a proposal message that sets extensions stored in the group /// state. /// /// # Warning /// /// This function does not create a diff that will be applied to the /// current set of extension that are in use. In order for an existing /// extension to not be overwritten by this proposal, it must be included /// in the new set of extensions being proposed. /// /// /// `authenticated_data` will be sent unencrypted along with the contents /// of the proposal message. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_group_context_extensions(
&mutself,
extensions: ExtensionList,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let proposal = self.group_context_extensions_proposal(extensions); self.proposal_message(proposal, authenticated_data).await
}
/// Create a custom proposal message. /// /// `authenticated_data` will be sent unencrypted along with the contents /// of the proposal message. #[cfg(all(feature = "custom_proposal", feature = "by_ref_proposal"))] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_custom(
&mutself,
proposal: CustomProposal,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { self.proposal_message(Proposal::Custom(proposal), authenticated_data)
.await
}
/// Validate a custom proposal message, verifying its signature and membership /// tag. Can be a proposal from the current or a prior epoch. /// Can also pass in a ProposalType to check the custom proposal against. /// Returns the `authenticated_data` and sender of the custom proposal, or an error if /// it fails to validate or if the message is not a custom proposal. /// /// WARNING: This API is not recommend for general purpose usage as it reduces the guarantees of RFC9420. /// Since prior epochs' membership keys are stored, an attacker that compromises a client's signature key /// and membership key is able to send valid MLS PublicMessages appearing to originate from the compromised /// client from prior epochs. /// This API is used by the GSMA's Rich Communication Suite for certain types of messages. See section 7.6 /// for specifics https://www.gsma.com/solutions-and-impact/technologies/networks/wp-content/uploads/2025/03/RCC.16-v1.0.pdf. #[cfg(all(
feature = "custom_proposal",
feature = "by_ref_proposal",
feature = "prior_epoch_membership_key"
))] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn validate_custom_proposal(
&mutself,
msg: &MlsMessage,
proposal_type: Option<ProposalType>,
) -> Result<(Vec<u8>, Sender), MlsError> { let auth_content = self.validate_public_message(msg).await?; let proposal = match auth_content.content.content {
Content::Proposal(p) => match *p {
Proposal::Custom(c) => c,
_ => return Err(MlsError::UnexpectedMessageType),
},
_ => return Err(MlsError::UnexpectedMessageType),
}; if proposal_type.is_some() && Some(proposal.proposal_type()) != proposal_type { return Err(MlsError::UnsupportedCustomProposal(
proposal.proposal_type(),
));
}
Ok((
auth_content.content.authenticated_data,
auth_content.content.sender,
))
}
// Only used for custom proposals right now; consider removing this feature flag // if there are future use cases. #[cfg(all(feature = "custom_proposal", feature = "prior_epoch_membership_key"))] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] asyncfn validate_public_message(
&mutself,
msg: &MlsMessage,
) -> Result<AuthenticatedContent, MlsError> { let plaintext = match msg.payload {
MlsMessagePayload::Plain(ref plaintext) => plaintext.clone(),
_ => return Err(MlsError::UnexpectedMessageType),
}; let epoch_id = msg.epoch().ok_or(MlsError::EpochNotFound)?; let auth_content = if epoch_id == self.context().epoch { let auth_content = verify_plaintext_authentication(
&self.cipher_suite_provider,
plaintext,
Some(&self.key_schedule.membership_key),
&self.state.context,
SignaturePublicKeysContainer::RatchetTree(&self.state.public_tree),
)
.await?;
/// Delete all sent and received proposals cached for commit. #[cfg(feature = "by_ref_proposal")] pubfn clear_proposal_cache(&mutself) { self.state.proposals.clear()
}
/// Encrypt an application message using the current group state. /// /// `authenticated_data` will be sent unencrypted along with the contents /// of the proposal message. #[cfg(feature = "private_message")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn encrypt_application_message(
&mutself,
message: &[u8],
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { // A group member that has observed one or more proposals within an epoch MUST send a Commit message // before sending application data #[cfg(feature = "by_ref_proposal")] if !self.state.proposals.is_empty() { return Err(MlsError::CommitRequired);
}
let auth_content = if epoch_id == self.context().epoch { let content = CiphertextProcessor::new(self, self.cipher_suite_provider.clone())
.open(message)
.await?;
/// Apply a pending commit that was created by [`Group::commit`] or /// [`CommitBuilder::build`]. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn apply_pending_commit(&mutself) -> Result<CommitMessageDescription, MlsError> { let pending = core::mem::take(&mutself.pending_commit); self.apply_detached_commit(CommitSecrets(pending)).await
}
/// Apply a detached commit that was created by [`Group::commit_detached`] or /// [`CommitBuilder::build_detached`]. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn apply_detached_commit(
&mutself,
commit_secrets: CommitSecrets,
) -> Result<CommitMessageDescription, MlsError> { let pending = match commit_secrets.0 {
PendingCommitSnapshot::PendingCommit(bytes) => PendingCommit::mls_decode(&='color:red'>mut &*bytes)?,
_ => return Err(MlsError::PendingCommitNotFound),
};
/// Returns true if a commit has been created but not yet applied /// with [`Group::apply_pending_commit`] or cleared with [`Group::clear_pending_commit`] pubfn has_pending_commit(&self) -> bool {
!self.pending_commit.is_none()
}
/// Clear the currently pending commit. /// /// This function will automatically be called in the event that a /// commit message is processed using [`Group::process_incoming_message`] /// before [`Group::apply_pending_commit`] is called. pubfn clear_pending_commit(&mutself) { self.pending_commit = Default::default()
}
/// Returns true if the client has received or issued a proposal /// that needs to be committed to with [`Group::commit`] before encrypting an /// application message. #[cfg(feature = "by_ref_proposal")] pubfn commit_required(&self) -> bool {
!self.state.proposals.is_empty()
}
/// Process an inbound message for this group. /// /// # Warning /// /// Changes to the group's state as a result of processing `message` will /// not be persisted by the /// [`GroupStateStorage`](crate::GroupStateStorage) /// in use by this group until [`Group::write_to_storage`] is called. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] #[inline(never)] pubasyncfn process_incoming_message(
&mutself,
message: MlsMessage,
) -> Result<ReceivedMessage, MlsError> { iflet Some(pending) = self.pending_commit.commit_hash()? { let message_hash = MessageHash::compute(&self.cipher_suite_provider, &message).await?;
if message_hash == pending { let message_description = self.apply_pending_commit().await?;
/// Process an inbound message for this group, providing additional context /// with a message timestamp. /// /// Providing a timestamp is useful when the /// [`IdentityProvider`](crate::IdentityProvider) /// in use by the group can determine validity based on a timestamp. /// For example, this allows for checking X.509 certificate expiration /// at the time when `message` was received by a server rather than when /// a specific client asynchronously received `message` /// /// # Warning /// /// Changes to the group's state as a result of processing `message` will /// not be persisted by the /// [`GroupStateStorage`](crate::GroupStateStorage) /// in use by this group until [`Group::write_to_storage`] is called. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn process_incoming_message_with_time(
&mutself,
message: MlsMessage,
time: MlsTime,
) -> Result<ReceivedMessage, MlsError> { iflet Some(pending) = self.pending_commit.commit_hash()? { let message_hash = MessageHash::compute(&self.cipher_suite_provider, &message).await?;
if message_hash == pending { let message_description = self.apply_pending_commit().await?;
/// Find a group member by /// [identity](crate::IdentityProvider::identity) /// /// This function determines identity by calling the /// [`IdentityProvider`](crate::IdentityProvider) /// currently in use by the group. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn member_with_identity(&self, identity: &[u8]) -> Result<Member, MlsError> { let tree = &self.state.public_tree;
#[cfg(feature = "tree_index")] let index = tree.get_leaf_node_with_identity(identity);
#[cfg(not(feature = "tree_index"))] let index = tree
.get_leaf_node_with_identity(
identity,
&self.identity_provider(),
&self.state.context.extensions,
)
.await?;
let index = index.ok_or(MlsError::MemberNotFound)?; let node = self.state.public_tree.get_leaf_node(index)?;
Ok(member_from_leaf_node(node, index))
}
/// Create a group info message that can be used for external proposals and commits. /// /// The returned `GroupInfo` is suitable for one external commit for the current epoch. /// If `with_tree_in_extension` is set to true, the returned `GroupInfo` contains the /// ratchet tree and therefore contains all information needed to join the group. Otherwise, /// the ratchet tree must be obtained separately, e.g. via /// (ExternalClient::export_tree)[crate::external_client::ExternalGroup::export_tree]. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn group_info_message_allowing_ext_commit(
&self,
with_tree_in_extension: bool,
) -> Result<MlsMessage, MlsError> { let exts = ExtensionList::new(); self.group_info_message_allowing_ext_commit_with_extensions(with_tree_in_extension, exts)
.await
}
/// Create a group info message that can be used for external proposals and commits, /// and that includes a user-specified list of group info extensions. /// /// The returned `GroupInfo` is suitable for one external commit for the current epoch. /// If `with_tree_in_extension` is set to true, the returned `GroupInfo` contains the /// ratchet tree and therefore contains all information needed to join the group. Otherwise, /// the ratchet tree must be obtained separately, e.g. via /// (ExternalClient::export_tree)[crate::external_client::ExternalGroup::export_tree]. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn group_info_message_allowing_ext_commit_with_extensions(
&self,
with_tree_in_extension: bool, mut extensions: ExtensionList,
) -> Result<MlsMessage, MlsError> {
extensions.set_from({ self.key_schedule
.get_external_key_pair_ext(&self.cipher_suite_provider)
.await?
})?;
/// Create a group info message that can be used for external proposals. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn group_info_message(
&self,
with_tree_in_extension: bool,
) -> Result<MlsMessage, MlsError> { self.group_info_message_internal(ExtensionList::new(), with_tree_in_extension)
.await
}
/// Get the current group context summarizing various information about the group. #[inline(always)] pubfn context(&self) -> &GroupContext {
&self.group_state().context
}
/// Export a secret for use outside of MLS. Each epoch, label, context /// combination has a unique and independent secret. Secrets for all /// epochs, labels and contexts can be derived until either the epoch /// changes, i.e. a commit is received (or own commit is applied), or /// [Group::delete_exporter] is called. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn export_secret(
&self,
label: &[u8],
context: &[u8],
len: usize,
) -> Result<Secret, MlsError> { self.key_schedule
.export_secret(label, context, len, &self.cipher_suite_provider)
.await
.map(Into::into)
}
/// Delete the exporter secret. Afterwards the state contains no information /// about any secrets outputted by [Group::export_secret] (for the current or /// past epochs). This means that after calling this function, [Group::export_secret] /// can no longer be used and we get forward secrecy for all secrets derived using /// [Group::export_secret]. pubfn delete_exporter(&mutself) { self.key_schedule.delete_exporter();
}
/// Export the current epoch's ratchet tree in serialized format. /// /// This function is used to provide the current group tree to new members /// when the `ratchet_tree_extension` is not used according to [`MlsRules::commit_options`]. pubfn export_tree(&self) -> ExportedTree<'_> {
ExportedTree::new_borrowed(&self.current_epoch_tree().nodes)
}
/// Current version of the MLS protocol in use by this group. pubfn protocol_version(&self) -> ProtocolVersion { self.context().protocol_version
}
/// Current cipher suite in use by this group. pubfn cipher_suite(&self) -> CipherSuite { self.context().cipher_suite
}
/// Current roster pubfn roster(&self) -> Roster<'_> { self.group_state().public_tree.roster()
}
/// Determines equality of two different groups internal states. /// Useful for testing. /// pubfn equal_group_state(a: &Group<C>, b: &Group<C>) -> bool {
a.state == b.state && a.key_schedule == b.key_schedule && a.epoch_secrets == b.epoch_secrets
}
#[cfg(feature = "psk")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] asyncfn get_psk(
&self,
psks: &[ProposalInfo<PreSharedKeyProposal>],
) -> Result<(PskSecret, Vec<PreSharedKeyID>), MlsError> { iflet Some(psk) = self.previous_psk.clone() { // TODO consider throwing error if psks not empty let psk_id = vec![psk.id.clone()]; let psk = PskSecret::calculate(&[psk], self.cipher_suite_provider()).await?;
/// Returns the key generation used by the next invocation of /// [`Group::encrypt_application_message`]. Does not increment the generation nor /// derive keys. /// /// Used by clients to authenticate the generation to defend against in-group forgery /// attacks described in https://eprint.iacr.org/2025/554. This may be accomplished /// by placing the generation in the `message` or `authenticated_data` parameters of /// [`Group::encrypt_application_message`], as both fields are signed by the sender's /// signature key. /// /// To verify, get the unauthenticated generation from ApplicationMessageDescription /// returned from [`Group::process_incoming_message`], which is the value used to /// derive keys to decrypt the message, and check that it equals the authenticated /// generation. /// /// WARNING: This is only safe for synchronous usage of [`Group`] APIs. #[cfg(all(
feature = "export_key_generation",
feature = "private_message",
feature = "secret_tree_access",
))] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubfn peek_next_key_generation(&mutself) -> Result<u32, MlsError> { self.epoch_secrets
.secret_tree
.peek_next_key_generation(
&self.cipher_suite_provider, crate::tree_kem::node::NodeIndex::from(self.private_tree.self_index),
KeyType::Application,
)
.await
}
if !config.version_supported(protocol_version) { return Err(MlsError::UnsupportedProtocolVersion(protocol_version));
}
let MlsMessagePayload::Welcome(welcome) = &welcome.payload else { return Err(MlsError::UnexpectedMessageType);
};
let cipher_suite_provider =
cipher_suite_provider(config.crypto_provider(), welcome.cipher_suite)?;
let (encrypted_group_secrets, key_package_generation) =
find_key_package_generation(&config.key_package_repo(), &welcome.secrets).await?;
let key_package_version = key_package_generation.key_package.version;
if key_package_version != protocol_version { return Err(MlsError::ProtocolVersionMismatch);
}
// Decrypt the encrypted_group_secrets using HPKE with the algorithms indicated by the // cipher suite and the HPKE private key corresponding to the GroupSecrets. If a // PreSharedKeyID is part of the GroupSecrets and the client is not in possession of // the corresponding PSK, return an error let group_secrets = GroupSecrets::decrypt(
&cipher_suite_provider,
&key_package_generation.init_secret_key,
&key_package_generation.key_package.hpke_init_key,
&welcome.encrypted_group_info,
&encrypted_group_secrets.encrypted_group_secrets,
)
.await?;
// From the joiner_secret in the decrypted GroupSecrets object and the PSKs specified in // the GroupSecrets, derive the welcome_secret and using that the welcome_key and // welcome_nonce. let welcome_secret = WelcomeSecret::from_joiner_secret(
&cipher_suite_provider,
&group_secrets.joiner_secret,
&psk_secret,
)
.await?;
// Use the key and nonce to decrypt the encrypted_group_info field. let decrypted_group_info = welcome_secret
.decrypt(&welcome.encrypted_group_info)
.await?;
let group_info = GroupInfo::mls_decode(&mut &**decrypted_group_info)?;
// Update the tree hash to get context for decryption
provisional_state.group_context.tree_hash = provisional_state
.public_tree
.tree_hash(&self.cipher_suite_provider)
.await?;
let context_bytes = provisional_state.group_context.mls_encode_to_vec()?;
// Use the commit_secret, the psk_secret, the provisional GroupContext, and the init secret // from the previous epoch (or from the external init) to compute the epoch secret and // derived secrets for the new epoch
// Use the confirmation_key for the new epoch to compute the confirmation tag for // this message, as described below, and verify that it is the same as the // confirmation_tag field in the MlsPlaintext object. let new_confirmation_tag = ConfirmationTag::create(
&key_schedule_result.confirmation_key,
&provisional_state.group_context.confirmed_transcript_hash,
&self.cipher_suite_provider,
)
.await?;
if &new_confirmation_tag != confirmation_tag { return Err(MlsError::InvalidConfirmationTag);
}
use message_processor::CommitEffect; use mls_rs_core::identity::{Credential, CredentialType, CustomCredential}; use mls_rs_core::{
extension::{Extension, ExtensionType},
identity::BasicCredential,
};
#[cfg(feature = "by_ref_proposal")] use mls_rs_core::identity::CertificateChain;
// We should not be able to send application messages until a commit happens let res = test_group
.encrypt_application_message(b"test", vec![])
.await;
// The leaf node should not be the one from the update, because the committer rejects it
assert_ne!(&update_leaf, test_group.current_user_leaf_node().unwrap());
}
#[cfg(feature = "non_domain_separated_hpke_encrypt_decrypt")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn test_hpke_non_recipient_cant_decrypt() { letmut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let (mut bob, _) = alice.join("bob").await; let (carol, commit) = alice.join("carol").await;
// Apply the commit that adds carol
bob.process_incoming_message(commit).await.unwrap();
let receiver_index = alice.current_member_index(); let sender_index = bob.current_member_index();
let context_info: Vec<u8> = vec![
receiver_index.try_into().unwrap(),
sender_index.try_into().unwrap(),
]; let plaintext = b"message";
let hpke_ciphertext = bob
.hpke_encrypt_to_recipient(receiver_index, &context_info, None, plaintext)
.await
.unwrap();
// different recipient tries to decrypt let hpke_decrypted = carol
.hpke_decrypt_for_current_member(&context_info, None, hpke_ciphertext)
.await;
// should fail because carol can't decrypt the message encrypted for alice
assert_matches!(hpke_decrypted, Err(MlsError::CryptoProviderError(_)));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn safe_context_test_hpke_non_recipient_cant_decrypt() { let component_id: ComponentID = 345; letmut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let (mut bob, _) = alice.join("bob").await; let (carol, commit) = alice.join("carol").await;
// Apply the commit that adds carol
bob.process_incoming_message(commit).await.unwrap();
let receiver_index = alice.current_member_index(); let sender_index = bob.current_member_index();
let context_info: Vec<u8> = vec![
receiver_index.try_into().unwrap(),
sender_index.try_into().unwrap(),
]; let plaintext = b"message";
let hpke_ciphertext = bob
.safe_encrypt_with_context_to_recipient(
receiver_index,
component_id,
&context_info,
None,
plaintext,
)
.await
.unwrap();
// different recipient tries to decrypt let hpke_decrypted = carol
.safe_decrypt_with_context_for_current_member(
component_id,
&context_info,
None,
hpke_ciphertext,
)
.await;
// should fail because carol can't decrypt the message encrypted for alice
assert_matches!(hpke_decrypted, Err(MlsError::CryptoProviderError(_)));
}
#[cfg(feature = "non_domain_separated_hpke_encrypt_decrypt")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn test_hpke_can_decrypt_after_group_changes() { letmut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let (mut bob, _) = alice.join("bob").await;
let receiver_index = alice.current_member_index(); let sender_index = bob.current_member_index(); let context_info: Vec<u8> = vec![
receiver_index.try_into().unwrap(),
sender_index.try_into().unwrap(),
]; let associated_data: Vec<u8> = vec![1, 2, 3, 4]; let plaintext = b"message";
// encrypt the message to alice let hpke_ciphertext = bob
.hpke_encrypt_to_recipient(
receiver_index,
&context_info,
Some(&associated_data),
plaintext,
)
.await
.unwrap();
// add carol to the group let (_carol, commit) = alice.join("carol").await;
bob.process_incoming_message(commit).await.unwrap();
// make sure alice can still decrypt let hpke_decrypted = alice
.hpke_decrypt_for_current_member(&context_info, Some(&associated_data), hpke_ciphertext)
.await
.unwrap();
assert_eq!(plaintext.to_vec(), *hpke_decrypted);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn safe_context_test_hpke_can_decrypt_after_group_changes() { let component_id: ComponentID = 2; letmut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let (mut bob, _) = alice.join("bob").await;
let receiver_index = alice.current_member_index(); let sender_index = bob.current_member_index(); let context_info: Vec<u8> = vec![
receiver_index.try_into().unwrap(),
sender_index.try_into().unwrap(),
]; let associated_data: Vec<u8> = vec![1, 2, 3, 4]; let plaintext = b"message";
// encrypt the message to alice let hpke_ciphertext = bob
.safe_encrypt_with_context_to_recipient(
receiver_index,
component_id,
&context_info,
Some(&associated_data),
plaintext,
)
.await
.unwrap();
// add carol to the group let (_carol, commit) = alice.join("carol").await;
bob.process_incoming_message(commit).await.unwrap();
// make sure alice can still decrypt let hpke_decrypted = alice
.safe_decrypt_with_context_for_current_member(
component_id,
&context_info,
Some(&associated_data),
hpke_ciphertext,
)
.await
.unwrap();
assert_eq!(plaintext.to_vec(), *hpke_decrypted);
}
let data = vec![1, 2, 3]; let custom_proposal = CustomProposal::new(TEST_CUSTOM_PROPOSAL_TYPE, vec![4, 5, 6]); let proposal = alice
.propose_custom(custom_proposal.clone(), data.clone())
.await
.unwrap();
// add carol to the group let (_carol, commit) = alice.join("carol").await;
bob.process_incoming_message(commit).await.unwrap(); let (validated_data, sender) = bob
.validate_custom_proposal(&proposal, Some(TEST_CUSTOM_PROPOSAL_TYPE))
.await
.unwrap();
assert_eq!(data, validated_data);
assert_eq!(sender, Sender::Member(0));
}
let data = vec![1, 2, 3]; let custom_proposal = CustomProposal::new(TEST_CUSTOM_PROPOSAL_TYPE, vec![3, 4, 5]); let proposal = alice
.propose_custom(custom_proposal.clone(), data.clone())
.await
.unwrap();
let custom_proposal = CustomProposal::new(TEST_CUSTOM_PROPOSAL_TYPE, vec![0, 1, 2]); let proposal = alice
.propose_custom(custom_proposal.clone(), vec![])
.await
.unwrap();
// Alice adds Bob to her group. let commit_output = alice_group
.group
.commit_builder()
.add_member(bob_key_package.clone())
.unwrap()
.build()
.await
.unwrap();
// Bob joins group. let (mut bob_group, _) = bob_client
.join_group(None, &commit_output.welcome_messages[0], None)
.await
.unwrap(); // This deletes the key package used to join the group.
bob_group.write_to_storage().await.unwrap();
// Carla adds Bob, reusing the same key package. let commit_output = carla_group
.group
.commit_builder()
.add_member(bob_key_package.clone())
.unwrap()
.build()
.await
.unwrap();
// Bob cannot join Carla's group. let bob_group = bob_client
.join_group(None, &commit_output.welcome_messages[0], None)
.await
.map(|_| ());
assert_matches!(bob_group, Err(MlsError::WelcomeKeyPackageNotFound));
}
// Alice adds Bob to her group. let commit_output = alice_group
.group
.commit_builder()
.add_member(bob_key_package.clone())?
.build()
.await?;
// Bob joins group. let (mut bob_group, _) = bob_client
.join_group(None, &commit_output.welcome_messages[0], None)
.await?; // This no longer deletes the key package
bob_group.write_to_storage()?;
// Carla adds Bob, reusing the same key package. let commit_output = carla_group
.group
.commit_builder()
.add_member(bob_key_package.clone())?
.build()
.await?;
// Bob can join Carla's group.
bob_client
.join_group(None, &commit_output.welcome_messages[0], None)
.await?;
#[cfg(all(not(target_arch = "wasm32"), feature = "private_message"))] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn test_group_encrypt_plaintext_padding() { let protocol_version = TEST_PROTOCOL_VERSION; // This test requires a cipher suite whose signatures are not variable in length. let cipher_suite = CipherSuite::CURVE25519_AES128;
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn external_commit_requires_external_pub_extension() { let protocol_version = TEST_PROTOCOL_VERSION; let cipher_suite = TEST_CIPHER_SUITE; let group = test_group(protocol_version, cipher_suite).await;
let info = group
.group_info_message(false)
.await
.unwrap()
.into_group_info()
.unwrap();
let info_msg = MlsMessage::new(protocol_version, MlsMessagePayload::GroupInfo(info));
let signing_identity = group.current_member_signing_identity().unwrap().clone();
let res = external_commit::ExternalCommitBuilder::new(
group.group.signer,
signing_identity,
group.group.config,
)
.build(info_msg)
.await
.map(|_| {});
let (_, _) = test_group.join("b").await; let (_, _) = test_group.join("c").await; let (_, _) = test_group.join("d").await; let (_, _) = test_group.join("e").await; let (_, _) = test_group.join("f").await; let (_, _) = test_group.join("g").await; let (_, _) = test_group.join("h").await; let (_, _) = test_group.join("i").await; let (_, _) = test_group.join("j").await; let (_, _) = test_group.join("k").await;
// Advance the key generation so Alice has a different keygen than // Bob when the message was encrypted. let alice_key_gen = alice_group.peek_next_key_generation().unwrap();
assert!(alice_key_gen != key_gen);
// Advance the epoch so the message will be decrypted in an epoch // after it was encrypted.
alice_group.commit(vec![]).await.unwrap();
assert!(alice_group.has_pending_commit());
alice_group.apply_pending_commit().await.unwrap();
let received_by_alice = alice_group.process_incoming_message(msg).await.unwrap();
assert_matches!(
received_by_alice,
ReceivedMessage::ApplicationMessage(ApplicationMessageDescription { unauthenticated_key_generation, .. }) if unauthenticated_key_generation.unwrap().to_be_bytes() == authn_key_gen
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn commit_leaf_wrong_source() { // RFC, 13.4.2. "The leaf_node_source field MUST be set to commit."
let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 3).await;
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn commit_leaf_same_hpke_key() { // RFC 13.4.2. "Verify that the encryption_key value in the LeafNode is different from the committer's current leaf node"
let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 3).await;
// Group 0 starts using fixed key
groups[0].commit_modifiers.modify_leaf = |leaf, sk| {
leaf.public_key = get_test_25519_key(1u8);
Some(sk.clone())
};
let commit_output = groups[0].commit(vec![]).await.unwrap();
groups[0].process_pending_commit().await.unwrap();
groups[2]
.process_message(commit_output.commit_message)
.await
.unwrap();
// Group 0 tries to use the fixed key againd
let commit_output = groups[0].commit(vec![]).await.unwrap();
let res = groups[2]
.process_message(commit_output.commit_message)
.await;
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn commit_leaf_duplicate_hpke_key() { // RFC 8.3 "Verify that the following fields are unique among the members of the group: `encryption_key`"
// Group 0 tries to use the fixed key too
groups[0].commit_modifiers.modify_leaf = |leaf, sk| {
leaf.public_key = get_test_25519_key(1u8);
Some(sk.clone())
};
let commit_output = groups[0].commit(vec![]).await.unwrap();
let res = groups[7]
.process_message(commit_output.commit_message)
.await;
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn commit_leaf_duplicate_signature_key() { // RFC 8.3 "Verify that the following fields are unique among the members of the group: `signature_key`"
let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 10).await;
// Group 1 uses the fixed key
groups[1].commit_modifiers.modify_leaf = |leaf, _| {
let sk = hex!( "3468b4c890255c983e3d5cbf5cb64c1ef7f6433a518f2f3151d6672f839a06ebcad4fc381fe61822af45135c82921a348e6f46643d66ddefc70483565433714b"
)
.into();
// Group 0 tries to use the fixed key too
groups[0].commit_modifiers.modify_leaf = |leaf, _| {
let sk = hex!( "3468b4c890255c983e3d5cbf5cb64c1ef7f6433a518f2f3151d6672f839a06ebcad4fc381fe61822af45135c82921a348e6f46643d66ddefc70483565433714b"
)
.into();
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn add_leaf_duplicate_signature_key() { // RFC 8.3 "Verify that the following fields are unique among the members of the group: `signature_key`"
let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 10).await;
// copy existing signing key
let mut signing_identity = groups[1].current_member_signing_identity().unwrap().clone();
signing_identity.credential = Credential::Basic(BasicCredential::new(b"fred".to_vec()));
let secret_key = groups[1].signer.clone();
let client = TestClientBuilder::new_for_test()
.signing_identity(signing_identity, secret_key, TEST_CIPHER_SUITE)
.build();
let kp = client
.generate_key_package_message(Default::default(), Default::default(), None)
.await
.unwrap();
let res = groups[0]
.commit_builder()
.add_member(kp)
.unwrap()
.build()
.await;
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn commit_leaf_not_supporting_required_extension() { // The new leaf of the committer doesn't support an extension required by group context
let extensions = vec![extension.into_extension().unwrap()];
let mut groups =
get_test_groups_with_features(3, extensions.into(), Default::default()).await;
let commit_output = groups[0].commit(vec![]).await.unwrap();
let res = groups[2]
.process_incoming_message(commit_output.commit_message)
.await;
assert!(res.is_err());
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn commit_leaf_has_unsupported_credential() { // The new leaf of the committer has a credential unsupported by another leaf
let mut groups =
get_test_groups_with_features(3, Default::default(), Default::default()).await;
for group in groups.iter_mut() {
group.config.0.identity_provider.allow_any_custom = true;
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn commit_leaf_not_supporting_credential_used_in_another_leaf() { // The new leaf of the committer doesn't support another leaf's credential
let mut groups =
get_test_groups_with_features(3, Default::default(), Default::default()).await;
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn commit_leaf_not_supporting_required_credential() { // The new leaf of the committer doesn't support a credential required by group context
let extensions = vec![extension.into_extension().unwrap()];
let mut groups =
get_test_groups_with_features(3, extensions.into(), Default::default()).await;
let mut alice = ClientBuilder::new()
.crypto_provider(TestCryptoProvider::new())
.identity_provider(
BasicWithCustomProvider::default().with_credential_type(CredentialType::X509),
)
.with_random_signing_identity("alice", TEST_CIPHER_SUITE)
.await
.build()
.create_group(vec![ext_senders].into(), Default::default(), None)
.await
.unwrap();
let bob = ClientBuilder::new()
.crypto_provider(TestCryptoProvider::new())
.identity_provider(
BasicWithCustomProvider::default().with_credential_type(CredentialType::X509),
)
.with_random_signing_identity("bob", TEST_CIPHER_SUITE)
.await
.build();
let kp = bob
.generate_key_package_message(Default::default(), Default::default(), None)
.await
.unwrap();
let commit = alice
.commit_builder()
.add_member(kp)
.unwrap()
.build()
.await
.unwrap();
let (mut bob, _) = bob
.join_group(None, &commit.welcome_messages[0], None)
.await
.unwrap();
alice.apply_pending_commit().await.unwrap();
// New leaf supports only basic credentials (used by the group) but not X509 used by external sender
alice.commit_modifiers.modify_leaf = |leaf, sk| {
leaf.capabilities.credentials = vec![CredentialType::BASIC];
Some(sk.clone())
};
let commit = alice.commit(vec![]).await.unwrap();
let res = bob.process_incoming_message(commit.commit_message).await;
let commit_output = groups[0].commit(vec![]).await.unwrap();
let res = groups[7]
.process_message(commit_output.commit_message)
.await;
assert!(res.is_err());
}
#[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn update_proposal_can_change_credential() {
let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 3).await;
let (identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"member").await;
let update = groups[0]
.propose_update_with_identity(secret_key, identity.clone(), vec![])
.await
.unwrap();
groups[1].process_message(update).await.unwrap();
let commit_output = groups[1].commit(vec![]).await.unwrap();
// Check that the credential was updated by in the committer's state.
groups[1].process_pending_commit().await.unwrap();
let new_member = groups[1].roster().member_with_index(0).unwrap();
// Check that the credential was updated in the updater's state.
groups[0]
.process_message(commit_output.commit_message)
.await
.unwrap();
let new_member = groups[0].roster().member_with_index(0).unwrap();
let (mut bob, _) = alice
.join_with_custom_config("bob", true, |c| {
c.0.settings
.custom_proposal_types
.push(ProposalType::RCS_SIGNATURE)
})
.await
.unwrap();
let custom_proposal = RcsSignature {};
let proposal = alice
.propose_custom(custom_proposal.to_custom_proposal().unwrap(), vec![])
.await
.unwrap();
let recv_prop = bob.process_incoming_message(proposal).await.unwrap();
assert_matches!(recv_prop, ReceivedMessage::Proposal(ProposalMessageDescription { proposal: Proposal::Custom(c), ..}) if c == custom_proposal.to_custom_proposal().unwrap());
let commit = bob.commit(vec![]).await.unwrap().commit_message;
let (mut bob, _) = alice
.join_with_custom_config("bob", true, |c| {
c.0.settings
.custom_proposal_types
.push(ProposalType::RCS_SERVER_REMOVE)
})
.await
.unwrap();
let server_remove_proposal = RcsServerRemove { to_remove: 1 }; // show directly how the encoding works
let custom_proposal = CustomProposal::new(
ProposalType::RCS_SERVER_REMOVE,
server_remove_proposal.mls_encode_to_vec().unwrap(),
);
let proposal = alice
.propose_custom(custom_proposal.clone(), vec![])
.await
.unwrap();
let recv_prop = bob.process_incoming_message(proposal).await.unwrap();
assert_matches!(recv_prop, ReceivedMessage::Proposal(ProposalMessageDescription { proposal: Proposal::Custom(c), ..}) if c == custom_proposal);
let commit = bob.commit(vec![]).await.unwrap().commit_message;
// Alice creates a group requiring support for an extension
let mut alice = TestClientBuilder::new_for_test()
.with_random_signing_identity("alice", TEST_CIPHER_SUITE)
.await
.extension_type(EXTENSION_TYPE)
.build()
.create_group(group_extensions.clone(), Default::default(), None)
.await
.unwrap();
let (bob_signing_identity, bob_secret_key) =
get_test_signing_identity(TEST_CIPHER_SUITE, b"bob").await;
let carol_client = TestClientBuilder::new_for_test()
.with_random_signing_identity("carol", TEST_CIPHER_SUITE)
.await
.extension_type(EXTENSION_TYPE)
.build();
let dave_client = TestClientBuilder::new_for_test()
.with_random_signing_identity("dave", TEST_CIPHER_SUITE)
.await
.extension_type(EXTENSION_TYPE)
.build();
// Alice adds Bob, Carol and Dave to the group. They all support the mandatory extension.
let commit = alice
.commit_builder()
.add_member(
bob_client
.generate_key_package_message(Default::default(), Default::default(), None)
.await
.unwrap(),
)
.unwrap()
.add_member(
carol_client
.generate_key_package_message(Default::default(), Default::default(), None)
.await
.unwrap(),
)
.unwrap()
.add_member(
dave_client
.generate_key_package_message(Default::default(), Default::default(), None)
.await
.unwrap(),
)
.unwrap()
.build()
.await
.unwrap();
alice.apply_pending_commit().await.unwrap();
let mut bob = bob_client
.join_group(None, &commit.welcome_messages[0], None)
.await
.unwrap()
.0;
bob.write_to_storage().await.unwrap();
// Bob reloads his group data, but with parameters that will cause his generated leaves to // not support the mandatory extension.
let mut bob = TestClientBuilder::new_for_test()
.signing_identity(bob_signing_identity, bob_secret_key, TEST_CIPHER_SUITE)
.key_package_repo(bob.config.key_package_repo())
.group_state_storage(bob.config.group_state_storage())
.build()
.load_group(alice.group_id())
.await
.unwrap();
let mut carol = carol_client
.join_group(None, &commit.welcome_messages[0], None)
.await
.unwrap()
.0;
let mut dave = dave_client
.join_group(None, &commit.welcome_messages[0], None)
.await
.unwrap()
.0;
// Bob's updated leaf does not support the mandatory extension.
let bob_update = bob.propose_update(Vec::new()).await.unwrap();
let carol_update = carol.propose_update(Vec::new()).await.unwrap();
let dave_update = dave.propose_update(Vec::new()).await.unwrap();
// Alice receives the update proposals to be committed.
alice.process_incoming_message(bob_update).await.unwrap();
alice.process_incoming_message(carol_update).await.unwrap();
alice.process_incoming_message(dave_update).await.unwrap();
// Alice commits the update proposals.
alice.commit(Vec::new()).await.unwrap();
let CommitEffect::NewEpoch(new_epoch) = alice.apply_pending_commit().await.unwrap().effect else {
panic!("unexpected commit effect");
};
// Carol's and Dave's updates should be part of the commit.
assert!(find_update_for("carol"));
assert!(find_update_for("dave"));
// Bob's update should be rejected.
assert!(!find_update_for("bob"));
// Check that all members are still in the group.
let all_members_are_in = alice
.roster()
.members_iter()
.zip(["alice", "bob", "carol", "dave"])
.all(|(member, id)| {
member
.signing_identity
.credential
.as_basic()
.unwrap()
.identifier
== id.as_bytes()
});
alice.propose_self_remove(Vec::new()).await.unwrap();
let again = alice.propose_self_remove(Vec::new()).await;
assert_matches!(again, Err(MlsError::SelfRemoveAlreadyProposed));
}
let carol_client = TestClientBuilder::new_for_test()
.with_random_signing_identity("carol", TEST_CIPHER_SUITE)
.await
.custom_proposal_type(ProposalType::SELF_REMOVE)
.build();
// Alice adds Carol to the group.
let commit = alice
.commit_builder()
.add_member(
carol_client
.generate_key_package_message(Default::default(), Default::default(), None)
.await
.unwrap(),
)
.unwrap()
.build()
.await
.unwrap();
let mut carol = carol_client
.join_group(None, &commit.welcome_messages[0], None)
.await
.unwrap()
.0;
// Bob proposes self-remove.
let bob_self_remove = bob.propose_self_remove(Vec::new()).await.unwrap();
// Alice receives the self-remove proposal to be committed. // Carol also receives the self-remove proposal. Carol will need this in order // to process the commit including the proposal, because self-remove proposals // are included by reference.
alice
.process_incoming_message(bob_self_remove.clone())
.await
.unwrap();
carol
.process_incoming_message(bob_self_remove)
.await
.unwrap();
// Alice commits Bob's self-remove.
let commit = alice.commit(Vec::new()).await.unwrap();
alice.apply_pending_commit().await.unwrap();
// Assert that after applying the commit removing Bob, that Bob is no longer in the group.
let expected_members = vec![
alice.member_at_index(alice.current_member_index()).unwrap(),
carol.member_at_index(carol.current_member_index()).unwrap(),
];
itertools::assert_equal(alice.roster().members_iter(), expected_members.clone());
// Assert that Carol can also process the commit and it removes Bob from the group.
carol
.process_incoming_message(commit.commit_message.clone())
.await
.unwrap();
itertools::assert_equal(carol.roster().members_iter(), expected_members.clone()); // Assert that Bob can process the commit.
bob.process_incoming_message(commit.commit_message)
.await
.unwrap();
}
// Bob proposes self-remove.
let bob_self_remove = bob.propose_self_remove(Vec::new()).await.unwrap();
// Alice receives the self-remove proposal to be committed.
alice
.process_incoming_message(bob_self_remove.clone())
.await
.unwrap();
// Alice also removes Bob with a regular remove proposal.
alice.propose_remove(1, Vec::new()).await.unwrap();
// Alice commits Bob's self-remove and Alice's removal of Bob. This filters out the remove proposal.
let commit = alice.commit(Vec::new()).await.unwrap();
let unused = &commit.unused_proposals[0];
let expected_index = LeafIndex::unchecked(1);
assert_matches!(
unused,
ProposalInfo {
proposal: Proposal::Remove(RemoveProposal {
to_remove: i,
}),
sender: Sender::Member(0),
..
} if *i == expected_index);
}
let carol_client = TestClientBuilder::new_for_test()
.with_random_signing_identity("carol", TEST_CIPHER_SUITE)
.await
.custom_proposal_type(ProposalType::SELF_REMOVE)
.build();
// Alice adds Carol.
let commit = alice
.commit_builder()
.add_member(
carol_client
.generate_key_package_message(Default::default(), Default::default(), None)
.await
.unwrap(),
)
.unwrap()
.build()
.await
.unwrap();
let mut carol = carol_client
.join_group(None, &commit.welcome_messages[0], None)
.await
.unwrap()
.0;
alice
.process_incoming_message(commit.commit_message.clone())
.await
.unwrap();
bob.process_incoming_message(commit.commit_message)
.await
.unwrap();
// Bob proposes self-remove.
let bob_self_remove = bob.propose_self_remove(Vec::new()).await.unwrap();
// Alice receives the self-remove proposal to be committed.
alice
.process_incoming_message(bob_self_remove.clone())
.await
.unwrap();
let remove_bob_commit = alice.commit(Vec::new()).await.unwrap();
// Carol has not processed Bob's self-remove proposal, // and so the by-ref proposal is not found.
let carol_attempts_commit_processing = carol
.process_incoming_message(remove_bob_commit.commit_message)
.await;
assert_matches!(
carol_attempts_commit_processing,
Err(MlsError::ProposalNotFound)
);
}
let carol_client = TestClientBuilder::new_for_test()
.with_random_signing_identity("carol", TEST_CIPHER_SUITE)
.await
.custom_proposal_type(ProposalType::SELF_REMOVE)
.build();
// Alice adds Carol.
let commit = alice
.commit_builder()
.add_member(
carol_client
.generate_key_package_message(Default::default(), Default::default(), None)
.await
.unwrap(),
)
.unwrap()
.build()
.await
.unwrap();
let mut carol = carol_client
.join_group(None, &commit.welcome_messages[0], None)
.await
.unwrap()
.0;
alice
.process_incoming_message(commit.commit_message.clone())
.await
.unwrap();
bob.process_incoming_message(commit.commit_message)
.await
.unwrap();
let bob_self_remove = bob.propose_self_remove(Vec::new()).await.unwrap();
let group_info = alice
.group_info_message_allowing_ext_commit(true)
.await
.unwrap();
carol
.process_incoming_message(bob_self_remove.clone())
.await
.unwrap();
alice
.process_incoming_message(bob_self_remove.clone())
.await
.unwrap();
let (mut carol_new_group, commit) = carol_client
.external_commit_builder()
.unwrap()
.with_removal(carol.current_member_index())
.with_received_custom_proposal(bob_self_remove)
.build(group_info)
.await
.unwrap();
bob.process_incoming_message(commit.clone()).await.unwrap();
alice
.process_incoming_message(commit.clone())
.await
.unwrap();
// Check that carol can decrypt a message in this new group
let encrypted_message = alice
.encrypt_application_message(b"test", vec![])
.await
.unwrap();
carol_new_group
.process_incoming_message(encrypted_message)
.await
.unwrap();
// Assert that after applying the commit removing Bob, that Bob is no longer in the group.
let alice_identity = alice.current_member_signing_identity().unwrap();
let carol_identity = carol_new_group.current_member_signing_identity().unwrap();
let expected_member_identities = vec![alice_identity.clone(), carol_identity.clone()];
itertools::assert_equal(
alice.roster().members_iter().map(|m| m.signing_identity),
expected_member_identities.clone(),
);
itertools::assert_equal(
carol_new_group
.roster()
.members_iter()
.map(|m| m.signing_identity),
expected_member_identities.clone(),
);
// Check that carol's signing identity has not changed.
let carol_old_identity = carol_new_group.current_member_signing_identity().unwrap();
assert!(carol_identity == carol_old_identity);
}
let carol_client = TestClientBuilder::new_for_test()
.with_random_signing_identity("carol", TEST_CIPHER_SUITE)
.await
.custom_proposal_type(ProposalType::SELF_REMOVE)
.build();
// Alice adds Carol.
let commit = alice
.commit_builder()
.add_member(
carol_client
.generate_key_package_message(Default::default(), Default::default(), None)
.await
.unwrap(),
)
.unwrap()
.build()
.await
.unwrap();
let carol = carol_client
.join_group(None, &commit.welcome_messages[0], None)
.await
.unwrap()
.0;
alice
.process_incoming_message(commit.commit_message.clone())
.await
.unwrap();
bob.process_incoming_message(commit.commit_message)
.await
.unwrap();
let bob_self_remove = bob.propose_self_remove(Vec::new()).await.unwrap();
alice
.process_incoming_message(bob_self_remove.clone())
.await
.unwrap();
// Alice commits Bob's self-remove proposal
let remove_bob_commit = alice.commit(Vec::new()).await.unwrap();
alice
.process_incoming_message(remove_bob_commit.commit_message.clone())
.await
.unwrap();
let group_info = alice
.group_info_message_allowing_ext_commit(true)
.await
.unwrap();
// Carol builds an external commit with Alice's group state, that includes Bob's self-remove committed.
let (_, commit) = carol_client
.external_commit_builder()
.unwrap()
.with_removal(carol.current_member_index())
.build(group_info)
.await
.unwrap();
alice
.process_incoming_message(commit.clone())
.await
.unwrap();
}
let carol_client = TestClientBuilder::new_for_test()
.with_random_signing_identity("carol", TEST_CIPHER_SUITE)
.await
.custom_proposal_type(ProposalType::SELF_REMOVE)
.build();
// Alice adds Carol.
let commit = alice
.commit_builder()
.add_member(
carol_client
.generate_key_package_message(Default::default(), Default::default(), None)
.await
.unwrap(),
)
.unwrap()
.build()
.await
.unwrap();
let mut carol = carol_client
.join_group(None, &commit.welcome_messages[0], None)
.await
.unwrap()
.0;
alice
.process_incoming_message(commit.commit_message.clone())
.await
.unwrap();
bob.process_incoming_message(commit.commit_message)
.await
.unwrap();
let bob_self_remove = bob.propose_self_remove(Vec::new()).await.unwrap();
let alice_self_remove = alice.propose_self_remove(Vec::new()).await.unwrap();
let group_info = alice
.group_info_message_allowing_ext_commit(true)
.await
.unwrap();
carol
.process_incoming_message(bob_self_remove.clone())
.await
.unwrap();
alice
.process_incoming_message(bob_self_remove.clone())
.await
.unwrap();
bob.process_incoming_message(alice_self_remove.clone())
.await
.unwrap();
carol
.process_incoming_message(alice_self_remove.clone())
.await
.unwrap();
carol
.process_incoming_message(commit.clone())
.await
.unwrap();
bob.process_incoming_message(commit.clone()).await.unwrap();
alice.process_incoming_message(commit).await.unwrap();
// Assert that after applying the commit removing Bob, that Bob and Alice are no longer in the group.
let carol_identity = carol_new_group.current_member_signing_identity().unwrap();
let expected_member_identities = vec![carol_identity.clone()];
itertools::assert_equal(
carol_new_group
.roster()
.members_iter()
.map(|m| m.signing_identity),
expected_member_identities.clone(),
);
}
#[cfg(feature = "std")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn commit_processes_with_custom_time() { // Corresponds to Wednesday, February 19, 2025 9:20:00 PM GMT
let mut current_time: u64 = 1740000000;
let (bob_identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"bob").await;
let bob = TestClientBuilder::new_for_test()
.signing_identity(bob_identity, secret_key, TEST_CIPHER_SUITE)
.build();
current_time += 10;
let (alice_identity, secret_key) =
get_test_signing_identity(TEST_CIPHER_SUITE, b"alice").await;
let alice = TestClientBuilder::new_for_test()
.signing_identity(alice_identity, secret_key, TEST_CIPHER_SUITE)
.build();
current_time += 10;
let mut alice_group = alice
.create_group( Default::default(), Default::default(),
Some(current_time.into()),
)
.await
.unwrap();
current_time += 10;
let bob_key_package = bob
.generate_key_package_message( Default::default(), Default::default(),
Some(current_time.into()),
)
.await
.unwrap();
let mut alice = TestGroup {
group: alice
.create_group(Default::default(), Default::default(), None)
.await
.unwrap(),
};
let mut bob = alice.join("bob").await.0;
let mut alice = alice;
let upd = alice.propose_update(vec![]).await.unwrap();
alice.process_incoming_message(upd.clone()).await.unwrap();
bob.process_incoming_message(upd).await.unwrap();
let commit = bob.commit(vec![]).await.unwrap().commit_message;
let update = alice.process_incoming_message(commit).await.unwrap();
// Testing with std is sufficient. Non-std creates incompatible storage and a lot of special cases. #[cfg(feature = "std")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn can_be_stored_without_tree() {
let mut group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let storage = group.config.group_state_storage().inner;
group.write_to_storage().await.unwrap();
let snapshot_with_tree = storage.lock().unwrap().drain().next().unwrap().1;
group.write_to_storage_without_ratchet_tree().await.unwrap();
let snapshot_without_tree = storage.lock().unwrap().iter().next().unwrap().1.clone();
let tree = group.state.public_tree.nodes.mls_encode_to_vec().unwrap();
let empty_tree = Vec::<u8>::new().mls_encode_to_vec().unwrap();
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn tree_with_duplicate_signature_key_is_rejected() {
let cs = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let mut tree = TreeWithSigners::make_full_tree(8, &cs).await;
let signer = tree.signers[0].clone().unwrap();
let existing_leaf = tree.tree.nodes.leaves().next().unwrap().unwrap();
let duplicate_leaf = make_leaf(&cs, existing_leaf.signing_identity.clone(), &signer).await;
tree.add_leaf(duplicate_leaf, signer);
let res = TreeKemPublic::import_node_data(
tree.tree.nodes,
&BasicIdentityProvider,
&Default::default(),
)
.await;
¤ Diese beiden folgenden Angebotsgruppen bietet das Unternehmen0.179Angebot
(Wie Sie bei der Firma Beratungs- und Dienstleistungen beauftragen können 2026-08-26)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.