1 // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 // Copyright by contributors to this project. 3 // SPDX-License-Identifier: (Apache-2.0 OR MIT) 4 5 use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize}; 6 use mls_rs_core::{ 7 crypto::SignatureSecretKey, error::IntoAnyError, extension::ExtensionList, group::Member, 8 identity::IdentityProvider, 9 }; 10 11 use crate::{ 12 cipher_suite::CipherSuite, 13 client::MlsError, 14 external_client::ExternalClientConfig, 15 group::{ 16 cipher_suite_provider, 17 confirmation_tag::ConfirmationTag, 18 framing::PublicMessage, 19 member_from_leaf_node, 20 message_processor::{ 21 ApplicationMessageDescription, CommitMessageDescription, EventOrContent, 22 MessageProcessor, ProposalMessageDescription, ProvisionalState, 23 }, 24 snapshot::RawGroupState, 25 state::GroupState, 26 transcript_hash::InterimTranscriptHash, 27 validate_group_info_joiner, ContentType, ExportedTree, GroupContext, GroupInfo, Roster, 28 Welcome, 29 }, 30 identity::SigningIdentity, 31 protocol_version::ProtocolVersion, 32 psk::AlwaysFoundPskStorage, 33 tree_kem::{node::LeafIndex, path_secret::PathSecret, TreeKemPrivate}, 34 CryptoProvider, KeyPackage, MlsMessage, 35 }; 36 37 #[cfg(feature = "by_ref_proposal")] 38 use crate::{ 39 group::{ 40 framing::{Content, MlsMessagePayload}, 41 message_processor::CachedProposal, 42 message_signature::AuthenticatedContent, 43 proposal::Proposal, 44 proposal_ref::ProposalRef, 45 Sender, 46 }, 47 WireFormat, 48 }; 49 50 #[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))] 51 use crate::group::proposal::CustomProposal; 52 53 #[cfg(feature = "by_ref_proposal")] 54 use mls_rs_core::{crypto::CipherSuiteProvider, psk::ExternalPskId}; 55 56 #[cfg(feature = "by_ref_proposal")] 57 use crate::{ 58 extension::ExternalSendersExt, 59 group::proposal::{AddProposal, ReInitProposal, RemoveProposal}, 60 }; 61 62 #[cfg(all(feature = "by_ref_proposal", feature = "psk"))] 63 use crate::{ 64 group::proposal::PreSharedKeyProposal, 65 psk::{ 66 JustPreSharedKeyID, PreSharedKeyID, PskGroupId, PskNonce, ResumptionPSKUsage, ResumptionPsk, 67 }, 68 }; 69 70 #[cfg(feature = "private_message")] 71 use crate::group::framing::PrivateMessage; 72 73 use alloc::boxed::Box; 74 75 /// The result of processing an [ExternalGroup](ExternalGroup) message using 76 /// [process_incoming_message](ExternalGroup::process_incoming_message) 77 #[derive(Clone, Debug)] 78 #[allow(clippy::large_enum_variant)] 79 pub enum ExternalReceivedMessage { 80 /// State update as the result of a successful commit. 81 Commit(CommitMessageDescription), 82 /// Received proposal and its unique identifier. 83 Proposal(ProposalMessageDescription), 84 /// Encrypted message that can not be processed. 85 Ciphertext(ContentType), 86 /// Validated GroupInfo object 87 GroupInfo(GroupInfo), 88 /// Validated welcome message 89 Welcome, 90 /// Validated key package 91 KeyPackage(KeyPackage), 92 } 93 94 /// A handle to an observed group that can track plaintext control messages 95 /// and the resulting group state. 96 #[derive(Clone)] 97 pub struct ExternalGroup<C> 98 where 99 C: ExternalClientConfig, 100 { 101 pub(crate) config: C, 102 pub(crate) cipher_suite_provider: <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider, 103 pub(crate) state: GroupState, 104 pub(crate) signing_data: Option<(SignatureSecretKey, SigningIdentity)>, 105 } 106 107 impl<C: ExternalClientConfig + Clone> ExternalGroup<C> { 108 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] join( config: C, signing_data: Option<(SignatureSecretKey, SigningIdentity)>, group_info: MlsMessage, tree_data: Option<ExportedTree<'_>>, ) -> Result<Self, MlsError>109 pub(crate) async fn join( 110 config: C, 111 signing_data: Option<(SignatureSecretKey, SigningIdentity)>, 112 group_info: MlsMessage, 113 tree_data: Option<ExportedTree<'_>>, 114 ) -> Result<Self, MlsError> { 115 let protocol_version = group_info.version; 116 117 if !config.version_supported(protocol_version) { 118 return Err(MlsError::UnsupportedProtocolVersion(protocol_version)); 119 } 120 121 let group_info = group_info 122 .into_group_info() 123 .ok_or(MlsError::UnexpectedMessageType)?; 124 125 let cipher_suite_provider = cipher_suite_provider( 126 config.crypto_provider(), 127 group_info.group_context.cipher_suite, 128 )?; 129 130 let public_tree = validate_group_info_joiner( 131 protocol_version, 132 &group_info, 133 tree_data, 134 &config.identity_provider(), 135 &cipher_suite_provider, 136 ) 137 .await?; 138 139 let interim_transcript_hash = InterimTranscriptHash::create( 140 &cipher_suite_provider, 141 &group_info.group_context.confirmed_transcript_hash, 142 &group_info.confirmation_tag, 143 ) 144 .await?; 145 146 Ok(Self { 147 config, 148 signing_data, 149 state: GroupState::new( 150 group_info.group_context, 151 public_tree, 152 interim_transcript_hash, 153 group_info.confirmation_tag, 154 ), 155 cipher_suite_provider, 156 }) 157 } 158 159 /// Process a message that was sent to the group. 160 /// 161 /// * Proposals will be stored in the group state and processed by the 162 /// same rules as a standard group. 163 /// 164 /// * Commits will result in the same outcome as a standard group. 165 /// However, the integrity of the resulting group state can only be partially 166 /// verified, since the external group does have access to the group 167 /// secrets required to do a complete check. 168 /// 169 /// * Application messages are always encrypted so they result in a no-op 170 /// that returns [ExternalReceivedMessage::Ciphertext] 171 /// 172 /// # Warning 173 /// 174 /// Processing an encrypted commit or proposal message has the same result 175 /// as processing an encrypted application message. Proper tracking of 176 /// the group state requires that all proposal and commit messages are 177 /// readable. 178 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] process_incoming_message( &mut self, message: MlsMessage, ) -> Result<ExternalReceivedMessage, MlsError>179 pub async fn process_incoming_message( 180 &mut self, 181 message: MlsMessage, 182 ) -> Result<ExternalReceivedMessage, MlsError> { 183 MessageProcessor::process_incoming_message( 184 self, 185 message, 186 #[cfg(feature = "by_ref_proposal")] 187 self.config.cache_proposals(), 188 ) 189 .await 190 } 191 192 /// Replay a proposal message into the group skipping all validation steps. 193 #[cfg(feature = "by_ref_proposal")] 194 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] insert_proposal_from_message( &mut self, message: MlsMessage, ) -> Result<(), MlsError>195 pub async fn insert_proposal_from_message( 196 &mut self, 197 message: MlsMessage, 198 ) -> Result<(), MlsError> { 199 let ptxt = match message.payload { 200 MlsMessagePayload::Plain(p) => Ok(p), 201 _ => Err(MlsError::UnexpectedMessageType), 202 }?; 203 204 let auth_content: AuthenticatedContent = ptxt.into(); 205 206 let proposal_ref = 207 ProposalRef::from_content(&self.cipher_suite_provider, &auth_content).await?; 208 209 let sender = auth_content.content.sender; 210 211 let proposal = match auth_content.content.content { 212 Content::Proposal(p) => Ok(*p), 213 _ => Err(MlsError::UnexpectedMessageType), 214 }?; 215 216 self.group_state_mut() 217 .proposals 218 .insert(proposal_ref, proposal, sender); 219 220 Ok(()) 221 } 222 223 /// Force insert a proposal directly into the internal state of the group 224 /// with no validation. 225 #[cfg(feature = "by_ref_proposal")] insert_proposal(&mut self, proposal: CachedProposal)226 pub fn insert_proposal(&mut self, proposal: CachedProposal) { 227 self.group_state_mut().proposals.insert( 228 proposal.proposal_ref, 229 proposal.proposal, 230 proposal.sender, 231 ) 232 } 233 234 /// Create an external proposal to request that a group add a new member 235 /// 236 /// # Warning 237 /// 238 /// In order for the proposal generated by this function to be successfully 239 /// committed, the group needs to have `signing_identity` as an entry 240 /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) 241 /// as part of its group context extensions. 242 #[cfg(feature = "by_ref_proposal")] 243 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] propose_add( &mut self, key_package: MlsMessage, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>244 pub async fn propose_add( 245 &mut self, 246 key_package: MlsMessage, 247 authenticated_data: Vec<u8>, 248 ) -> Result<MlsMessage, MlsError> { 249 let key_package = key_package 250 .into_key_package() 251 .ok_or(MlsError::UnexpectedMessageType)?; 252 253 self.propose( 254 Proposal::Add(alloc::boxed::Box::new(AddProposal { key_package })), 255 authenticated_data, 256 ) 257 .await 258 } 259 260 /// Create an external proposal to request that a group remove an existing member 261 /// 262 /// # Warning 263 /// 264 /// In order for the proposal generated by this function to be successfully 265 /// committed, the group needs to have `signing_identity` as an entry 266 /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) 267 /// as part of its group context extensions. 268 #[cfg(feature = "by_ref_proposal")] 269 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] propose_remove( &mut self, index: u32, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>270 pub async fn propose_remove( 271 &mut self, 272 index: u32, 273 authenticated_data: Vec<u8>, 274 ) -> Result<MlsMessage, MlsError> { 275 let to_remove = LeafIndex(index); 276 277 // Verify that this leaf is actually in the tree 278 self.group_state().public_tree.get_leaf_node(to_remove)?; 279 280 self.propose( 281 Proposal::Remove(RemoveProposal { to_remove }), 282 authenticated_data, 283 ) 284 .await 285 } 286 287 /// Create an external proposal to request that a group inserts an external 288 /// pre shared key into its state. 289 /// 290 /// # Warning 291 /// 292 /// In order for the proposal generated by this function to be successfully 293 /// committed, the group needs to have `signing_identity` as an entry 294 /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) 295 /// as part of its group context extensions. 296 #[cfg(all(feature = "by_ref_proposal", feature = "psk"))] 297 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] propose_external_psk( &mut self, psk: ExternalPskId, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>298 pub async fn propose_external_psk( 299 &mut self, 300 psk: ExternalPskId, 301 authenticated_data: Vec<u8>, 302 ) -> Result<MlsMessage, MlsError> { 303 let proposal = self.psk_proposal(JustPreSharedKeyID::External(psk))?; 304 self.propose(proposal, authenticated_data).await 305 } 306 307 /// Create an external proposal to request that a group adds a pre shared key 308 /// from a previous epoch to the current group state. 309 /// 310 /// # Warning 311 /// 312 /// In order for the proposal generated by this function to be successfully 313 /// committed, the group needs to have `signing_identity` as an entry 314 /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) 315 /// as part of its group context extensions. 316 #[cfg(all(feature = "by_ref_proposal", feature = "psk"))] 317 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] propose_resumption_psk( &mut self, psk_epoch: u64, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>318 pub async fn propose_resumption_psk( 319 &mut self, 320 psk_epoch: u64, 321 authenticated_data: Vec<u8>, 322 ) -> Result<MlsMessage, MlsError> { 323 let key_id = ResumptionPsk { 324 psk_epoch, 325 usage: ResumptionPSKUsage::Application, 326 psk_group_id: PskGroupId(self.group_context().group_id().to_vec()), 327 }; 328 329 let proposal = self.psk_proposal(JustPreSharedKeyID::Resumption(key_id))?; 330 self.propose(proposal, authenticated_data).await 331 } 332 333 #[cfg(all(feature = "by_ref_proposal", feature = "psk"))] psk_proposal(&self, key_id: JustPreSharedKeyID) -> Result<Proposal, MlsError>334 fn psk_proposal(&self, key_id: JustPreSharedKeyID) -> Result<Proposal, MlsError> { 335 Ok(Proposal::Psk(PreSharedKeyProposal { 336 psk: PreSharedKeyID { 337 key_id, 338 psk_nonce: PskNonce::random(&self.cipher_suite_provider) 339 .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))?, 340 }, 341 })) 342 } 343 344 /// Create an external proposal to request that a group sets extensions stored in the group 345 /// state. 346 /// 347 /// # Warning 348 /// 349 /// In order for the proposal generated by this function to be successfully 350 /// committed, the group needs to have `signing_identity` as an entry 351 /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) 352 /// as part of its group context extensions. 353 #[cfg(feature = "by_ref_proposal")] 354 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] propose_group_context_extensions( &mut self, extensions: ExtensionList, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>355 pub async fn propose_group_context_extensions( 356 &mut self, 357 extensions: ExtensionList, 358 authenticated_data: Vec<u8>, 359 ) -> Result<MlsMessage, MlsError> { 360 let proposal = Proposal::GroupContextExtensions(extensions); 361 self.propose(proposal, authenticated_data).await 362 } 363 364 /// Create an external proposal to request that a group is reinitialized. 365 /// 366 /// # Warning 367 /// 368 /// In order for the proposal generated by this function to be successfully 369 /// committed, the group needs to have `signing_identity` as an entry 370 /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) 371 /// as part of its group context extensions. 372 #[cfg(feature = "by_ref_proposal")] 373 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] propose_reinit( &mut self, group_id: Option<Vec<u8>>, version: ProtocolVersion, cipher_suite: CipherSuite, extensions: ExtensionList, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>374 pub async fn propose_reinit( 375 &mut self, 376 group_id: Option<Vec<u8>>, 377 version: ProtocolVersion, 378 cipher_suite: CipherSuite, 379 extensions: ExtensionList, 380 authenticated_data: Vec<u8>, 381 ) -> Result<MlsMessage, MlsError> { 382 let group_id = group_id.map(Ok).unwrap_or_else(|| { 383 self.cipher_suite_provider 384 .random_bytes_vec(self.cipher_suite_provider.kdf_extract_size()) 385 .map_err(|e| MlsError::CryptoProviderError(e.into_any_error())) 386 })?; 387 388 let proposal = Proposal::ReInit(ReInitProposal { 389 group_id, 390 version, 391 cipher_suite, 392 extensions, 393 }); 394 395 self.propose(proposal, authenticated_data).await 396 } 397 398 /// Create a custom proposal message. 399 /// 400 /// # Warning 401 /// 402 /// In order for the proposal generated by this function to be successfully 403 /// committed, the group needs to have `signing_identity` as an entry 404 /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) 405 /// as part of its group context extensions. 406 #[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))] 407 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] propose_custom( &mut self, proposal: CustomProposal, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>408 pub async fn propose_custom( 409 &mut self, 410 proposal: CustomProposal, 411 authenticated_data: Vec<u8>, 412 ) -> Result<MlsMessage, MlsError> { 413 self.propose(Proposal::Custom(proposal), authenticated_data) 414 .await 415 } 416 417 #[cfg(feature = "by_ref_proposal")] 418 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] propose( &mut self, proposal: Proposal, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>419 async fn propose( 420 &mut self, 421 proposal: Proposal, 422 authenticated_data: Vec<u8>, 423 ) -> Result<MlsMessage, MlsError> { 424 let (signer, signing_identity) = 425 self.signing_data.as_ref().ok_or(MlsError::SignerNotFound)?; 426 427 let external_senders_ext = self 428 .state 429 .context 430 .extensions 431 .get_as::<ExternalSendersExt>()? 432 .ok_or(MlsError::ExternalProposalsDisabled)?; 433 434 let sender_index = external_senders_ext 435 .allowed_senders 436 .iter() 437 .position(|allowed_signer| signing_identity == allowed_signer) 438 .ok_or(MlsError::InvalidExternalSigningIdentity)?; 439 440 let sender = Sender::External(sender_index as u32); 441 442 let auth_content = AuthenticatedContent::new_signed( 443 &self.cipher_suite_provider, 444 &self.state.context, 445 sender, 446 Content::Proposal(Box::new(proposal.clone())), 447 signer, 448 WireFormat::PublicMessage, 449 authenticated_data, 450 ) 451 .await?; 452 453 self.state.proposals.insert( 454 ProposalRef::from_content(&self.cipher_suite_provider, &auth_content).await?, 455 proposal, 456 sender, 457 ); 458 459 let plaintext = PublicMessage { 460 content: auth_content.content, 461 auth: auth_content.auth, 462 membership_tag: None, 463 }; 464 465 Ok(MlsMessage::new( 466 self.group_context().version(), 467 MlsMessagePayload::Plain(plaintext), 468 )) 469 } 470 471 /// Delete all sent and received proposals cached for commit. 472 #[cfg(feature = "by_ref_proposal")] clear_proposal_cache(&mut self)473 pub fn clear_proposal_cache(&mut self) { 474 self.state.proposals.clear() 475 } 476 477 #[inline(always)] group_state(&self) -> &GroupState478 pub(crate) fn group_state(&self) -> &GroupState { 479 &self.state 480 } 481 482 /// Get the current group context summarizing various information about the group. 483 #[inline(always)] group_context(&self) -> &GroupContext484 pub fn group_context(&self) -> &GroupContext { 485 &self.group_state().context 486 } 487 488 /// Export the current ratchet tree used within the group. export_tree(&self) -> Result<Vec<u8>, MlsError>489 pub fn export_tree(&self) -> Result<Vec<u8>, MlsError> { 490 self.group_state() 491 .public_tree 492 .nodes 493 .mls_encode_to_vec() 494 .map_err(Into::into) 495 } 496 497 /// Get the current roster of the group. 498 #[inline(always)] roster(&self) -> Roster499 pub fn roster(&self) -> Roster { 500 self.group_state().public_tree.roster() 501 } 502 503 /// Get the 504 /// [transcript hash](https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol.html#name-transcript-hashes) 505 /// for the current epoch that the group is in. 506 #[inline(always)] transcript_hash(&self) -> &Vec<u8>507 pub fn transcript_hash(&self) -> &Vec<u8> { 508 &self.group_state().context.confirmed_transcript_hash 509 } 510 511 /// Get the 512 /// [tree hash](https://www.rfc-editor.org/rfc/rfc9420.html#name-tree-hashes) 513 /// for the current epoch that the group is in. 514 #[inline(always)] tree_hash(&self) -> &[u8]515 pub fn tree_hash(&self) -> &[u8] { 516 &self.group_state().context.tree_hash 517 } 518 519 /// Find a member based on their identity. 520 /// 521 /// Identities are matched based on the 522 /// [IdentityProvider](crate::IdentityProvider) 523 /// that this group was configured with. 524 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] get_member_with_identity( &self, identity_id: &SigningIdentity, ) -> Result<Member, MlsError>525 pub async fn get_member_with_identity( 526 &self, 527 identity_id: &SigningIdentity, 528 ) -> Result<Member, MlsError> { 529 let identity = self 530 .identity_provider() 531 .identity(identity_id, self.group_context().extensions()) 532 .await 533 .map_err(|error| MlsError::IdentityProviderError(error.into_any_error()))?; 534 535 let tree = &self.group_state().public_tree; 536 537 #[cfg(feature = "tree_index")] 538 let index = tree.get_leaf_node_with_identity(&identity); 539 540 #[cfg(not(feature = "tree_index"))] 541 let index = tree 542 .get_leaf_node_with_identity( 543 &identity, 544 &self.identity_provider(), 545 self.group_context().extensions(), 546 ) 547 .await?; 548 549 let index = index.ok_or(MlsError::MemberNotFound)?; 550 let node = self.group_state().public_tree.get_leaf_node(index)?; 551 552 Ok(member_from_leaf_node(node, index)) 553 } 554 } 555 556 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] 557 #[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))] 558 #[cfg_attr( 559 all(not(target_arch = "wasm32"), mls_build_async), 560 maybe_async::must_be_async 561 )] 562 impl<C> MessageProcessor for ExternalGroup<C> 563 where 564 C: ExternalClientConfig + Clone, 565 { 566 type MlsRules = C::MlsRules; 567 type IdentityProvider = C::IdentityProvider; 568 type PreSharedKeyStorage = AlwaysFoundPskStorage; 569 type OutputType = ExternalReceivedMessage; 570 type CipherSuiteProvider = <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider; 571 572 #[cfg(feature = "private_message")] self_index(&self) -> Option<LeafIndex>573 fn self_index(&self) -> Option<LeafIndex> { 574 None 575 } 576 mls_rules(&self) -> Self::MlsRules577 fn mls_rules(&self) -> Self::MlsRules { 578 self.config.mls_rules() 579 } 580 581 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] verify_plaintext_authentication( &self, message: PublicMessage, ) -> Result<EventOrContent<Self::OutputType>, MlsError>582 async fn verify_plaintext_authentication( 583 &self, 584 message: PublicMessage, 585 ) -> Result<EventOrContent<Self::OutputType>, MlsError> { 586 let auth_content = crate::group::message_verifier::verify_plaintext_authentication( 587 &self.cipher_suite_provider, 588 message, 589 None, 590 None, 591 &self.state, 592 ) 593 .await?; 594 595 Ok(EventOrContent::Content(auth_content)) 596 } 597 598 #[cfg(feature = "private_message")] process_ciphertext( &mut self, cipher_text: &PrivateMessage, ) -> Result<EventOrContent<Self::OutputType>, MlsError>599 async fn process_ciphertext( 600 &mut self, 601 cipher_text: &PrivateMessage, 602 ) -> Result<EventOrContent<Self::OutputType>, MlsError> { 603 Ok(EventOrContent::Event(ExternalReceivedMessage::Ciphertext( 604 cipher_text.content_type, 605 ))) 606 } 607 update_key_schedule( &mut self, _secrets: Option<(TreeKemPrivate, PathSecret)>, interim_transcript_hash: InterimTranscriptHash, confirmation_tag: &ConfirmationTag, provisional_public_state: ProvisionalState, ) -> Result<(), MlsError>608 async fn update_key_schedule( 609 &mut self, 610 _secrets: Option<(TreeKemPrivate, PathSecret)>, 611 interim_transcript_hash: InterimTranscriptHash, 612 confirmation_tag: &ConfirmationTag, 613 provisional_public_state: ProvisionalState, 614 ) -> Result<(), MlsError> { 615 self.state.context = provisional_public_state.group_context; 616 #[cfg(feature = "by_ref_proposal")] 617 self.state.proposals.clear(); 618 self.state.interim_transcript_hash = interim_transcript_hash; 619 self.state.public_tree = provisional_public_state.public_tree; 620 self.state.confirmation_tag = confirmation_tag.clone(); 621 622 Ok(()) 623 } 624 identity_provider(&self) -> Self::IdentityProvider625 fn identity_provider(&self) -> Self::IdentityProvider { 626 self.config.identity_provider() 627 } 628 psk_storage(&self) -> Self::PreSharedKeyStorage629 fn psk_storage(&self) -> Self::PreSharedKeyStorage { 630 AlwaysFoundPskStorage 631 } 632 group_state(&self) -> &GroupState633 fn group_state(&self) -> &GroupState { 634 &self.state 635 } 636 group_state_mut(&mut self) -> &mut GroupState637 fn group_state_mut(&mut self) -> &mut GroupState { 638 &mut self.state 639 } 640 can_continue_processing(&self, _provisional_state: &ProvisionalState) -> bool641 fn can_continue_processing(&self, _provisional_state: &ProvisionalState) -> bool { 642 true 643 } 644 645 #[cfg(feature = "private_message")] min_epoch_available(&self) -> Option<u64>646 fn min_epoch_available(&self) -> Option<u64> { 647 self.config 648 .max_epoch_jitter() 649 .map(|j| self.state.context.epoch - j) 650 } 651 cipher_suite_provider(&self) -> &Self::CipherSuiteProvider652 fn cipher_suite_provider(&self) -> &Self::CipherSuiteProvider { 653 &self.cipher_suite_provider 654 } 655 } 656 657 /// Serializable snapshot of an [ExternalGroup](ExternalGroup) state. 658 #[derive(Debug, MlsEncode, MlsSize, MlsDecode, PartialEq, Clone)] 659 pub struct ExternalSnapshot { 660 version: u16, 661 state: RawGroupState, 662 signing_data: Option<(SignatureSecretKey, SigningIdentity)>, 663 } 664 665 impl ExternalSnapshot { 666 /// Serialize the snapshot to_bytes(&self) -> Result<Vec<u8>, MlsError>667 pub fn to_bytes(&self) -> Result<Vec<u8>, MlsError> { 668 Ok(self.mls_encode_to_vec()?) 669 } 670 671 /// Deserialize the snapshot from_bytes(bytes: &[u8]) -> Result<Self, MlsError>672 pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> { 673 Ok(Self::mls_decode(&mut &*bytes)?) 674 } 675 } 676 677 impl<C> ExternalGroup<C> 678 where 679 C: ExternalClientConfig + Clone, 680 { 681 /// Create a snapshot of this group's current internal state. snapshot(&self) -> ExternalSnapshot682 pub fn snapshot(&self) -> ExternalSnapshot { 683 ExternalSnapshot { 684 state: RawGroupState::export(self.group_state()), 685 version: 1, 686 signing_data: self.signing_data.clone(), 687 } 688 } 689 690 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] from_snapshot( config: C, snapshot: ExternalSnapshot, ) -> Result<Self, MlsError>691 pub(crate) async fn from_snapshot( 692 config: C, 693 snapshot: ExternalSnapshot, 694 ) -> Result<Self, MlsError> { 695 #[cfg(feature = "tree_index")] 696 let identity_provider = config.identity_provider(); 697 698 let cipher_suite_provider = cipher_suite_provider( 699 config.crypto_provider(), 700 snapshot.state.context.cipher_suite, 701 )?; 702 703 Ok(ExternalGroup { 704 config, 705 signing_data: snapshot.signing_data, 706 state: snapshot 707 .state 708 .import( 709 #[cfg(feature = "tree_index")] 710 &identity_provider, 711 ) 712 .await?, 713 cipher_suite_provider, 714 }) 715 } 716 } 717 718 impl From<CommitMessageDescription> for ExternalReceivedMessage { from(value: CommitMessageDescription) -> Self719 fn from(value: CommitMessageDescription) -> Self { 720 ExternalReceivedMessage::Commit(value) 721 } 722 } 723 724 impl TryFrom<ApplicationMessageDescription> for ExternalReceivedMessage { 725 type Error = MlsError; 726 try_from(_: ApplicationMessageDescription) -> Result<Self, Self::Error>727 fn try_from(_: ApplicationMessageDescription) -> Result<Self, Self::Error> { 728 Err(MlsError::UnencryptedApplicationMessage) 729 } 730 } 731 732 impl From<ProposalMessageDescription> for ExternalReceivedMessage { from(value: ProposalMessageDescription) -> Self733 fn from(value: ProposalMessageDescription) -> Self { 734 ExternalReceivedMessage::Proposal(value) 735 } 736 } 737 738 impl From<GroupInfo> for ExternalReceivedMessage { from(value: GroupInfo) -> Self739 fn from(value: GroupInfo) -> Self { 740 ExternalReceivedMessage::GroupInfo(value) 741 } 742 } 743 744 impl From<Welcome> for ExternalReceivedMessage { from(_: Welcome) -> Self745 fn from(_: Welcome) -> Self { 746 ExternalReceivedMessage::Welcome 747 } 748 } 749 750 impl From<KeyPackage> for ExternalReceivedMessage { from(value: KeyPackage) -> Self751 fn from(value: KeyPackage) -> Self { 752 ExternalReceivedMessage::KeyPackage(value) 753 } 754 } 755 756 #[cfg(test)] 757 pub(crate) mod test_utils { 758 use crate::{ 759 external_client::tests_utils::{TestExternalClientBuilder, TestExternalClientConfig}, 760 group::test_utils::TestGroup, 761 }; 762 763 use super::ExternalGroup; 764 765 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] make_external_group( group: &TestGroup, ) -> ExternalGroup<TestExternalClientConfig>766 pub(crate) async fn make_external_group( 767 group: &TestGroup, 768 ) -> ExternalGroup<TestExternalClientConfig> { 769 make_external_group_with_config( 770 group, 771 TestExternalClientBuilder::new_for_test().build_config(), 772 ) 773 .await 774 } 775 776 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] make_external_group_with_config( group: &TestGroup, config: TestExternalClientConfig, ) -> ExternalGroup<TestExternalClientConfig>777 pub(crate) async fn make_external_group_with_config( 778 group: &TestGroup, 779 config: TestExternalClientConfig, 780 ) -> ExternalGroup<TestExternalClientConfig> { 781 ExternalGroup::join( 782 config, 783 None, 784 group 785 .group 786 .group_info_message_allowing_ext_commit(true) 787 .await 788 .unwrap(), 789 None, 790 ) 791 .await 792 .unwrap() 793 } 794 } 795 796 #[cfg(test)] 797 mod tests { 798 use super::test_utils::make_external_group; 799 use crate::{ 800 cipher_suite::CipherSuite, 801 client::{ 802 test_utils::{TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION}, 803 MlsError, 804 }, 805 crypto::{test_utils::TestCryptoProvider, SignatureSecretKey}, 806 extension::ExternalSendersExt, 807 external_client::{ 808 group::test_utils::make_external_group_with_config, 809 tests_utils::{TestExternalClientBuilder, TestExternalClientConfig}, 810 ExternalGroup, ExternalReceivedMessage, ExternalSnapshot, 811 }, 812 group::{ 813 framing::{Content, MlsMessagePayload}, 814 proposal::{AddProposal, Proposal, ProposalOrRef}, 815 proposal_ref::ProposalRef, 816 test_utils::{test_group, TestGroup}, 817 ProposalMessageDescription, 818 }, 819 identity::{test_utils::get_test_signing_identity, SigningIdentity}, 820 key_package::test_utils::{test_key_package, test_key_package_message}, 821 protocol_version::ProtocolVersion, 822 ExtensionList, MlsMessage, 823 }; 824 use assert_matches::assert_matches; 825 use mls_rs_codec::{MlsDecode, MlsEncode}; 826 827 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] test_group_with_one_commit(v: ProtocolVersion, cs: CipherSuite) -> TestGroup828 async fn test_group_with_one_commit(v: ProtocolVersion, cs: CipherSuite) -> TestGroup { 829 let mut group = test_group(v, cs).await; 830 group.group.commit(Vec::new()).await.unwrap(); 831 group.process_pending_commit().await.unwrap(); 832 group 833 } 834 835 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] test_group_two_members( v: ProtocolVersion, cs: CipherSuite, #[cfg(feature = "by_ref_proposal")] ext_identity: Option<SigningIdentity>, ) -> TestGroup836 async fn test_group_two_members( 837 v: ProtocolVersion, 838 cs: CipherSuite, 839 #[cfg(feature = "by_ref_proposal")] ext_identity: Option<SigningIdentity>, 840 ) -> TestGroup { 841 let mut group = test_group_with_one_commit(v, cs).await; 842 843 let bob_key_package = test_key_package_message(v, cs, "bob").await; 844 845 let mut commit_builder = group 846 .group 847 .commit_builder() 848 .add_member(bob_key_package) 849 .unwrap(); 850 851 #[cfg(feature = "by_ref_proposal")] 852 if let Some(ext_signer) = ext_identity { 853 let mut ext_list = ExtensionList::new(); 854 855 ext_list 856 .set_from(ExternalSendersExt { 857 allowed_senders: vec![ext_signer], 858 }) 859 .unwrap(); 860 861 commit_builder = commit_builder.set_group_context_ext(ext_list).unwrap(); 862 } 863 864 commit_builder.build().await.unwrap(); 865 866 group.process_pending_commit().await.unwrap(); 867 group 868 } 869 870 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_can_be_created()871 async fn external_group_can_be_created() { 872 for (v, cs) in ProtocolVersion::all().flat_map(|v| { 873 TestCryptoProvider::all_supported_cipher_suites() 874 .into_iter() 875 .map(move |cs| (v, cs)) 876 }) { 877 make_external_group(&test_group_with_one_commit(v, cs).await).await; 878 } 879 } 880 881 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_can_process_commit()882 async fn external_group_can_process_commit() { 883 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 884 let mut server = make_external_group(&alice).await; 885 let commit_output = alice.group.commit(Vec::new()).await.unwrap(); 886 alice.group.apply_pending_commit().await.unwrap(); 887 888 server 889 .process_incoming_message(commit_output.commit_message) 890 .await 891 .unwrap(); 892 893 assert_eq!(alice.group.state, server.state); 894 } 895 896 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_can_process_proposals_by_reference()897 async fn external_group_can_process_proposals_by_reference() { 898 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 899 let mut server = make_external_group(&alice).await; 900 901 let bob_key_package = 902 test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await; 903 904 let add_proposal = Proposal::Add(Box::new(AddProposal { 905 key_package: bob_key_package, 906 })); 907 908 let packet = alice.propose(add_proposal.clone()).await; 909 910 let proposal_process = server.process_incoming_message(packet).await.unwrap(); 911 912 assert_matches!( 913 proposal_process, 914 ExternalReceivedMessage::Proposal(ProposalMessageDescription { ref proposal, ..}) if proposal == &add_proposal 915 ); 916 917 let commit_output = alice.group.commit(vec![]).await.unwrap(); 918 alice.group.apply_pending_commit().await.unwrap(); 919 920 let commit_result = server 921 .process_incoming_message(commit_output.commit_message) 922 .await 923 .unwrap(); 924 925 #[cfg(feature = "state_update")] 926 assert_matches!( 927 commit_result, 928 ExternalReceivedMessage::Commit(commit_description) 929 if commit_description.state_update.roster_update.added().iter().any(|added| added.index == 1) 930 ); 931 932 #[cfg(not(feature = "state_update"))] 933 assert_matches!(commit_result, ExternalReceivedMessage::Commit(_)); 934 935 assert_eq!(alice.group.state, server.state); 936 } 937 938 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_can_process_commit_adding_member()939 async fn external_group_can_process_commit_adding_member() { 940 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 941 let mut server = make_external_group(&alice).await; 942 let (_, commit) = alice.join("bob").await; 943 944 let update = match server.process_incoming_message(commit).await.unwrap() { 945 ExternalReceivedMessage::Commit(update) => update.state_update, 946 _ => panic!("Expected processed commit"), 947 }; 948 949 #[cfg(feature = "state_update")] 950 assert_eq!(update.roster_update.added().len(), 1); 951 952 assert_eq!(server.state.public_tree.get_leaf_nodes().len(), 2); 953 954 assert_eq!(alice.group.state, server.state); 955 } 956 957 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_rejects_commit_not_for_current_epoch()958 async fn external_group_rejects_commit_not_for_current_epoch() { 959 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 960 let mut server = make_external_group(&alice).await; 961 962 let mut commit_output = alice.group.commit(vec![]).await.unwrap(); 963 964 match commit_output.commit_message.payload { 965 MlsMessagePayload::Plain(ref mut plain) => plain.content.epoch = 0, 966 _ => panic!("Unexpected non-plaintext data"), 967 }; 968 969 let res = server 970 .process_incoming_message(commit_output.commit_message) 971 .await; 972 973 assert_matches!(res, Err(MlsError::InvalidEpoch)); 974 } 975 976 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_can_reject_message_with_invalid_signature()977 async fn external_group_can_reject_message_with_invalid_signature() { 978 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 979 980 let mut server = make_external_group_with_config( 981 &alice, 982 TestExternalClientBuilder::new_for_test().build_config(), 983 ) 984 .await; 985 986 let mut commit_output = alice.group.commit(Vec::new()).await.unwrap(); 987 988 match commit_output.commit_message.payload { 989 MlsMessagePayload::Plain(ref mut plain) => plain.auth.signature = Vec::new().into(), 990 _ => panic!("Unexpected non-plaintext data"), 991 }; 992 993 let res = server 994 .process_incoming_message(commit_output.commit_message) 995 .await; 996 997 assert_matches!(res, Err(MlsError::InvalidSignature)); 998 } 999 1000 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_rejects_unencrypted_application_message()1001 async fn external_group_rejects_unencrypted_application_message() { 1002 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 1003 let mut server = make_external_group(&alice).await; 1004 1005 let plaintext = alice 1006 .make_plaintext(Content::Application(b"hello".to_vec().into())) 1007 .await; 1008 1009 let res = server.process_incoming_message(plaintext).await; 1010 1011 assert_matches!(res, Err(MlsError::UnencryptedApplicationMessage)); 1012 } 1013 1014 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_will_reject_unsupported_cipher_suites()1015 async fn external_group_will_reject_unsupported_cipher_suites() { 1016 let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 1017 1018 let config = 1019 TestExternalClientBuilder::new_for_test_disabling_cipher_suite(TEST_CIPHER_SUITE) 1020 .build_config(); 1021 1022 let res = ExternalGroup::join( 1023 config, 1024 None, 1025 alice 1026 .group 1027 .group_info_message_allowing_ext_commit(true) 1028 .await 1029 .unwrap(), 1030 None, 1031 ) 1032 .await 1033 .map(|_| ()); 1034 1035 assert_matches!( 1036 res, 1037 Err(MlsError::UnsupportedCipherSuite(TEST_CIPHER_SUITE)) 1038 ); 1039 } 1040 1041 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_will_reject_unsupported_protocol_versions()1042 async fn external_group_will_reject_unsupported_protocol_versions() { 1043 let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 1044 1045 let config = TestExternalClientBuilder::new_for_test().build_config(); 1046 1047 let mut group_info = alice 1048 .group 1049 .group_info_message_allowing_ext_commit(true) 1050 .await 1051 .unwrap(); 1052 1053 group_info.version = ProtocolVersion::from(64); 1054 1055 let res = ExternalGroup::join(config, None, group_info, None) 1056 .await 1057 .map(|_| ()); 1058 1059 assert_matches!( 1060 res, 1061 Err(MlsError::UnsupportedProtocolVersion(v)) if v == 1062 ProtocolVersion::from(64) 1063 ); 1064 } 1065 1066 #[cfg(feature = "by_ref_proposal")] 1067 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] setup_extern_proposal_test( extern_proposals_allowed: bool, ) -> (SigningIdentity, SignatureSecretKey, TestGroup)1068 async fn setup_extern_proposal_test( 1069 extern_proposals_allowed: bool, 1070 ) -> (SigningIdentity, SignatureSecretKey, TestGroup) { 1071 let (server_identity, server_key) = 1072 get_test_signing_identity(TEST_CIPHER_SUITE, b"server").await; 1073 1074 let alice = test_group_two_members( 1075 TEST_PROTOCOL_VERSION, 1076 TEST_CIPHER_SUITE, 1077 extern_proposals_allowed.then(|| server_identity.clone()), 1078 ) 1079 .await; 1080 1081 (server_identity, server_key, alice) 1082 } 1083 1084 #[cfg(feature = "by_ref_proposal")] 1085 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] test_external_proposal( server: &mut ExternalGroup<TestExternalClientConfig>, alice: &mut TestGroup, external_proposal: MlsMessage, )1086 async fn test_external_proposal( 1087 server: &mut ExternalGroup<TestExternalClientConfig>, 1088 alice: &mut TestGroup, 1089 external_proposal: MlsMessage, 1090 ) { 1091 let auth_content = external_proposal.clone().into_plaintext().unwrap().into(); 1092 1093 let proposal_ref = ProposalRef::from_content(&server.cipher_suite_provider, &auth_content) 1094 .await 1095 .unwrap(); 1096 1097 // Alice receives the proposal 1098 alice.process_message(external_proposal).await.unwrap(); 1099 1100 // Alice commits the proposal 1101 let commit_output = alice.group.commit(vec![]).await.unwrap(); 1102 1103 let commit = match commit_output 1104 .commit_message 1105 .clone() 1106 .into_plaintext() 1107 .unwrap() 1108 .content 1109 .content 1110 { 1111 Content::Commit(commit) => commit, 1112 _ => panic!("not a commit"), 1113 }; 1114 1115 // The proposal should be in the resulting commit 1116 assert!(commit 1117 .proposals 1118 .contains(&ProposalOrRef::Reference(proposal_ref))); 1119 1120 alice.process_pending_commit().await.unwrap(); 1121 1122 server 1123 .process_incoming_message(commit_output.commit_message) 1124 .await 1125 .unwrap(); 1126 1127 assert_eq!(alice.group.state, server.state); 1128 } 1129 1130 #[cfg(feature = "by_ref_proposal")] 1131 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_can_propose_add()1132 async fn external_group_can_propose_add() { 1133 let (server_identity, server_key, mut alice) = setup_extern_proposal_test(true).await; 1134 1135 let mut server = make_external_group(&alice).await; 1136 1137 server.signing_data = Some((server_key, server_identity)); 1138 1139 let charlie_key_package = 1140 test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "charlie").await; 1141 1142 let external_proposal = server 1143 .propose_add(charlie_key_package, vec![]) 1144 .await 1145 .unwrap(); 1146 1147 test_external_proposal(&mut server, &mut alice, external_proposal).await 1148 } 1149 1150 #[cfg(feature = "by_ref_proposal")] 1151 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_can_propose_remove()1152 async fn external_group_can_propose_remove() { 1153 let (server_identity, server_key, mut alice) = setup_extern_proposal_test(true).await; 1154 1155 let mut server = make_external_group(&alice).await; 1156 1157 server.signing_data = Some((server_key, server_identity)); 1158 1159 let external_proposal = server.propose_remove(1, vec![]).await.unwrap(); 1160 1161 test_external_proposal(&mut server, &mut alice, external_proposal).await 1162 } 1163 1164 #[cfg(feature = "by_ref_proposal")] 1165 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_external_proposal_not_allowed()1166 async fn external_group_external_proposal_not_allowed() { 1167 let (signing_id, secret_key, alice) = setup_extern_proposal_test(false).await; 1168 let mut server = make_external_group(&alice).await; 1169 1170 server.signing_data = Some((secret_key, signing_id)); 1171 1172 let charlie_key_package = 1173 test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "charlie").await; 1174 1175 let res = server.propose_add(charlie_key_package, vec![]).await; 1176 1177 assert_matches!(res, Err(MlsError::ExternalProposalsDisabled)); 1178 } 1179 1180 #[cfg(feature = "by_ref_proposal")] 1181 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_external_signing_identity_invalid()1182 async fn external_group_external_signing_identity_invalid() { 1183 let (server_identity, server_key) = 1184 get_test_signing_identity(TEST_CIPHER_SUITE, b"server").await; 1185 1186 let alice = test_group_two_members( 1187 TEST_PROTOCOL_VERSION, 1188 TEST_CIPHER_SUITE, 1189 Some( 1190 get_test_signing_identity(TEST_CIPHER_SUITE, b"not server") 1191 .await 1192 .0, 1193 ), 1194 ) 1195 .await; 1196 1197 let mut server = make_external_group(&alice).await; 1198 1199 server.signing_data = Some((server_key, server_identity)); 1200 1201 let res = server.propose_remove(1, vec![]).await; 1202 1203 assert_matches!(res, Err(MlsError::InvalidExternalSigningIdentity)); 1204 } 1205 1206 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_errors_on_old_epoch()1207 async fn external_group_errors_on_old_epoch() { 1208 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 1209 1210 let mut server = make_external_group_with_config( 1211 &alice, 1212 TestExternalClientBuilder::new_for_test() 1213 .max_epoch_jitter(0) 1214 .build_config(), 1215 ) 1216 .await; 1217 1218 let old_application_msg = alice 1219 .group 1220 .encrypt_application_message(&[], vec![]) 1221 .await 1222 .unwrap(); 1223 1224 let commit_output = alice.group.commit(vec![]).await.unwrap(); 1225 1226 server 1227 .process_incoming_message(commit_output.commit_message) 1228 .await 1229 .unwrap(); 1230 1231 let res = server.process_incoming_message(old_application_msg).await; 1232 1233 assert_matches!(res, Err(MlsError::InvalidEpoch)); 1234 } 1235 1236 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] proposals_can_be_cached_externally()1237 async fn proposals_can_be_cached_externally() { 1238 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 1239 1240 let mut server = make_external_group_with_config( 1241 &alice, 1242 TestExternalClientBuilder::new_for_test() 1243 .cache_proposals(false) 1244 .build_config(), 1245 ) 1246 .await; 1247 1248 let proposal = alice.group.propose_update(vec![]).await.unwrap(); 1249 1250 let commit_output = alice.group.commit(vec![]).await.unwrap(); 1251 1252 server 1253 .process_incoming_message(proposal.clone()) 1254 .await 1255 .unwrap(); 1256 1257 server.insert_proposal_from_message(proposal).await.unwrap(); 1258 1259 server 1260 .process_incoming_message(commit_output.commit_message) 1261 .await 1262 .unwrap(); 1263 } 1264 1265 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_can_observe_since_creation()1266 async fn external_group_can_observe_since_creation() { 1267 let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 1268 1269 let info = alice 1270 .group 1271 .group_info_message_allowing_ext_commit(true) 1272 .await 1273 .unwrap(); 1274 1275 let config = TestExternalClientBuilder::new_for_test().build_config(); 1276 let mut server = ExternalGroup::join(config, None, info, None).await.unwrap(); 1277 1278 for _ in 0..2 { 1279 let commit = alice.group.commit(vec![]).await.unwrap().commit_message; 1280 alice.process_pending_commit().await.unwrap(); 1281 server.process_incoming_message(commit).await.unwrap(); 1282 } 1283 } 1284 1285 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_can_be_serialized_to_tls_encoding()1286 async fn external_group_can_be_serialized_to_tls_encoding() { 1287 let server = 1288 make_external_group(&test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await).await; 1289 1290 let snapshot = server.snapshot().mls_encode_to_vec().unwrap(); 1291 let snapshot_restored = ExternalSnapshot::mls_decode(&mut snapshot.as_slice()).unwrap(); 1292 1293 let server_restored = 1294 ExternalGroup::from_snapshot(server.config.clone(), snapshot_restored) 1295 .await 1296 .unwrap(); 1297 1298 assert_eq!(server.group_state(), server_restored.group_state()); 1299 } 1300 1301 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_can_validate_info()1302 async fn external_group_can_validate_info() { 1303 let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 1304 let mut server = make_external_group(&alice).await; 1305 1306 let info = alice 1307 .group 1308 .group_info_message_allowing_ext_commit(false) 1309 .await 1310 .unwrap(); 1311 1312 let update = server.process_incoming_message(info.clone()).await.unwrap(); 1313 let info = info.into_group_info().unwrap(); 1314 1315 assert_matches!(update, ExternalReceivedMessage::GroupInfo(update_info) if update_info == info); 1316 } 1317 1318 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_can_validate_key_package()1319 async fn external_group_can_validate_key_package() { 1320 let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 1321 let mut server = make_external_group(&alice).await; 1322 1323 let kp = test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "john").await; 1324 1325 let update = server.process_incoming_message(kp.clone()).await.unwrap(); 1326 let kp = kp.into_key_package().unwrap(); 1327 1328 assert_matches!(update, ExternalReceivedMessage::KeyPackage(update_kp) if update_kp == kp); 1329 } 1330 1331 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] external_group_can_validate_welcome()1332 async fn external_group_can_validate_welcome() { 1333 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; 1334 let mut server = make_external_group(&alice).await; 1335 1336 let [welcome] = alice 1337 .group 1338 .commit_builder() 1339 .add_member( 1340 test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "john").await, 1341 ) 1342 .unwrap() 1343 .build() 1344 .await 1345 .unwrap() 1346 .welcome_messages 1347 .try_into() 1348 .unwrap(); 1349 1350 let update = server.process_incoming_message(welcome).await.unwrap(); 1351 1352 assert_matches!(update, ExternalReceivedMessage::Welcome); 1353 } 1354 } 1355