// 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; use mls_rs_core::secret::Secret; use mls_rs_core::time::MlsTime;
#[cfg(all(feature = "std", feature = "by_ref_proposal"))] use std::collections::HashMap;
#[cfg(feature = "private_message")] use ciphertext_processor::*;
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 state::*; use transcript_hash::*;
#[cfg(feature = "private_message")] mod ciphertext_processor;
mod commit; pub(crate) mod confirmation_tag; mod context; pub(crate) mod epoch; pub(crate) mod framing; mod group_info; pub(crate) mod key_schedule; mod membership_tag; 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)] // #[cfg_attr( // all(feature = "ffi", not(test)), // safer_ffi_gen::ffi_type(clone, opaque) // )] #[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,
}
/// Group info extensions found within the Welcome message used to join /// the group. #[cfg(feature = "ffi")] 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`]. // #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type(opaque))] #[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(all(feature = "std", feature = "by_ref_proposal"))]
pending_updates: HashMap<HpkePublicKey, (HpkeSecretKey, Option<SignatureSecretKey>)>, // Hash of leaf node hpke public key to secret key #[cfg(all(not(feature = "std"), feature = "by_ref_proposal"))]
pending_updates: Vec<(HpkePublicKey, (HpkeSecretKey, Option<SignatureSecretKey>))>,
pending_commit: Option<CommitGeneration>, #[cfg(feature = "psk")]
previous_psk: Option<PskSecretInput>, #[cfg(test)] pub(crate) commit_modifiers: CommitModifiers, pub(crate) signer: SignatureSecretKey,
}
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?;
#[cfg(feature = "psk")] let psk_secret = iflet Some(psk) = additional_psk { let psk_id = group_secrets
.psks
.first()
.ok_or(MlsError::UnexpectedPskId)?;
match &psk_id.key_id {
JustPreSharedKeyID::Resumption(r) if r.usage != ResumptionPSKUsage::Application => {
Ok(())
}
_ => Err(MlsError::UnexpectedPskId),
}?;
#[cfg(not(feature = "psk"))] let psk_secret = PskSecret::new(&cipher_suite_provider);
// 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)?;
// 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_generation.key_package.leaf_node)
.ok_or(MlsError::WelcomeKeyPackageNotFound)?;
let used_key_package_ref = 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
}
/// 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.private_tree.self_index.0
}
/// Signing identity currently in use by the local group instance. // #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)] 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> { let leaf_index = LeafIndex(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).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))
.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>,
) -> Result<Proposal, MlsError> { // Grab a copy of the current node and update it to have new key material letmut new_leaf_node = self.current_user_leaf_node()?.clone();
// 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
}
/// 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_commit = self
.pending_commit
.clone()
.ok_or(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_some()
}
/// 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 = None
}
/// 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 { let message_hash = CommitHash::compute(&self.cipher_suite_provider, &message).await?;
if message_hash == pending.commit_message_hash { 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> {
MessageProcessor::process_incoming_message_with_time( self,
message, #[cfg(feature = "by_ref_proposal")] true,
Some(time),
)
.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> { letmut extensions = ExtensionList::new();
/// 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 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?;
if sender == self.private_tree.self_index { let pending = self
.pending_commit
.as_ref()
.ok_or(MlsError::CantProcessMessageFromSelf)?;
Ok(Some((
pending.pending_private_tree.clone(),
pending.pending_commit_secret.clone(),
)))
} else { // 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);
}
// We should not be able to send application messages until a commit happens let res = test_group
.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.group.current_user_leaf_node().unwrap()
);
}
#[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
.group_info_message(false)
.await
.unwrap()
.into_group_info()
.unwrap();
let info_msg = MlsMessage::new(protocol_version, MlsMessagePayload::GroupInfo(info));
let signing_identity = group
.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(|_| {});
#[cfg(feature = "state_update")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn test_state_update() { let protocol_version = TEST_PROTOCOL_VERSION; let cipher_suite = TEST_CIPHER_SUITE;
// Create a group with 10 members letmut alice = test_group(protocol_version, cipher_suite).await; let (mut bob, _) = alice.join("bob").await; letmut leaves = vec![];
for i in0..8 { let (group, commit) = alice.join(&format!("charlie{i}")).await;
leaves.push(group.group.current_user_leaf_node().unwrap().clone());
bob.process_message(commit).await.unwrap();
}
// Create many proposals, make Alice commit them
let update_message = bob.group.propose_update(vec![]).await.unwrap();
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn commit_leaf_wrong_source() { // RFC, 13.4.2. "The leaf_node_source field MUST be set to commit." letmut 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))] asyncfn 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"
letmut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 3).await;
// Group 0 starts using fixed key
groups[0].group.commit_modifiers.modify_leaf = |leaf, sk| {
leaf.public_key = get_test_25519_key(1u8);
Some(sk.clone())
};
let commit_output = groups[0].group.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].group.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))] asyncfn 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].group.commit_modifiers.modify_leaf = |leaf, sk| {
leaf.public_key = get_test_25519_key(1u8);
Some(sk.clone())
};
let commit_output = groups[0].group.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))] asyncfn commit_leaf_duplicate_signature_key() { // RFC 8.3 "Verify that the following fields are unique among the members of the group: `signature_key`"
letmut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 10).await;
// Group 1 uses the fixed key
groups[1].group.commit_modifiers.modify_leaf = |leaf, _| { let sk = hex!( "3468b4c890255c983e3d5cbf5cb64c1ef7f6433a518f2f3151d6672f839a06ebcad4fc381fe61822af45135c82921a348e6f46643d66ddefc70483565433714b"
)
.into();
// Group 0 tries to use the fixed key too
groups[0].group.commit_modifiers.modify_leaf = |leaf, _| { let sk = hex!( "3468b4c890255c983e3d5cbf5cb64c1ef7f6433a518f2f3151d6672f839a06ebcad4fc381fe61822af45135c82921a348e6f46643d66ddefc70483565433714b"
)
.into();
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn 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()]; letmut 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))] asyncfn commit_leaf_has_unsupported_credential() { // The new leaf of the committer has a credential unsupported by another leaf letmut 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))] asyncfn commit_leaf_not_supporting_credential_used_in_another_leaf() { // The new leaf of the committer doesn't support another leaf's credential
letmut 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))] asyncfn 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()]; letmut groups =
get_test_groups_with_features(3, extensions.into(), Default::default()).await;
// 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())
};
alice.commit(vec![]).await.unwrap(); let res = alice.apply_pending_commit().await;
let update = groups[0]
.group
.propose_update_with_identity(secret_key, identity.clone(), vec![])
.await
.unwrap();
groups[1].process_message(update).await.unwrap(); let commit_output = groups[1].group.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].group.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].group.roster().member_with_index(0).unwrap();
// Alice creates a group requiring support for an extension letmut alice = TestClientBuilder::new_for_test()
.with_random_signing_identity("alice", TEST_CIPHER_SUITE)
.await
.extension_type(EXTENSION_TYPE)
.build()
.create_group(group_extensions.clone())
.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().await.unwrap())
.unwrap()
.add_member(carol_client.generate_key_package_message().await.unwrap())
.unwrap()
.add_member(dave_client.generate_key_package_message().await.unwrap())
.unwrap()
.build()
.await
.unwrap();
alice.apply_pending_commit().await.unwrap();
letmut bob = bob_client
.join_group(None, &commit.welcome_messages[0])
.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. letmut 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();
letmut carol = carol_client
.join_group(None, &commit.welcome_messages[0])
.await
.unwrap()
.0;
letmut dave = dave_client
.join_group(None, &commit.welcome_messages[0])
.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 commit_desc = alice.apply_pending_commit().await.unwrap();
// Check that all updates preserve identities. let identities_are_preserved = commit_desc
.state_update
.roster_update
.updated()
.iter()
.filter_map(|u| { let before = &u.prior.signing_identity.credential.as_basic()?.identifier; let after = &u.new.signing_identity.credential.as_basic()?.identifier;
Some((before, after))
})
.all(|(before, after)| before == after);
assert!(identities_are_preserved);
// 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()
});
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.