// 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 mls_rs_codec::{MlsDecode, MlsEncode, MlsSize}; use mls_rs_core::{ crypto::SignatureSecretKey, error::IntoAnyError, extension::ExtensionList, group::Member, identity::IdentityProvider, }; use crate::{ cipher_suite::CipherSuite, client::MlsError, external_client::ExternalClientConfig, group::{ cipher_suite_provider, confirmation_tag::ConfirmationTag, framing::PublicMessage, member_from_leaf_node, message_processor::{ ApplicationMessageDescription, CommitMessageDescription, EventOrContent, MessageProcessor, ProposalMessageDescription, ProvisionalState, }, snapshot::RawGroupState, state::GroupState, transcript_hash::InterimTranscriptHash, validate_group_info_joiner, ContentType, ExportedTree, GroupContext, GroupInfo, Roster, Welcome, }, identity::SigningIdentity, protocol_version::ProtocolVersion, psk::AlwaysFoundPskStorage, tree_kem::{node::LeafIndex, path_secret::PathSecret, TreeKemPrivate}, CryptoProvider, KeyPackage, MlsMessage, }; #[cfg(feature = "by_ref_proposal")] use crate::{ group::{ framing::{Content, MlsMessagePayload}, message_processor::CachedProposal, message_signature::AuthenticatedContent, proposal::Proposal, proposal_ref::ProposalRef, Sender, }, WireFormat, }; #[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))] use crate::group::proposal::CustomProposal; #[cfg(feature = "by_ref_proposal")] use mls_rs_core::{crypto::CipherSuiteProvider, psk::ExternalPskId}; #[cfg(feature = "by_ref_proposal")] use crate::{ extension::ExternalSendersExt, group::proposal::{AddProposal, ReInitProposal, RemoveProposal}, }; #[cfg(all(feature = "by_ref_proposal", feature = "psk"))] use crate::{ group::proposal::PreSharedKeyProposal, psk::{ JustPreSharedKeyID, PreSharedKeyID, PskGroupId, PskNonce, ResumptionPSKUsage, ResumptionPsk, }, }; #[cfg(feature = "private_message")] use crate::group::framing::PrivateMessage; use alloc::boxed::Box; /// The result of processing an [ExternalGroup](ExternalGroup) message using /// [process_incoming_message](ExternalGroup::process_incoming_message) #[derive(Clone, Debug)] #[allow(clippy::large_enum_variant)] pub enum ExternalReceivedMessage { /// State update as the result of a successful commit. Commit(CommitMessageDescription), /// Received proposal and its unique identifier. Proposal(ProposalMessageDescription), /// Encrypted message that can not be processed. Ciphertext(ContentType), /// Validated GroupInfo object GroupInfo(GroupInfo), /// Validated welcome message Welcome, /// Validated key package KeyPackage(KeyPackage), } /// A handle to an observed group that can track plaintext control messages /// and the resulting group state. #[derive(Clone)] pub struct ExternalGroup where C: ExternalClientConfig, { pub(crate) config: C, pub(crate) cipher_suite_provider: ::CipherSuiteProvider, pub(crate) state: GroupState, pub(crate) signing_data: Option<(SignatureSecretKey, SigningIdentity)>, } impl ExternalGroup { #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub(crate) async fn join( config: C, signing_data: Option<(SignatureSecretKey, SigningIdentity)>, group_info: MlsMessage, tree_data: Option>, ) -> Result { let protocol_version = group_info.version; if !config.version_supported(protocol_version) { return Err(MlsError::UnsupportedProtocolVersion(protocol_version)); } let group_info = group_info .into_group_info() .ok_or(MlsError::UnexpectedMessageType)?; let cipher_suite_provider = cipher_suite_provider( config.crypto_provider(), group_info.group_context.cipher_suite, )?; let public_tree = validate_group_info_joiner( protocol_version, &group_info, tree_data, &config.identity_provider(), &cipher_suite_provider, ) .await?; let interim_transcript_hash = InterimTranscriptHash::create( &cipher_suite_provider, &group_info.group_context.confirmed_transcript_hash, &group_info.confirmation_tag, ) .await?; Ok(Self { config, signing_data, state: GroupState::new( group_info.group_context, public_tree, interim_transcript_hash, group_info.confirmation_tag, ), cipher_suite_provider, }) } /// Process a message that was sent to the group. /// /// * Proposals will be stored in the group state and processed by the /// same rules as a standard group. /// /// * Commits will result in the same outcome as a standard group. /// However, the integrity of the resulting group state can only be partially /// verified, since the external group does have access to the group /// secrets required to do a complete check. /// /// * Application messages are always encrypted so they result in a no-op /// that returns [ExternalReceivedMessage::Ciphertext] /// /// # Warning /// /// Processing an encrypted commit or proposal message has the same result /// as processing an encrypted application message. Proper tracking of /// the group state requires that all proposal and commit messages are /// readable. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn process_incoming_message( &mut self, message: MlsMessage, ) -> Result { MessageProcessor::process_incoming_message( self, message, #[cfg(feature = "by_ref_proposal")] self.config.cache_proposals(), ) .await } /// Replay a proposal message into the group skipping all validation steps. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn insert_proposal_from_message( &mut self, message: MlsMessage, ) -> Result<(), MlsError> { let ptxt = match message.payload { MlsMessagePayload::Plain(p) => Ok(p), _ => Err(MlsError::UnexpectedMessageType), }?; let auth_content: AuthenticatedContent = ptxt.into(); let proposal_ref = ProposalRef::from_content(&self.cipher_suite_provider, &auth_content).await?; let sender = auth_content.content.sender; let proposal = match auth_content.content.content { Content::Proposal(p) => Ok(*p), _ => Err(MlsError::UnexpectedMessageType), }?; self.group_state_mut() .proposals .insert(proposal_ref, proposal, sender); Ok(()) } /// Force insert a proposal directly into the internal state of the group /// with no validation. #[cfg(feature = "by_ref_proposal")] pub fn insert_proposal(&mut self, proposal: CachedProposal) { self.group_state_mut().proposals.insert( proposal.proposal_ref, proposal.proposal, proposal.sender, ) } /// Create an external proposal to request that a group add a new member /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn propose_add( &mut self, key_package: MlsMessage, authenticated_data: Vec, ) -> Result { let key_package = key_package .into_key_package() .ok_or(MlsError::UnexpectedMessageType)?; self.propose( Proposal::Add(alloc::boxed::Box::new(AddProposal { key_package })), authenticated_data, ) .await } /// Create an external proposal to request that a group remove an existing member /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn propose_remove( &mut self, index: u32, authenticated_data: Vec, ) -> Result { let to_remove = LeafIndex(index); // Verify that this leaf is actually in the tree self.group_state().public_tree.get_leaf_node(to_remove)?; self.propose( Proposal::Remove(RemoveProposal { to_remove }), authenticated_data, ) .await } /// Create an external proposal to request that a group inserts an external /// pre shared key into its state. /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(all(feature = "by_ref_proposal", feature = "psk"))] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn propose_external_psk( &mut self, psk: ExternalPskId, authenticated_data: Vec, ) -> Result { let proposal = self.psk_proposal(JustPreSharedKeyID::External(psk))?; self.propose(proposal, authenticated_data).await } /// Create an external proposal to request that a group adds a pre shared key /// from a previous epoch to the current group state. /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(all(feature = "by_ref_proposal", feature = "psk"))] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn propose_resumption_psk( &mut self, psk_epoch: u64, authenticated_data: Vec, ) -> Result { let key_id = ResumptionPsk { psk_epoch, usage: ResumptionPSKUsage::Application, psk_group_id: PskGroupId(self.group_context().group_id().to_vec()), }; let proposal = self.psk_proposal(JustPreSharedKeyID::Resumption(key_id))?; self.propose(proposal, authenticated_data).await } #[cfg(all(feature = "by_ref_proposal", feature = "psk"))] fn psk_proposal(&self, key_id: JustPreSharedKeyID) -> Result { Ok(Proposal::Psk(PreSharedKeyProposal { psk: PreSharedKeyID { key_id, psk_nonce: PskNonce::random(&self.cipher_suite_provider) .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))?, }, })) } /// Create an external proposal to request that a group sets extensions stored in the group /// state. /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn propose_group_context_extensions( &mut self, extensions: ExtensionList, authenticated_data: Vec, ) -> Result { let proposal = Proposal::GroupContextExtensions(extensions); self.propose(proposal, authenticated_data).await } /// Create an external proposal to request that a group is reinitialized. /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn propose_reinit( &mut self, group_id: Option>, version: ProtocolVersion, cipher_suite: CipherSuite, extensions: ExtensionList, authenticated_data: Vec, ) -> Result { let group_id = group_id.map(Ok).unwrap_or_else(|| { self.cipher_suite_provider .random_bytes_vec(self.cipher_suite_provider.kdf_extract_size()) .map_err(|e| MlsError::CryptoProviderError(e.into_any_error())) })?; let proposal = Proposal::ReInit(ReInitProposal { group_id, version, cipher_suite, extensions, }); self.propose(proposal, authenticated_data).await } /// Create a custom proposal message. /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn propose_custom( &mut self, proposal: CustomProposal, authenticated_data: Vec, ) -> Result { self.propose(Proposal::Custom(proposal), authenticated_data) .await } #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn propose( &mut self, proposal: Proposal, authenticated_data: Vec, ) -> Result { let (signer, signing_identity) = self.signing_data.as_ref().ok_or(MlsError::SignerNotFound)?; let external_senders_ext = self .state .context .extensions .get_as::()? .ok_or(MlsError::ExternalProposalsDisabled)?; let sender_index = external_senders_ext .allowed_senders .iter() .position(|allowed_signer| signing_identity == allowed_signer) .ok_or(MlsError::InvalidExternalSigningIdentity)?; let sender = Sender::External(sender_index as u32); let auth_content = AuthenticatedContent::new_signed( &self.cipher_suite_provider, &self.state.context, sender, Content::Proposal(Box::new(proposal.clone())), signer, WireFormat::PublicMessage, authenticated_data, ) .await?; self.state.proposals.insert( ProposalRef::from_content(&self.cipher_suite_provider, &auth_content).await?, proposal, sender, ); let plaintext = PublicMessage { content: auth_content.content, auth: auth_content.auth, membership_tag: None, }; Ok(MlsMessage::new( self.group_context().version(), MlsMessagePayload::Plain(plaintext), )) } /// Delete all sent and received proposals cached for commit. #[cfg(feature = "by_ref_proposal")] pub fn clear_proposal_cache(&mut self) { self.state.proposals.clear() } #[inline(always)] pub(crate) fn group_state(&self) -> &GroupState { &self.state } /// Get the current group context summarizing various information about the group. #[inline(always)] pub fn group_context(&self) -> &GroupContext { &self.group_state().context } /// Export the current ratchet tree used within the group. pub fn export_tree(&self) -> Result, MlsError> { self.group_state() .public_tree .nodes .mls_encode_to_vec() .map_err(Into::into) } /// Get the current roster of the group. #[inline(always)] pub fn roster(&self) -> Roster { self.group_state().public_tree.roster() } /// Get the /// [transcript hash](https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol.html#name-transcript-hashes) /// for the current epoch that the group is in. #[inline(always)] pub fn transcript_hash(&self) -> &Vec { &self.group_state().context.confirmed_transcript_hash } /// Get the /// [tree hash](https://www.rfc-editor.org/rfc/rfc9420.html#name-tree-hashes) /// for the current epoch that the group is in. #[inline(always)] pub fn tree_hash(&self) -> &[u8] { &self.group_state().context.tree_hash } /// Find a member based on their identity. /// /// Identities are matched based on the /// [IdentityProvider](crate::IdentityProvider) /// that this group was configured with. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn get_member_with_identity( &self, identity_id: &SigningIdentity, ) -> Result { let identity = self .identity_provider() .identity(identity_id, self.group_context().extensions()) .await .map_err(|error| MlsError::IdentityProviderError(error.into_any_error()))?; let tree = &self.group_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.group_context().extensions(), ) .await?; let index = index.ok_or(MlsError::MemberNotFound)?; let node = self.group_state().public_tree.get_leaf_node(index)?; Ok(member_from_leaf_node(node, index)) } } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] #[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))] #[cfg_attr( all(not(target_arch = "wasm32"), mls_build_async), maybe_async::must_be_async )] impl MessageProcessor for ExternalGroup where C: ExternalClientConfig + Clone, { type MlsRules = C::MlsRules; type IdentityProvider = C::IdentityProvider; type PreSharedKeyStorage = AlwaysFoundPskStorage; type OutputType = ExternalReceivedMessage; type CipherSuiteProvider = ::CipherSuiteProvider; #[cfg(feature = "private_message")] fn self_index(&self) -> Option { None } fn mls_rules(&self) -> Self::MlsRules { self.config.mls_rules() } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn verify_plaintext_authentication( &self, message: PublicMessage, ) -> Result, MlsError> { let auth_content = crate::group::message_verifier::verify_plaintext_authentication( &self.cipher_suite_provider, message, None, None, &self.state, ) .await?; Ok(EventOrContent::Content(auth_content)) } #[cfg(feature = "private_message")] async fn process_ciphertext( &mut self, cipher_text: &PrivateMessage, ) -> Result, MlsError> { Ok(EventOrContent::Event(ExternalReceivedMessage::Ciphertext( cipher_text.content_type, ))) } async fn update_key_schedule( &mut self, _secrets: Option<(TreeKemPrivate, PathSecret)>, interim_transcript_hash: InterimTranscriptHash, confirmation_tag: &ConfirmationTag, provisional_public_state: ProvisionalState, ) -> Result<(), MlsError> { self.state.context = provisional_public_state.group_context; #[cfg(feature = "by_ref_proposal")] self.state.proposals.clear(); self.state.interim_transcript_hash = interim_transcript_hash; self.state.public_tree = provisional_public_state.public_tree; self.state.confirmation_tag = confirmation_tag.clone(); Ok(()) } fn identity_provider(&self) -> Self::IdentityProvider { self.config.identity_provider() } fn psk_storage(&self) -> Self::PreSharedKeyStorage { AlwaysFoundPskStorage } fn group_state(&self) -> &GroupState { &self.state } fn group_state_mut(&mut self) -> &mut GroupState { &mut self.state } fn can_continue_processing(&self, _provisional_state: &ProvisionalState) -> bool { true } #[cfg(feature = "private_message")] fn min_epoch_available(&self) -> Option { self.config .max_epoch_jitter() .map(|j| self.state.context.epoch - j) } fn cipher_suite_provider(&self) -> &Self::CipherSuiteProvider { &self.cipher_suite_provider } } /// Serializable snapshot of an [ExternalGroup](ExternalGroup) state. #[derive(Debug, MlsEncode, MlsSize, MlsDecode, PartialEq, Clone)] pub struct ExternalSnapshot { version: u16, state: RawGroupState, signing_data: Option<(SignatureSecretKey, SigningIdentity)>, } impl ExternalSnapshot { /// Serialize the snapshot pub fn to_bytes(&self) -> Result, MlsError> { Ok(self.mls_encode_to_vec()?) } /// Deserialize the snapshot pub fn from_bytes(bytes: &[u8]) -> Result { Ok(Self::mls_decode(&mut &*bytes)?) } } impl ExternalGroup where C: ExternalClientConfig + Clone, { /// Create a snapshot of this group's current internal state. pub fn snapshot(&self) -> ExternalSnapshot { ExternalSnapshot { state: RawGroupState::export(self.group_state()), version: 1, signing_data: self.signing_data.clone(), } } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub(crate) async fn from_snapshot( config: C, snapshot: ExternalSnapshot, ) -> Result { #[cfg(feature = "tree_index")] let identity_provider = config.identity_provider(); let cipher_suite_provider = cipher_suite_provider( config.crypto_provider(), snapshot.state.context.cipher_suite, )?; Ok(ExternalGroup { config, signing_data: snapshot.signing_data, state: snapshot .state .import( #[cfg(feature = "tree_index")] &identity_provider, ) .await?, cipher_suite_provider, }) } } impl From for ExternalReceivedMessage { fn from(value: CommitMessageDescription) -> Self { ExternalReceivedMessage::Commit(value) } } impl TryFrom for ExternalReceivedMessage { type Error = MlsError; fn try_from(_: ApplicationMessageDescription) -> Result { Err(MlsError::UnencryptedApplicationMessage) } } impl From for ExternalReceivedMessage { fn from(value: ProposalMessageDescription) -> Self { ExternalReceivedMessage::Proposal(value) } } impl From for ExternalReceivedMessage { fn from(value: GroupInfo) -> Self { ExternalReceivedMessage::GroupInfo(value) } } impl From for ExternalReceivedMessage { fn from(_: Welcome) -> Self { ExternalReceivedMessage::Welcome } } impl From for ExternalReceivedMessage { fn from(value: KeyPackage) -> Self { ExternalReceivedMessage::KeyPackage(value) } } #[cfg(test)] pub(crate) mod test_utils { use crate::{ external_client::tests_utils::{TestExternalClientBuilder, TestExternalClientConfig}, group::test_utils::TestGroup, }; use super::ExternalGroup; #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub(crate) async fn make_external_group( group: &TestGroup, ) -> ExternalGroup { make_external_group_with_config( group, TestExternalClientBuilder::new_for_test().build_config(), ) .await } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub(crate) async fn make_external_group_with_config( group: &TestGroup, config: TestExternalClientConfig, ) -> ExternalGroup { ExternalGroup::join( config, None, group .group .group_info_message_allowing_ext_commit(true) .await .unwrap(), None, ) .await .unwrap() } } #[cfg(test)] mod tests { use super::test_utils::make_external_group; use crate::{ cipher_suite::CipherSuite, client::{ test_utils::{TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION}, MlsError, }, crypto::{test_utils::TestCryptoProvider, SignatureSecretKey}, extension::ExternalSendersExt, external_client::{ group::test_utils::make_external_group_with_config, tests_utils::{TestExternalClientBuilder, TestExternalClientConfig}, ExternalGroup, ExternalReceivedMessage, ExternalSnapshot, }, group::{ framing::{Content, MlsMessagePayload}, proposal::{AddProposal, Proposal, ProposalOrRef}, proposal_ref::ProposalRef, test_utils::{test_group, TestGroup}, ProposalMessageDescription, }, identity::{test_utils::get_test_signing_identity, SigningIdentity}, key_package::test_utils::{test_key_package, test_key_package_message}, protocol_version::ProtocolVersion, ExtensionList, MlsMessage, }; use assert_matches::assert_matches; use mls_rs_codec::{MlsDecode, MlsEncode}; #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn test_group_with_one_commit(v: ProtocolVersion, cs: CipherSuite) -> TestGroup { let mut group = test_group(v, cs).await; group.group.commit(Vec::new()).await.unwrap(); group.process_pending_commit().await.unwrap(); group } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn test_group_two_members( v: ProtocolVersion, cs: CipherSuite, #[cfg(feature = "by_ref_proposal")] ext_identity: Option, ) -> TestGroup { let mut group = test_group_with_one_commit(v, cs).await; let bob_key_package = test_key_package_message(v, cs, "bob").await; let mut commit_builder = group .group .commit_builder() .add_member(bob_key_package) .unwrap(); #[cfg(feature = "by_ref_proposal")] if let Some(ext_signer) = ext_identity { let mut ext_list = ExtensionList::new(); ext_list .set_from(ExternalSendersExt { allowed_senders: vec![ext_signer], }) .unwrap(); commit_builder = commit_builder.set_group_context_ext(ext_list).unwrap(); } commit_builder.build().await.unwrap(); group.process_pending_commit().await.unwrap(); group } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_can_be_created() { for (v, cs) in ProtocolVersion::all().flat_map(|v| { TestCryptoProvider::all_supported_cipher_suites() .into_iter() .map(move |cs| (v, cs)) }) { make_external_group(&test_group_with_one_commit(v, cs).await).await; } } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_can_process_commit() { let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut server = make_external_group(&alice).await; let commit_output = alice.group.commit(Vec::new()).await.unwrap(); alice.group.apply_pending_commit().await.unwrap(); server .process_incoming_message(commit_output.commit_message) .await .unwrap(); assert_eq!(alice.group.state, server.state); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_can_process_proposals_by_reference() { let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut server = make_external_group(&alice).await; let bob_key_package = test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await; let add_proposal = Proposal::Add(Box::new(AddProposal { key_package: bob_key_package, })); let packet = alice.propose(add_proposal.clone()).await; let proposal_process = server.process_incoming_message(packet).await.unwrap(); assert_matches!( proposal_process, ExternalReceivedMessage::Proposal(ProposalMessageDescription { ref proposal, ..}) if proposal == &add_proposal ); let commit_output = alice.group.commit(vec![]).await.unwrap(); alice.group.apply_pending_commit().await.unwrap(); let commit_result = server .process_incoming_message(commit_output.commit_message) .await .unwrap(); #[cfg(feature = "state_update")] assert_matches!( commit_result, ExternalReceivedMessage::Commit(commit_description) if commit_description.state_update.roster_update.added().iter().any(|added| added.index == 1) ); #[cfg(not(feature = "state_update"))] assert_matches!(commit_result, ExternalReceivedMessage::Commit(_)); assert_eq!(alice.group.state, server.state); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_can_process_commit_adding_member() { let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut server = make_external_group(&alice).await; let (_, commit) = alice.join("bob").await; let update = match server.process_incoming_message(commit).await.unwrap() { ExternalReceivedMessage::Commit(update) => update.state_update, _ => panic!("Expected processed commit"), }; #[cfg(feature = "state_update")] assert_eq!(update.roster_update.added().len(), 1); assert_eq!(server.state.public_tree.get_leaf_nodes().len(), 2); assert_eq!(alice.group.state, server.state); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_rejects_commit_not_for_current_epoch() { let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut server = make_external_group(&alice).await; let mut commit_output = alice.group.commit(vec![]).await.unwrap(); match commit_output.commit_message.payload { MlsMessagePayload::Plain(ref mut plain) => plain.content.epoch = 0, _ => panic!("Unexpected non-plaintext data"), }; let res = server .process_incoming_message(commit_output.commit_message) .await; assert_matches!(res, Err(MlsError::InvalidEpoch)); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_can_reject_message_with_invalid_signature() { let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut server = make_external_group_with_config( &alice, TestExternalClientBuilder::new_for_test().build_config(), ) .await; let mut commit_output = alice.group.commit(Vec::new()).await.unwrap(); match commit_output.commit_message.payload { MlsMessagePayload::Plain(ref mut plain) => plain.auth.signature = Vec::new().into(), _ => panic!("Unexpected non-plaintext data"), }; let res = server .process_incoming_message(commit_output.commit_message) .await; assert_matches!(res, Err(MlsError::InvalidSignature)); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_rejects_unencrypted_application_message() { let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut server = make_external_group(&alice).await; let plaintext = alice .make_plaintext(Content::Application(b"hello".to_vec().into())) .await; let res = server.process_incoming_message(plaintext).await; assert_matches!(res, Err(MlsError::UnencryptedApplicationMessage)); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_will_reject_unsupported_cipher_suites() { let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let config = TestExternalClientBuilder::new_for_test_disabling_cipher_suite(TEST_CIPHER_SUITE) .build_config(); let res = ExternalGroup::join( config, None, alice .group .group_info_message_allowing_ext_commit(true) .await .unwrap(), None, ) .await .map(|_| ()); assert_matches!( res, Err(MlsError::UnsupportedCipherSuite(TEST_CIPHER_SUITE)) ); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_will_reject_unsupported_protocol_versions() { let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let config = TestExternalClientBuilder::new_for_test().build_config(); let mut group_info = alice .group .group_info_message_allowing_ext_commit(true) .await .unwrap(); group_info.version = ProtocolVersion::from(64); let res = ExternalGroup::join(config, None, group_info, None) .await .map(|_| ()); assert_matches!( res, Err(MlsError::UnsupportedProtocolVersion(v)) if v == ProtocolVersion::from(64) ); } #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn setup_extern_proposal_test( extern_proposals_allowed: bool, ) -> (SigningIdentity, SignatureSecretKey, TestGroup) { let (server_identity, server_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"server").await; let alice = test_group_two_members( TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, extern_proposals_allowed.then(|| server_identity.clone()), ) .await; (server_identity, server_key, alice) } #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn test_external_proposal( server: &mut ExternalGroup, alice: &mut TestGroup, external_proposal: MlsMessage, ) { let auth_content = external_proposal.clone().into_plaintext().unwrap().into(); let proposal_ref = ProposalRef::from_content(&server.cipher_suite_provider, &auth_content) .await .unwrap(); // Alice receives the proposal alice.process_message(external_proposal).await.unwrap(); // Alice commits the proposal let commit_output = alice.group.commit(vec![]).await.unwrap(); let commit = match commit_output .commit_message .clone() .into_plaintext() .unwrap() .content .content { Content::Commit(commit) => commit, _ => panic!("not a commit"), }; // The proposal should be in the resulting commit assert!(commit .proposals .contains(&ProposalOrRef::Reference(proposal_ref))); alice.process_pending_commit().await.unwrap(); server .process_incoming_message(commit_output.commit_message) .await .unwrap(); assert_eq!(alice.group.state, server.state); } #[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_can_propose_add() { let (server_identity, server_key, mut alice) = setup_extern_proposal_test(true).await; let mut server = make_external_group(&alice).await; server.signing_data = Some((server_key, server_identity)); let charlie_key_package = test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "charlie").await; let external_proposal = server .propose_add(charlie_key_package, vec![]) .await .unwrap(); test_external_proposal(&mut server, &mut alice, external_proposal).await } #[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_can_propose_remove() { let (server_identity, server_key, mut alice) = setup_extern_proposal_test(true).await; let mut server = make_external_group(&alice).await; server.signing_data = Some((server_key, server_identity)); let external_proposal = server.propose_remove(1, vec![]).await.unwrap(); test_external_proposal(&mut server, &mut alice, external_proposal).await } #[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_external_proposal_not_allowed() { let (signing_id, secret_key, alice) = setup_extern_proposal_test(false).await; let mut server = make_external_group(&alice).await; server.signing_data = Some((secret_key, signing_id)); let charlie_key_package = test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "charlie").await; let res = server.propose_add(charlie_key_package, vec![]).await; assert_matches!(res, Err(MlsError::ExternalProposalsDisabled)); } #[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_external_signing_identity_invalid() { let (server_identity, server_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"server").await; let alice = test_group_two_members( TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, Some( get_test_signing_identity(TEST_CIPHER_SUITE, b"not server") .await .0, ), ) .await; let mut server = make_external_group(&alice).await; server.signing_data = Some((server_key, server_identity)); let res = server.propose_remove(1, vec![]).await; assert_matches!(res, Err(MlsError::InvalidExternalSigningIdentity)); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_errors_on_old_epoch() { let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut server = make_external_group_with_config( &alice, TestExternalClientBuilder::new_for_test() .max_epoch_jitter(0) .build_config(), ) .await; let old_application_msg = alice .group .encrypt_application_message(&[], vec![]) .await .unwrap(); let commit_output = alice.group.commit(vec![]).await.unwrap(); server .process_incoming_message(commit_output.commit_message) .await .unwrap(); let res = server.process_incoming_message(old_application_msg).await; assert_matches!(res, Err(MlsError::InvalidEpoch)); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn proposals_can_be_cached_externally() { let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut server = make_external_group_with_config( &alice, TestExternalClientBuilder::new_for_test() .cache_proposals(false) .build_config(), ) .await; let proposal = alice.group.propose_update(vec![]).await.unwrap(); let commit_output = alice.group.commit(vec![]).await.unwrap(); server .process_incoming_message(proposal.clone()) .await .unwrap(); server.insert_proposal_from_message(proposal).await.unwrap(); server .process_incoming_message(commit_output.commit_message) .await .unwrap(); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_can_observe_since_creation() { let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let info = alice .group .group_info_message_allowing_ext_commit(true) .await .unwrap(); let config = TestExternalClientBuilder::new_for_test().build_config(); let mut server = ExternalGroup::join(config, None, info, None).await.unwrap(); for _ in 0..2 { let commit = alice.group.commit(vec![]).await.unwrap().commit_message; alice.process_pending_commit().await.unwrap(); server.process_incoming_message(commit).await.unwrap(); } } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_can_be_serialized_to_tls_encoding() { let server = make_external_group(&test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await).await; let snapshot = server.snapshot().mls_encode_to_vec().unwrap(); let snapshot_restored = ExternalSnapshot::mls_decode(&mut snapshot.as_slice()).unwrap(); let server_restored = ExternalGroup::from_snapshot(server.config.clone(), snapshot_restored) .await .unwrap(); assert_eq!(server.group_state(), server_restored.group_state()); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_can_validate_info() { let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut server = make_external_group(&alice).await; let info = alice .group .group_info_message_allowing_ext_commit(false) .await .unwrap(); let update = server.process_incoming_message(info.clone()).await.unwrap(); let info = info.into_group_info().unwrap(); assert_matches!(update, ExternalReceivedMessage::GroupInfo(update_info) if update_info == info); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_can_validate_key_package() { let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut server = make_external_group(&alice).await; let kp = test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "john").await; let update = server.process_incoming_message(kp.clone()).await.unwrap(); let kp = kp.into_key_package().unwrap(); assert_matches!(update, ExternalReceivedMessage::KeyPackage(update_kp) if update_kp == kp); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_group_can_validate_welcome() { let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut server = make_external_group(&alice).await; let [welcome] = alice .group .commit_builder() .add_member( test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "john").await, ) .unwrap() .build() .await .unwrap() .welcome_messages .try_into() .unwrap(); let update = server.process_incoming_message(welcome).await.unwrap(); assert_matches!(update, ExternalReceivedMessage::Welcome); } }