1use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
6use mls_rs_core::time::MlsTime;
7use mls_rs_core::{
8 crypto::SignatureSecretKey, error::IntoAnyError, extension::ExtensionList, group::Member,
9 identity::IdentityProvider,
10};
11
12use crate::{
13 cipher_suite::CipherSuite,
14 client::MlsError,
15 external_client::ExternalClientConfig,
16 group::{
17 cipher_suite_provider,
18 confirmation_tag::ConfirmationTag,
19 framing::PublicMessage,
20 member_from_leaf_node,
21 message_processor::{
22 ApplicationMessageDescription, CommitMessageDescription, EventOrContent,
23 MessageProcessor, ProposalMessageDescription, ProvisionalState,
24 },
25 proposal::RemoveProposal,
26 proposal_filter::ProposalInfo,
27 snapshot::RawGroupState,
28 state::GroupState,
29 transcript_hash::InterimTranscriptHash,
30 validate_tree_and_info_joiner, ContentType, ExportedTree, GroupContext, GroupInfo, Roster,
31 Welcome,
32 },
33 identity::SigningIdentity,
34 protocol_version::ProtocolVersion,
35 psk::AlwaysFoundPskStorage,
36 tree_kem::{node::LeafIndex, path_secret::PathSecret, TreeKemPrivate},
37 CryptoProvider, KeyPackage, MlsMessage,
38};
39
40#[cfg(all(
41 feature = "by_ref_proposal",
42 feature = "custom_proposal",
43 feature = "self_remove_proposal"
44))]
45use crate::group::proposal::SelfRemoveProposal;
46
47#[cfg(feature = "by_ref_proposal")]
48use crate::{
49 group::{
50 framing::{Content, MlsMessagePayload},
51 message_processor::CachedProposal,
52 message_signature::AuthenticatedContent,
53 proposal::Proposal,
54 proposal_ref::ProposalRef,
55 Sender,
56 },
57 WireFormat,
58};
59
60#[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))]
61use crate::group::proposal::CustomProposal;
62
63#[cfg(feature = "by_ref_proposal")]
64use mls_rs_core::{crypto::CipherSuiteProvider, psk::ExternalPskId};
65
66#[cfg(feature = "by_ref_proposal")]
67use crate::{
68 extension::ExternalSendersExt,
69 group::proposal::{AddProposal, ReInitProposal},
70};
71
72#[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
73use crate::{
74 group::proposal::PreSharedKeyProposal,
75 psk::{
76 JustPreSharedKeyID, PreSharedKeyID, PskGroupId, PskNonce, ResumptionPSKUsage, ResumptionPsk,
77 },
78};
79
80#[cfg(feature = "private_message")]
81use crate::group::framing::PrivateMessage;
82
83use alloc::boxed::Box;
84
85#[derive(Clone, Debug)]
88#[allow(clippy::large_enum_variant)]
89pub enum ExternalReceivedMessage {
90 Commit(CommitMessageDescription),
92 Proposal(ProposalMessageDescription),
94 Ciphertext(ContentType),
96 GroupInfo(GroupInfo),
98 Welcome,
100 KeyPackage(KeyPackage),
102}
103
104#[derive(Clone)]
107pub struct ExternalGroup<C>
108where
109 C: ExternalClientConfig,
110{
111 pub(crate) config: C,
112 pub(crate) cipher_suite_provider: <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider,
113 pub(crate) state: GroupState,
114 pub(crate) signing_data: Option<(SignatureSecretKey, SigningIdentity)>,
115}
116
117impl<C: ExternalClientConfig + Clone> ExternalGroup<C> {
118 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
119 pub(crate) async fn join(
120 config: C,
121 signing_data: Option<(SignatureSecretKey, SigningIdentity)>,
122 group_info: MlsMessage,
123 tree_data: Option<ExportedTree<'_>>,
124 maybe_time: Option<MlsTime>,
125 ) -> Result<Self, MlsError> {
126 let protocol_version = group_info.version;
127
128 if !config.version_supported(protocol_version) {
129 return Err(MlsError::UnsupportedProtocolVersion(protocol_version));
130 }
131
132 let group_info = group_info
133 .into_group_info()
134 .ok_or(MlsError::UnexpectedMessageType)?;
135
136 let cipher_suite_provider = cipher_suite_provider(
137 config.crypto_provider(),
138 group_info.group_context.cipher_suite,
139 )?;
140
141 let public_tree = validate_tree_and_info_joiner(
142 protocol_version,
143 &group_info,
144 tree_data,
145 &config.identity_provider(),
146 &cipher_suite_provider,
147 maybe_time,
148 )
149 .await?;
150
151 let interim_transcript_hash = InterimTranscriptHash::create(
152 &cipher_suite_provider,
153 &group_info.group_context.confirmed_transcript_hash,
154 &group_info.confirmation_tag,
155 )
156 .await?;
157
158 Ok(Self {
159 config,
160 signing_data,
161 state: GroupState::new(
162 group_info.group_context,
163 public_tree,
164 interim_transcript_hash,
165 group_info.confirmation_tag,
166 ),
167 cipher_suite_provider,
168 })
169 }
170
171 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
191 pub async fn process_incoming_message(
192 &mut self,
193 message: MlsMessage,
194 ) -> Result<ExternalReceivedMessage, MlsError> {
195 MessageProcessor::process_incoming_message(
196 self,
197 message,
198 #[cfg(feature = "by_ref_proposal")]
199 self.config.cache_proposals(),
200 )
201 .await
202 }
203
204 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
217 pub async fn process_incoming_message_with_time(
218 &mut self,
219 message: MlsMessage,
220 time: MlsTime,
221 ) -> Result<ExternalReceivedMessage, MlsError> {
222 MessageProcessor::process_incoming_message_with_time(
223 self,
224 message,
225 #[cfg(feature = "by_ref_proposal")]
226 self.config.cache_proposals(),
227 Some(time),
228 )
229 .await
230 }
231
232 #[cfg(feature = "by_ref_proposal")]
234 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
235 pub async fn insert_proposal_from_message(
236 &mut self,
237 message: MlsMessage,
238 ) -> Result<(), MlsError> {
239 let ptxt = match message.payload {
240 MlsMessagePayload::Plain(p) => Ok(p),
241 _ => Err(MlsError::UnexpectedMessageType),
242 }?;
243
244 let auth_content: AuthenticatedContent = ptxt.into();
245
246 let proposal_ref =
247 ProposalRef::from_content(&self.cipher_suite_provider, &auth_content).await?;
248
249 let sender = auth_content.content.sender;
250
251 let proposal = match auth_content.content.content {
252 Content::Proposal(p) => Ok(*p),
253 _ => Err(MlsError::UnexpectedMessageType),
254 }?;
255
256 self.group_state_mut()
257 .proposals
258 .insert(proposal_ref, proposal, sender);
259
260 Ok(())
261 }
262
263 #[cfg(feature = "by_ref_proposal")]
266 pub fn insert_proposal(&mut self, proposal: CachedProposal) {
267 self.group_state_mut().proposals.insert(
268 proposal.proposal_ref,
269 proposal.proposal,
270 proposal.sender,
271 )
272 }
273
274 #[cfg(feature = "by_ref_proposal")]
283 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
284 pub async fn propose_add(
285 &mut self,
286 key_package: MlsMessage,
287 authenticated_data: Vec<u8>,
288 ) -> Result<MlsMessage, MlsError> {
289 let key_package = key_package
290 .into_key_package()
291 .ok_or(MlsError::UnexpectedMessageType)?;
292
293 self.propose(
294 Proposal::Add(alloc::boxed::Box::new(AddProposal { key_package })),
295 authenticated_data,
296 )
297 .await
298 }
299
300 #[cfg(feature = "by_ref_proposal")]
309 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
310 pub async fn propose_remove(
311 &mut self,
312 index: u32,
313 authenticated_data: Vec<u8>,
314 ) -> Result<MlsMessage, MlsError> {
315 let to_remove = LeafIndex::try_from(index)?;
316
317 self.group_state().public_tree.get_leaf_node(to_remove)?;
319
320 self.propose(
321 Proposal::Remove(RemoveProposal { to_remove }),
322 authenticated_data,
323 )
324 .await
325 }
326
327 #[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
337 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
338 pub async fn propose_external_psk(
339 &mut self,
340 psk: ExternalPskId,
341 authenticated_data: Vec<u8>,
342 ) -> Result<MlsMessage, MlsError> {
343 let proposal = self.psk_proposal(JustPreSharedKeyID::External(psk))?;
344 self.propose(proposal, authenticated_data).await
345 }
346
347 #[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
357 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
358 pub async fn propose_resumption_psk(
359 &mut self,
360 psk_epoch: u64,
361 authenticated_data: Vec<u8>,
362 ) -> Result<MlsMessage, MlsError> {
363 let key_id = ResumptionPsk {
364 psk_epoch,
365 usage: ResumptionPSKUsage::Application,
366 psk_group_id: PskGroupId(self.group_context().group_id().to_vec()),
367 };
368
369 let proposal = self.psk_proposal(JustPreSharedKeyID::Resumption(key_id))?;
370 self.propose(proposal, authenticated_data).await
371 }
372
373 #[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
374 fn psk_proposal(&self, key_id: JustPreSharedKeyID) -> Result<Proposal, MlsError> {
375 Ok(Proposal::Psk(PreSharedKeyProposal {
376 psk: PreSharedKeyID {
377 key_id,
378 psk_nonce: PskNonce::random(&self.cipher_suite_provider)
379 .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))?,
380 },
381 }))
382 }
383
384 #[cfg(feature = "by_ref_proposal")]
394 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
395 pub async fn propose_group_context_extensions(
396 &mut self,
397 extensions: ExtensionList,
398 authenticated_data: Vec<u8>,
399 ) -> Result<MlsMessage, MlsError> {
400 let proposal = Proposal::GroupContextExtensions(extensions);
401 self.propose(proposal, authenticated_data).await
402 }
403
404 #[cfg(feature = "by_ref_proposal")]
413 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
414 pub async fn propose_reinit(
415 &mut self,
416 group_id: Option<Vec<u8>>,
417 version: ProtocolVersion,
418 cipher_suite: CipherSuite,
419 extensions: ExtensionList,
420 authenticated_data: Vec<u8>,
421 ) -> Result<MlsMessage, MlsError> {
422 let group_id = group_id.map(Ok).unwrap_or_else(|| {
423 self.cipher_suite_provider
424 .random_bytes_vec(self.cipher_suite_provider.kdf_extract_size())
425 .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))
426 })?;
427
428 let proposal = Proposal::ReInit(ReInitProposal {
429 group_id,
430 version,
431 cipher_suite,
432 extensions,
433 });
434
435 self.propose(proposal, authenticated_data).await
436 }
437
438 #[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))]
447 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
448 pub async fn propose_custom(
449 &mut self,
450 proposal: CustomProposal,
451 authenticated_data: Vec<u8>,
452 ) -> Result<MlsMessage, MlsError> {
453 self.propose(Proposal::Custom(proposal), authenticated_data)
454 .await
455 }
456
457 #[cfg(feature = "by_ref_proposal")]
463 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
464 pub async fn propose(
465 &mut self,
466 proposal: Proposal,
467 authenticated_data: Vec<u8>,
468 ) -> Result<MlsMessage, MlsError> {
469 let (signer, signing_identity) =
470 self.signing_data.as_ref().ok_or(MlsError::SignerNotFound)?;
471
472 let external_senders_ext = self
473 .state
474 .context
475 .extensions
476 .get_as::<ExternalSendersExt>()?
477 .ok_or(MlsError::ExternalProposalsDisabled)?;
478
479 let sender_index = external_senders_ext
480 .allowed_senders
481 .iter()
482 .position(|allowed_signer| signing_identity == allowed_signer)
483 .ok_or(MlsError::InvalidExternalSigningIdentity)?;
484
485 let sender = Sender::External(sender_index as u32);
486
487 let auth_content = AuthenticatedContent::new_signed(
488 &self.cipher_suite_provider,
489 &self.state.context,
490 sender,
491 Content::Proposal(Box::new(proposal.clone())),
492 signer,
493 WireFormat::PublicMessage,
494 authenticated_data,
495 )
496 .await?;
497
498 let proposal_ref =
499 ProposalRef::from_content(&self.cipher_suite_provider, &auth_content).await?;
500
501 let plaintext = PublicMessage {
502 content: auth_content.content,
503 auth: auth_content.auth,
504 membership_tag: None,
505 };
506
507 let message = MlsMessage::new(
508 self.group_context().version(),
509 MlsMessagePayload::Plain(plaintext),
510 );
511
512 self.state.proposals.insert(proposal_ref, proposal, sender);
513
514 Ok(message)
515 }
516
517 #[cfg(feature = "by_ref_proposal")]
519 pub fn clear_proposal_cache(&mut self) {
520 self.state.proposals.clear()
521 }
522
523 #[cfg(feature = "by_ref_proposal")]
528 pub fn get_cached_proposals(&self) -> Vec<CachedProposal> {
529 self.state
530 .proposals
531 .proposals
532 .iter()
533 .map(|(proposal_ref, cached)| CachedProposal {
534 proposal: cached.proposal.clone(),
535 proposal_ref: proposal_ref.clone(),
536 sender: cached.sender,
537 })
538 .collect()
539 }
540
541 #[inline(always)]
542 pub(crate) fn group_state(&self) -> &GroupState {
543 &self.state
544 }
545
546 #[inline(always)]
548 pub fn group_context(&self) -> &GroupContext {
549 &self.group_state().context
550 }
551
552 pub fn export_tree(&self) -> Result<Vec<u8>, MlsError> {
554 self.group_state()
555 .public_tree
556 .nodes
557 .mls_encode_to_vec()
558 .map_err(Into::into)
559 }
560
561 #[inline(always)]
563 pub fn roster(&self) -> Roster<'_> {
564 self.group_state().public_tree.roster()
565 }
566
567 #[inline(always)]
571 pub fn transcript_hash(&self) -> &Vec<u8> {
572 &self.group_state().context.confirmed_transcript_hash
573 }
574
575 #[inline(always)]
579 pub fn tree_hash(&self) -> &[u8] {
580 &self.group_state().context.tree_hash
581 }
582
583 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
589 pub async fn get_member_with_identity(
590 &self,
591 identity_id: &SigningIdentity,
592 ) -> Result<Member, MlsError> {
593 let identity = self
594 .identity_provider()
595 .identity(identity_id, self.group_context().extensions())
596 .await
597 .map_err(|error| MlsError::IdentityProviderError(error.into_any_error()))?;
598
599 let tree = &self.group_state().public_tree;
600
601 #[cfg(feature = "tree_index")]
602 let index = tree.get_leaf_node_with_identity(&identity);
603
604 #[cfg(not(feature = "tree_index"))]
605 let index = tree
606 .get_leaf_node_with_identity(
607 &identity,
608 &self.identity_provider(),
609 self.group_context().extensions(),
610 )
611 .await?;
612
613 let index = index.ok_or(MlsError::MemberNotFound)?;
614 let node = self.group_state().public_tree.get_leaf_node(index)?;
615
616 Ok(member_from_leaf_node(node, index))
617 }
618}
619
620#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
621#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
622#[cfg_attr(
623 all(not(target_arch = "wasm32"), mls_build_async),
624 maybe_async::must_be_async
625)]
626impl<C> MessageProcessor for ExternalGroup<C>
627where
628 C: ExternalClientConfig + Clone,
629{
630 type MlsRules = C::MlsRules;
631 type IdentityProvider = C::IdentityProvider;
632 type PreSharedKeyStorage = AlwaysFoundPskStorage;
633 type OutputType = ExternalReceivedMessage;
634 type CipherSuiteProvider = <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider;
635
636 fn mls_rules(&self) -> Self::MlsRules {
637 self.config.mls_rules()
638 }
639
640 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
641 async fn verify_plaintext_authentication(
642 &self,
643 message: PublicMessage,
644 ) -> Result<EventOrContent<Self::OutputType>, MlsError> {
645 let auth_content = crate::group::message_verifier::verify_plaintext_authentication(
646 &self.cipher_suite_provider,
647 message,
648 None,
649 &self.state.context,
650 crate::group::message_verifier::SignaturePublicKeysContainer::RatchetTree(
651 &self.state.public_tree,
652 ),
653 )
654 .await?;
655
656 Ok(EventOrContent::Content(auth_content))
657 }
658
659 #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
660 async fn get_unauthenticated_key_generation_from_sender_data(
661 &mut self,
662 _cipher_text: &PrivateMessage,
663 ) -> Result<Option<u32>, MlsError> {
664 Ok(None)
665 }
666
667 #[cfg(feature = "private_message")]
668 async fn process_ciphertext(
669 &mut self,
670 cipher_text: &PrivateMessage,
671 ) -> Result<EventOrContent<Self::OutputType>, MlsError> {
672 Ok(EventOrContent::Event(ExternalReceivedMessage::Ciphertext(
673 cipher_text.content_type,
674 )))
675 }
676
677 async fn update_key_schedule(
678 &mut self,
679 _secrets: Option<(TreeKemPrivate, PathSecret)>,
680 interim_transcript_hash: InterimTranscriptHash,
681 confirmation_tag: &ConfirmationTag,
682 provisional_public_state: ProvisionalState,
683 ) -> Result<(), MlsError> {
684 self.state.context = provisional_public_state.group_context;
685 #[cfg(feature = "by_ref_proposal")]
686 self.state.proposals.clear();
687 self.state.interim_transcript_hash = interim_transcript_hash;
688 self.state.public_tree = provisional_public_state.public_tree;
689 self.state.confirmation_tag = confirmation_tag.clone();
690
691 Ok(())
692 }
693
694 fn identity_provider(&self) -> Self::IdentityProvider {
695 self.config.identity_provider()
696 }
697
698 fn psk_storage(&self) -> Self::PreSharedKeyStorage {
699 AlwaysFoundPskStorage
700 }
701
702 fn group_state(&self) -> &GroupState {
703 &self.state
704 }
705
706 fn group_state_mut(&mut self) -> &mut GroupState {
707 &mut self.state
708 }
709
710 fn removal_proposal(
711 &self,
712 _provisional_state: &ProvisionalState,
713 ) -> Option<ProposalInfo<RemoveProposal>> {
714 None
715 }
716
717 #[cfg(all(
718 feature = "by_ref_proposal",
719 feature = "custom_proposal",
720 feature = "self_remove_proposal"
721 ))]
722 fn self_removal_proposal(
723 &self,
724 _provisional_state: &ProvisionalState,
725 ) -> Option<ProposalInfo<SelfRemoveProposal>> {
726 None
727 }
728
729 #[cfg(feature = "private_message")]
730 fn min_epoch_available(&self) -> Option<u64> {
731 self.config
732 .max_epoch_jitter()
733 .map(|j| self.state.context.epoch - j)
734 }
735
736 fn cipher_suite_provider(&self) -> &Self::CipherSuiteProvider {
737 &self.cipher_suite_provider
738 }
739}
740
741#[derive(Debug, MlsEncode, MlsSize, MlsDecode, PartialEq, Clone)]
743pub struct ExternalSnapshot {
744 version: u16,
745 pub(crate) state: RawGroupState,
746}
747
748impl ExternalSnapshot {
749 pub fn to_bytes(&self) -> Result<Vec<u8>, MlsError> {
751 Ok(self.mls_encode_to_vec()?)
752 }
753
754 pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> {
756 Ok(Self::mls_decode(&mut &*bytes)?)
757 }
758
759 pub fn context(&self) -> &GroupContext {
761 &self.state.context
762 }
763}
764
765impl<C> ExternalGroup<C>
766where
767 C: ExternalClientConfig + Clone,
768{
769 pub fn snapshot(&self) -> ExternalSnapshot {
771 ExternalSnapshot {
772 state: RawGroupState::export(self.group_state()),
773 version: 1,
774 }
775 }
776
777 pub fn snapshot_without_ratchet_tree(&mut self) -> ExternalSnapshot {
781 let tree = std::mem::take(&mut self.state.public_tree.nodes);
782
783 let snapshot = ExternalSnapshot {
784 state: RawGroupState::export(&self.state),
785 version: 1,
786 };
787
788 self.state.public_tree.nodes = tree;
789
790 snapshot
791 }
792}
793
794impl From<CommitMessageDescription> for ExternalReceivedMessage {
795 fn from(value: CommitMessageDescription) -> Self {
796 ExternalReceivedMessage::Commit(value)
797 }
798}
799
800impl TryFrom<ApplicationMessageDescription> for ExternalReceivedMessage {
801 type Error = MlsError;
802
803 fn try_from(_: ApplicationMessageDescription) -> Result<Self, Self::Error> {
804 Err(MlsError::UnencryptedApplicationMessage)
805 }
806}
807
808impl From<ProposalMessageDescription> for ExternalReceivedMessage {
809 fn from(value: ProposalMessageDescription) -> Self {
810 ExternalReceivedMessage::Proposal(value)
811 }
812}
813
814impl From<GroupInfo> for ExternalReceivedMessage {
815 fn from(value: GroupInfo) -> Self {
816 ExternalReceivedMessage::GroupInfo(value)
817 }
818}
819
820impl From<Welcome> for ExternalReceivedMessage {
821 fn from(_: Welcome) -> Self {
822 ExternalReceivedMessage::Welcome
823 }
824}
825
826impl From<KeyPackage> for ExternalReceivedMessage {
827 fn from(value: KeyPackage) -> Self {
828 ExternalReceivedMessage::KeyPackage(value)
829 }
830}
831
832#[cfg(test)]
833pub(crate) mod test_utils {
834 use crate::{
835 external_client::tests_utils::{TestExternalClientBuilder, TestExternalClientConfig},
836 group::test_utils::TestGroup,
837 };
838
839 use super::ExternalGroup;
840
841 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
842 pub(crate) async fn make_external_group(
843 group: &TestGroup,
844 ) -> ExternalGroup<TestExternalClientConfig> {
845 make_external_group_with_config(
846 group,
847 TestExternalClientBuilder::new_for_test().build_config(),
848 )
849 .await
850 }
851
852 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
853 pub(crate) async fn make_external_group_with_config(
854 group: &TestGroup,
855 config: TestExternalClientConfig,
856 ) -> ExternalGroup<TestExternalClientConfig> {
857 ExternalGroup::join(
858 config,
859 None,
860 group
861 .group_info_message_allowing_ext_commit(true)
862 .await
863 .unwrap(),
864 None,
865 None,
866 )
867 .await
868 .unwrap()
869 }
870}
871
872#[cfg(test)]
873mod tests {
874 use super::test_utils::make_external_group;
875 use crate::{
876 cipher_suite::CipherSuite,
877 client::{
878 test_utils::{TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION},
879 MlsError,
880 },
881 crypto::{test_utils::TestCryptoProvider, SignatureSecretKey},
882 extension::ExternalSendersExt,
883 external_client::{
884 group::test_utils::make_external_group_with_config,
885 tests_utils::{TestExternalClientBuilder, TestExternalClientConfig},
886 ExternalClient, ExternalGroup, ExternalReceivedMessage, ExternalSnapshot,
887 },
888 group::{
889 framing::{Content, MlsMessagePayload},
890 message_processor::CommitEffect,
891 proposal::{AddProposal, Proposal, ProposalOrRef},
892 proposal_ref::ProposalRef,
893 snapshot::RawGroupState,
894 test_utils::{test_group, TestGroup},
895 CommitMessageDescription, ExportedTree, ProposalMessageDescription,
896 },
897 identity::{test_utils::get_test_signing_identity, SigningIdentity},
898 key_package::test_utils::{test_key_package, test_key_package_message},
899 protocol_version::ProtocolVersion,
900 ExtensionList, MlsMessage,
901 };
902 use assert_matches::assert_matches;
903 use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
904
905 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
906 async fn test_group_with_one_commit(v: ProtocolVersion, cs: CipherSuite) -> TestGroup {
907 let mut group = test_group(v, cs).await;
908 group.commit(Vec::new()).await.unwrap();
909 group.process_pending_commit().await.unwrap();
910 group
911 }
912
913 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
914 async fn test_group_two_members(
915 v: ProtocolVersion,
916 cs: CipherSuite,
917 #[cfg(feature = "by_ref_proposal")] ext_identity: Option<SigningIdentity>,
918 ) -> TestGroup {
919 let mut group = test_group_with_one_commit(v, cs).await;
920
921 let bob_key_package = test_key_package_message(v, cs, "bob").await;
922
923 let mut commit_builder = group.commit_builder().add_member(bob_key_package).unwrap();
924
925 #[cfg(feature = "by_ref_proposal")]
926 if let Some(ext_signer) = ext_identity {
927 let mut ext_list = ExtensionList::new();
928
929 ext_list
930 .set_from(ExternalSendersExt {
931 allowed_senders: vec![ext_signer],
932 })
933 .unwrap();
934
935 commit_builder = commit_builder.set_group_context_ext(ext_list).unwrap();
936 }
937
938 commit_builder.build().await.unwrap();
939
940 group.process_pending_commit().await.unwrap();
941 group
942 }
943
944 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
945 async fn external_group_can_be_created() {
946 for (v, cs) in ProtocolVersion::all().flat_map(|v| {
947 TestCryptoProvider::all_supported_cipher_suites()
948 .into_iter()
949 .map(move |cs| (v, cs))
950 }) {
951 make_external_group(&test_group_with_one_commit(v, cs).await).await;
952 }
953 }
954
955 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
956 async fn external_group_can_process_commit() {
957 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
958 let mut server = make_external_group(&alice).await;
959 let commit_output = alice.commit(Vec::new()).await.unwrap();
960 alice.apply_pending_commit().await.unwrap();
961
962 server
963 .process_incoming_message(commit_output.commit_message)
964 .await
965 .unwrap();
966
967 assert_eq!(alice.state, server.state);
968 }
969
970 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
971 async fn external_group_can_process_proposals_by_reference() {
972 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
973 let mut server = make_external_group(&alice).await;
974
975 let bob_key_package =
976 test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
977
978 let add_proposal = Proposal::Add(Box::new(AddProposal {
979 key_package: bob_key_package,
980 }));
981
982 let packet = alice.propose(add_proposal.clone()).await;
983
984 let proposal_process = server.process_incoming_message(packet).await.unwrap();
985
986 assert_matches!(
987 proposal_process,
988 ExternalReceivedMessage::Proposal(ProposalMessageDescription { ref proposal, ..}) if proposal == &add_proposal
989 );
990
991 let commit_output = alice.commit(vec![]).await.unwrap();
992 alice.apply_pending_commit().await.unwrap();
993
994 let new_epoch = match server
995 .process_incoming_message(commit_output.commit_message)
996 .await
997 .unwrap()
998 {
999 ExternalReceivedMessage::Commit(CommitMessageDescription {
1000 effect: CommitEffect::NewEpoch(new_epoch),
1001 ..
1002 }) => new_epoch,
1003 _ => panic!("Expected processed commit"),
1004 };
1005
1006 assert_eq!(new_epoch.applied_proposals.len(), 1);
1007
1008 assert!(new_epoch
1009 .applied_proposals
1010 .into_iter()
1011 .any(|p| p.proposal == add_proposal));
1012
1013 assert_eq!(alice.state, server.state);
1014 }
1015
1016 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1017 async fn external_group_can_process_commit_adding_member() {
1018 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1019 let mut server = make_external_group(&alice).await;
1020 let (_, commit) = alice.join("bob").await;
1021
1022 let new_epoch = match server.process_incoming_message(commit).await.unwrap() {
1023 ExternalReceivedMessage::Commit(CommitMessageDescription {
1024 effect: CommitEffect::NewEpoch(new_epoch),
1025 ..
1026 }) => new_epoch,
1027 _ => panic!("Expected processed commit"),
1028 };
1029
1030 assert_eq!(new_epoch.applied_proposals.len(), 1);
1031
1032 assert_eq!(
1033 new_epoch
1034 .applied_proposals
1035 .into_iter()
1036 .filter(|p| matches!(p.proposal, Proposal::Add(_)))
1037 .count(),
1038 1
1039 );
1040
1041 assert_eq!(server.state.public_tree.get_leaf_nodes().len(), 2);
1042
1043 assert_eq!(alice.state, server.state);
1044 }
1045
1046 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1047 async fn external_group_rejects_commit_not_for_current_epoch() {
1048 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1049 let mut server = make_external_group(&alice).await;
1050
1051 let mut commit_output = alice.commit(vec![]).await.unwrap();
1052
1053 match commit_output.commit_message.payload {
1054 MlsMessagePayload::Plain(ref mut plain) => plain.content.epoch = 0,
1055 _ => panic!("Unexpected non-plaintext data"),
1056 };
1057
1058 let res = server
1059 .process_incoming_message(commit_output.commit_message)
1060 .await;
1061
1062 assert_matches!(res, Err(MlsError::InvalidEpoch));
1063 }
1064
1065 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1066 async fn external_group_can_reject_message_with_invalid_signature() {
1067 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1068
1069 let mut server = make_external_group_with_config(
1070 &alice,
1071 TestExternalClientBuilder::new_for_test().build_config(),
1072 )
1073 .await;
1074
1075 let mut commit_output = alice.commit(Vec::new()).await.unwrap();
1076
1077 match commit_output.commit_message.payload {
1078 MlsMessagePayload::Plain(ref mut plain) => plain.auth.signature = Vec::new().into(),
1079 _ => panic!("Unexpected non-plaintext data"),
1080 };
1081
1082 let res = server
1083 .process_incoming_message(commit_output.commit_message)
1084 .await;
1085
1086 assert_matches!(res, Err(MlsError::InvalidSignature));
1087 }
1088
1089 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1090 async fn external_group_rejects_unencrypted_application_message() {
1091 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1092 let mut server = make_external_group(&alice).await;
1093
1094 let plaintext = alice
1095 .make_plaintext(Content::Application(b"hello".to_vec().into()))
1096 .await;
1097
1098 let res = server.process_incoming_message(plaintext).await;
1099
1100 assert_matches!(res, Err(MlsError::UnencryptedApplicationMessage));
1101 }
1102
1103 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1104 async fn external_group_will_reject_unsupported_cipher_suites() {
1105 let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1106
1107 let config =
1108 TestExternalClientBuilder::new_for_test_disabling_cipher_suite(TEST_CIPHER_SUITE)
1109 .build_config();
1110
1111 let res = ExternalGroup::join(
1112 config,
1113 None,
1114 alice
1115 .group_info_message_allowing_ext_commit(true)
1116 .await
1117 .unwrap(),
1118 None,
1119 None,
1120 )
1121 .await
1122 .map(|_| ());
1123
1124 assert_matches!(
1125 res,
1126 Err(MlsError::UnsupportedCipherSuite(TEST_CIPHER_SUITE))
1127 );
1128 }
1129
1130 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1131 async fn external_group_will_reject_unsupported_protocol_versions() {
1132 let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1133
1134 let config = TestExternalClientBuilder::new_for_test().build_config();
1135
1136 let mut group_info = alice
1137 .group_info_message_allowing_ext_commit(true)
1138 .await
1139 .unwrap();
1140
1141 group_info.version = ProtocolVersion::from(64);
1142
1143 let res = ExternalGroup::join(config, None, group_info, None, None)
1144 .await
1145 .map(|_| ());
1146
1147 assert_matches!(
1148 res,
1149 Err(MlsError::UnsupportedProtocolVersion(v)) if v ==
1150 ProtocolVersion::from(64)
1151 );
1152 }
1153
1154 #[cfg(feature = "by_ref_proposal")]
1155 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1156 async fn setup_extern_proposal_test(
1157 extern_proposals_allowed: bool,
1158 ) -> (SigningIdentity, SignatureSecretKey, TestGroup) {
1159 let (server_identity, server_key) =
1160 get_test_signing_identity(TEST_CIPHER_SUITE, b"server").await;
1161
1162 let alice = test_group_two_members(
1163 TEST_PROTOCOL_VERSION,
1164 TEST_CIPHER_SUITE,
1165 extern_proposals_allowed.then(|| server_identity.clone()),
1166 )
1167 .await;
1168
1169 (server_identity, server_key, alice)
1170 }
1171
1172 #[cfg(feature = "by_ref_proposal")]
1173 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1174 async fn test_external_proposal(
1175 server: &mut ExternalGroup<TestExternalClientConfig>,
1176 alice: &mut TestGroup,
1177 external_proposal: MlsMessage,
1178 ) {
1179 let auth_content = external_proposal.clone().into_plaintext().unwrap().into();
1180
1181 let proposal_ref = ProposalRef::from_content(&server.cipher_suite_provider, &auth_content)
1182 .await
1183 .unwrap();
1184
1185 alice.process_message(external_proposal).await.unwrap();
1187
1188 let commit_output = alice.commit(vec![]).await.unwrap();
1190
1191 let commit = match commit_output
1192 .commit_message
1193 .clone()
1194 .into_plaintext()
1195 .unwrap()
1196 .content
1197 .content
1198 {
1199 Content::Commit(commit) => commit,
1200 _ => panic!("not a commit"),
1201 };
1202
1203 assert!(commit
1205 .proposals
1206 .contains(&ProposalOrRef::Reference(proposal_ref)));
1207
1208 alice.process_pending_commit().await.unwrap();
1209
1210 server
1211 .process_incoming_message(commit_output.commit_message)
1212 .await
1213 .unwrap();
1214
1215 assert_eq!(alice.state, server.state);
1216 }
1217
1218 #[cfg(feature = "by_ref_proposal")]
1219 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1220 async fn external_group_can_propose_add() {
1221 let (server_identity, server_key, mut alice) = setup_extern_proposal_test(true).await;
1222
1223 let mut server = make_external_group(&alice).await;
1224
1225 server.signing_data = Some((server_key, server_identity));
1226
1227 let charlie_key_package =
1228 test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "charlie").await;
1229
1230 let external_proposal = server
1231 .propose_add(charlie_key_package, vec![])
1232 .await
1233 .unwrap();
1234
1235 test_external_proposal(&mut server, &mut alice, external_proposal).await
1236 }
1237
1238 #[cfg(feature = "by_ref_proposal")]
1239 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1240 async fn external_group_can_propose_remove() {
1241 let (server_identity, server_key, mut alice) = setup_extern_proposal_test(true).await;
1242
1243 let mut server = make_external_group(&alice).await;
1244
1245 server.signing_data = Some((server_key, server_identity));
1246
1247 let external_proposal = server.propose_remove(1, vec![]).await.unwrap();
1248
1249 test_external_proposal(&mut server, &mut alice, external_proposal).await
1250 }
1251
1252 #[cfg(feature = "by_ref_proposal")]
1253 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1254 async fn external_group_external_proposal_not_allowed() {
1255 let (signing_id, secret_key, alice) = setup_extern_proposal_test(false).await;
1256 let mut server = make_external_group(&alice).await;
1257
1258 server.signing_data = Some((secret_key, signing_id));
1259
1260 let charlie_key_package =
1261 test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "charlie").await;
1262
1263 let res = server.propose_add(charlie_key_package, vec![]).await;
1264
1265 assert_matches!(res, Err(MlsError::ExternalProposalsDisabled));
1266 }
1267
1268 #[cfg(feature = "by_ref_proposal")]
1269 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1270 async fn external_group_external_signing_identity_invalid() {
1271 let (server_identity, server_key) =
1272 get_test_signing_identity(TEST_CIPHER_SUITE, b"server").await;
1273
1274 let alice = test_group_two_members(
1275 TEST_PROTOCOL_VERSION,
1276 TEST_CIPHER_SUITE,
1277 Some(
1278 get_test_signing_identity(TEST_CIPHER_SUITE, b"not server")
1279 .await
1280 .0,
1281 ),
1282 )
1283 .await;
1284
1285 let mut server = make_external_group(&alice).await;
1286
1287 server.signing_data = Some((server_key, server_identity));
1288
1289 let res = server.propose_remove(1, vec![]).await;
1290
1291 assert_matches!(res, Err(MlsError::InvalidExternalSigningIdentity));
1292 }
1293
1294 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1295 async fn external_group_errors_on_old_epoch() {
1296 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1297
1298 let mut server = make_external_group_with_config(
1299 &alice,
1300 TestExternalClientBuilder::new_for_test()
1301 .max_epoch_jitter(0)
1302 .build_config(),
1303 )
1304 .await;
1305
1306 let old_application_msg = alice
1307 .encrypt_application_message(&[], vec![])
1308 .await
1309 .unwrap();
1310
1311 let commit_output = alice.commit(vec![]).await.unwrap();
1312
1313 server
1314 .process_incoming_message(commit_output.commit_message)
1315 .await
1316 .unwrap();
1317
1318 let res = server.process_incoming_message(old_application_msg).await;
1319
1320 assert_matches!(res, Err(MlsError::InvalidEpoch));
1321 }
1322
1323 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1324 async fn proposals_can_be_cached_externally() {
1325 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1326
1327 let mut server = make_external_group_with_config(
1328 &alice,
1329 TestExternalClientBuilder::new_for_test()
1330 .cache_proposals(false)
1331 .build_config(),
1332 )
1333 .await;
1334
1335 let proposal = alice.propose_update(vec![]).await.unwrap();
1336
1337 let commit_output = alice.commit(vec![]).await.unwrap();
1338
1339 server
1340 .process_incoming_message(proposal.clone())
1341 .await
1342 .unwrap();
1343
1344 server.insert_proposal_from_message(proposal).await.unwrap();
1345
1346 server
1347 .process_incoming_message(commit_output.commit_message)
1348 .await
1349 .unwrap();
1350 }
1351
1352 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1353 async fn external_group_cached_proposals_returns_pending_proposals() {
1354 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1355 let mut server = make_external_group(&alice).await;
1356
1357 assert!(server.get_cached_proposals().is_empty());
1358
1359 let proposal = alice.propose_update(vec![]).await.unwrap();
1360 server.process_incoming_message(proposal).await.unwrap();
1361
1362 let cached = server.get_cached_proposals();
1363 assert_eq!(cached.len(), 1);
1364 assert!(matches!(cached[0].proposal(), Proposal::Update(_)));
1365 assert!(!cached[0].proposal_ref().is_empty());
1366
1367 let commit_output = alice.commit(vec![]).await.unwrap();
1368 alice.apply_pending_commit().await.unwrap();
1369 server
1370 .process_incoming_message(commit_output.commit_message)
1371 .await
1372 .unwrap();
1373
1374 assert!(server.get_cached_proposals().is_empty());
1375 }
1376
1377 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1378 async fn external_group_cached_proposals_returns_independent_copies() {
1379 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1380 let mut server = make_external_group(&alice).await;
1381
1382 let proposal = alice.propose_update(vec![]).await.unwrap();
1383 server.process_incoming_message(proposal).await.unwrap();
1384
1385 let mut cached1 = server.get_cached_proposals();
1386 let cached2 = server.get_cached_proposals();
1387
1388 assert_eq!(cached1.len(), 1);
1389 assert_eq!(cached2.len(), 1);
1390 assert_eq!(cached1[0].proposal_ref(), cached2[0].proposal_ref());
1391
1392 cached1.clear();
1393 assert!(cached1.is_empty());
1394 assert_eq!(cached2.len(), 1);
1395
1396 let cached3 = server.get_cached_proposals();
1397 assert_eq!(cached3.len(), 1);
1398 assert_eq!(cached2[0].proposal_ref(), cached3[0].proposal_ref());
1399 }
1400
1401 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1402 async fn external_group_can_observe_since_creation() {
1403 let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1404
1405 let info = alice
1406 .group_info_message_allowing_ext_commit(true)
1407 .await
1408 .unwrap();
1409
1410 let config = TestExternalClientBuilder::new_for_test().build_config();
1411 let mut server = ExternalGroup::join(config, None, info, None, None)
1412 .await
1413 .unwrap();
1414
1415 for _ in 0..2 {
1416 let commit = alice.commit(vec![]).await.unwrap().commit_message;
1417 alice.process_pending_commit().await.unwrap();
1418 server.process_incoming_message(commit).await.unwrap();
1419 }
1420 }
1421
1422 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1423 async fn external_group_can_be_serialized_to_tls_encoding() {
1424 let server =
1425 make_external_group(&test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await).await;
1426
1427 let snapshot = server.snapshot().mls_encode_to_vec().unwrap();
1428 let snapshot_restored = ExternalSnapshot::mls_decode(&mut snapshot.as_slice()).unwrap();
1429
1430 assert_eq!(server.snapshot(), snapshot_restored);
1431 }
1432
1433 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1434 async fn legacy_snapshot_migration() {
1435 #[derive(MlsSize, MlsEncode)]
1436 struct LegacyExternalSnapshot {
1437 version: u16,
1438 state: RawGroupState,
1439 signing_data: Option<(SignatureSecretKey, SigningIdentity)>,
1440 }
1441
1442 let (server_identity, server_key, alice) = setup_extern_proposal_test(true).await;
1443 let server = make_external_group(&alice).await;
1444
1445 let legacy_snapshot = LegacyExternalSnapshot {
1446 version: *TEST_PROTOCOL_VERSION,
1447 state: server.snapshot().state,
1448 signing_data: Some((server_key, server_identity)),
1449 };
1450
1451 let legacy_snapshot_bytes = legacy_snapshot.mls_encode_to_vec().unwrap();
1452 let migrated_snapshot = ExternalSnapshot::mls_decode(&mut &*legacy_snapshot_bytes).unwrap();
1453
1454 assert_eq!(legacy_snapshot.state, migrated_snapshot.state);
1455 assert_eq!(*TEST_PROTOCOL_VERSION, migrated_snapshot.version);
1456 }
1457
1458 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1459 async fn external_group_can_validate_info() {
1460 let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1461 let mut server = make_external_group(&alice).await;
1462
1463 let info = alice
1464 .group_info_message_allowing_ext_commit(false)
1465 .await
1466 .unwrap();
1467
1468 let update = server.process_incoming_message(info.clone()).await.unwrap();
1469 let info = info.into_group_info().unwrap();
1470
1471 assert_matches!(update, ExternalReceivedMessage::GroupInfo(update_info) if update_info == info);
1472 }
1473
1474 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1475 async fn external_group_can_validate_key_package() {
1476 let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1477 let mut server = make_external_group(&alice).await;
1478
1479 let kp = test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "john").await;
1480
1481 let update = server.process_incoming_message(kp.clone()).await.unwrap();
1482 let kp = kp.into_key_package().unwrap();
1483
1484 assert_matches!(update, ExternalReceivedMessage::KeyPackage(update_kp) if update_kp == kp);
1485 }
1486
1487 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1488 async fn external_group_can_validate_welcome() {
1489 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1490 let mut server = make_external_group(&alice).await;
1491
1492 let [welcome] = alice
1493 .commit_builder()
1494 .add_member(
1495 test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "john").await,
1496 )
1497 .unwrap()
1498 .build()
1499 .await
1500 .unwrap()
1501 .welcome_messages
1502 .try_into()
1503 .unwrap();
1504
1505 let update = server.process_incoming_message(welcome).await.unwrap();
1506
1507 assert_matches!(update, ExternalReceivedMessage::Welcome);
1508 }
1509
1510 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1511 async fn external_group_can_be_stored_without_tree() {
1512 let mut server =
1513 make_external_group(&test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await).await;
1514
1515 let snapshot_with_tree = server.snapshot().mls_encode_to_vec().unwrap();
1516
1517 let snapshot_without_tree = server
1518 .snapshot_without_ratchet_tree()
1519 .mls_encode_to_vec()
1520 .unwrap();
1521
1522 let tree = server.state.public_tree.nodes.mls_encode_to_vec().unwrap();
1523 let empty_tree = Vec::<u8>::new().mls_encode_to_vec().unwrap();
1524
1525 assert_eq!(
1526 snapshot_with_tree.len() - snapshot_without_tree.len(),
1527 tree.len() - empty_tree.len()
1528 );
1529
1530 let exported_tree = server.export_tree().unwrap();
1531
1532 let restored = ExternalClient::new(server.config.clone(), None)
1533 .load_group_with_ratchet_tree(
1534 ExternalSnapshot::from_bytes(&snapshot_without_tree).unwrap(),
1535 ExportedTree::from_bytes(&exported_tree).unwrap(),
1536 )
1537 .await
1538 .unwrap();
1539
1540 assert_eq!(restored.group_state(), server.group_state());
1541 }
1542}