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)]
568 pub fn exported_tree(&self) -> ExportedTree<'_> {
569 ExportedTree::new_borrowed(&self.group_state().public_tree.nodes)
570 }
571
572 #[inline(always)]
574 pub fn roster(&self) -> Roster<'_> {
575 self.group_state().public_tree.roster()
576 }
577
578 #[inline(always)]
582 pub fn transcript_hash(&self) -> &Vec<u8> {
583 &self.group_state().context.confirmed_transcript_hash
584 }
585
586 #[inline(always)]
590 pub fn tree_hash(&self) -> &[u8] {
591 &self.group_state().context.tree_hash
592 }
593
594 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
600 pub async fn get_member_with_identity(
601 &self,
602 identity_id: &SigningIdentity,
603 ) -> Result<Member, MlsError> {
604 let identity = self
605 .identity_provider()
606 .identity(identity_id, self.group_context().extensions())
607 .await
608 .map_err(|error| MlsError::IdentityProviderError(error.into_any_error()))?;
609
610 let tree = &self.group_state().public_tree;
611
612 #[cfg(feature = "tree_index")]
613 let index = tree.get_leaf_node_with_identity(&identity);
614
615 #[cfg(not(feature = "tree_index"))]
616 let index = tree
617 .get_leaf_node_with_identity(
618 &identity,
619 &self.identity_provider(),
620 self.group_context().extensions(),
621 )
622 .await?;
623
624 let index = index.ok_or(MlsError::MemberNotFound)?;
625 let node = self.group_state().public_tree.get_leaf_node(index)?;
626
627 Ok(member_from_leaf_node(node, index))
628 }
629}
630
631#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
632#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
633#[cfg_attr(
634 all(not(target_arch = "wasm32"), mls_build_async),
635 maybe_async::must_be_async
636)]
637impl<C> MessageProcessor for ExternalGroup<C>
638where
639 C: ExternalClientConfig + Clone,
640{
641 type MlsRules = C::MlsRules;
642 type IdentityProvider = C::IdentityProvider;
643 type PreSharedKeyStorage = AlwaysFoundPskStorage;
644 type OutputType = ExternalReceivedMessage;
645 type CipherSuiteProvider = <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider;
646
647 fn mls_rules(&self) -> Self::MlsRules {
648 self.config.mls_rules()
649 }
650
651 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
652 async fn verify_plaintext_authentication(
653 &self,
654 message: PublicMessage,
655 ) -> Result<EventOrContent<Self::OutputType>, MlsError> {
656 let auth_content = crate::group::message_verifier::verify_plaintext_authentication(
657 &self.cipher_suite_provider,
658 message,
659 None,
660 &self.state.context,
661 crate::group::message_verifier::SignaturePublicKeysContainer::RatchetTree(
662 &self.state.public_tree,
663 ),
664 )
665 .await?;
666
667 Ok(EventOrContent::Content(auth_content))
668 }
669
670 #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
671 async fn get_unauthenticated_key_generation_from_sender_data(
672 &mut self,
673 _cipher_text: &PrivateMessage,
674 ) -> Result<Option<u32>, MlsError> {
675 Ok(None)
676 }
677
678 #[cfg(feature = "private_message")]
679 async fn process_ciphertext(
680 &mut self,
681 cipher_text: &PrivateMessage,
682 ) -> Result<EventOrContent<Self::OutputType>, MlsError> {
683 Ok(EventOrContent::Event(ExternalReceivedMessage::Ciphertext(
684 cipher_text.content_type,
685 )))
686 }
687
688 async fn update_key_schedule(
689 &mut self,
690 _secrets: Option<(TreeKemPrivate, PathSecret)>,
691 interim_transcript_hash: InterimTranscriptHash,
692 confirmation_tag: &ConfirmationTag,
693 provisional_public_state: ProvisionalState,
694 ) -> Result<(), MlsError> {
695 self.state.context = provisional_public_state.group_context;
696 #[cfg(feature = "by_ref_proposal")]
697 self.state.proposals.clear();
698 self.state.interim_transcript_hash = interim_transcript_hash;
699 self.state.public_tree = provisional_public_state.public_tree;
700 self.state.confirmation_tag = confirmation_tag.clone();
701
702 Ok(())
703 }
704
705 fn identity_provider(&self) -> Self::IdentityProvider {
706 self.config.identity_provider()
707 }
708
709 fn psk_storage(&self) -> Self::PreSharedKeyStorage {
710 AlwaysFoundPskStorage
711 }
712
713 fn group_state(&self) -> &GroupState {
714 &self.state
715 }
716
717 fn group_state_mut(&mut self) -> &mut GroupState {
718 &mut self.state
719 }
720
721 fn removal_proposal(
722 &self,
723 _provisional_state: &ProvisionalState,
724 ) -> Option<ProposalInfo<RemoveProposal>> {
725 None
726 }
727
728 #[cfg(all(
729 feature = "by_ref_proposal",
730 feature = "custom_proposal",
731 feature = "self_remove_proposal"
732 ))]
733 fn self_removal_proposal(
734 &self,
735 _provisional_state: &ProvisionalState,
736 ) -> Option<ProposalInfo<SelfRemoveProposal>> {
737 None
738 }
739
740 #[cfg(feature = "private_message")]
741 fn min_epoch_available(&self) -> Option<u64> {
742 self.config
743 .max_epoch_jitter()
744 .map(|j| self.state.context.epoch - j)
745 }
746
747 fn cipher_suite_provider(&self) -> &Self::CipherSuiteProvider {
748 &self.cipher_suite_provider
749 }
750}
751
752#[derive(Debug, MlsEncode, MlsSize, MlsDecode, PartialEq, Clone)]
754pub struct ExternalSnapshot {
755 version: u16,
756 pub(crate) state: RawGroupState,
757}
758
759impl ExternalSnapshot {
760 pub fn to_bytes(&self) -> Result<Vec<u8>, MlsError> {
762 Ok(self.mls_encode_to_vec()?)
763 }
764
765 pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> {
767 Ok(Self::mls_decode(&mut &*bytes)?)
768 }
769
770 pub fn context(&self) -> &GroupContext {
772 &self.state.context
773 }
774}
775
776impl<C> ExternalGroup<C>
777where
778 C: ExternalClientConfig + Clone,
779{
780 pub fn snapshot(&self) -> ExternalSnapshot {
782 ExternalSnapshot {
783 state: RawGroupState::export(self.group_state()),
784 version: 1,
785 }
786 }
787
788 pub fn snapshot_without_ratchet_tree(&mut self) -> ExternalSnapshot {
792 let tree = std::mem::take(&mut self.state.public_tree.nodes);
793
794 let snapshot = ExternalSnapshot {
795 state: RawGroupState::export(&self.state),
796 version: 1,
797 };
798
799 self.state.public_tree.nodes = tree;
800
801 snapshot
802 }
803}
804
805impl From<CommitMessageDescription> for ExternalReceivedMessage {
806 fn from(value: CommitMessageDescription) -> Self {
807 ExternalReceivedMessage::Commit(value)
808 }
809}
810
811impl TryFrom<ApplicationMessageDescription> for ExternalReceivedMessage {
812 type Error = MlsError;
813
814 fn try_from(_: ApplicationMessageDescription) -> Result<Self, Self::Error> {
815 Err(MlsError::UnencryptedApplicationMessage)
816 }
817}
818
819impl From<ProposalMessageDescription> for ExternalReceivedMessage {
820 fn from(value: ProposalMessageDescription) -> Self {
821 ExternalReceivedMessage::Proposal(value)
822 }
823}
824
825impl From<GroupInfo> for ExternalReceivedMessage {
826 fn from(value: GroupInfo) -> Self {
827 ExternalReceivedMessage::GroupInfo(value)
828 }
829}
830
831impl From<Welcome> for ExternalReceivedMessage {
832 fn from(_: Welcome) -> Self {
833 ExternalReceivedMessage::Welcome
834 }
835}
836
837impl From<KeyPackage> for ExternalReceivedMessage {
838 fn from(value: KeyPackage) -> Self {
839 ExternalReceivedMessage::KeyPackage(value)
840 }
841}
842
843#[cfg(test)]
844pub(crate) mod test_utils {
845 use crate::{
846 external_client::tests_utils::{TestExternalClientBuilder, TestExternalClientConfig},
847 group::test_utils::TestGroup,
848 };
849
850 use super::ExternalGroup;
851
852 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
853 pub(crate) async fn make_external_group(
854 group: &TestGroup,
855 ) -> ExternalGroup<TestExternalClientConfig> {
856 make_external_group_with_config(
857 group,
858 TestExternalClientBuilder::new_for_test().build_config(),
859 )
860 .await
861 }
862
863 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
864 pub(crate) async fn make_external_group_with_config(
865 group: &TestGroup,
866 config: TestExternalClientConfig,
867 ) -> ExternalGroup<TestExternalClientConfig> {
868 ExternalGroup::join(
869 config,
870 None,
871 group
872 .group_info_message_allowing_ext_commit(true)
873 .await
874 .unwrap(),
875 None,
876 None,
877 )
878 .await
879 .unwrap()
880 }
881}
882
883#[cfg(test)]
884mod tests {
885 use super::test_utils::make_external_group;
886 use crate::{
887 cipher_suite::CipherSuite,
888 client::{
889 test_utils::{TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION},
890 MlsError,
891 },
892 crypto::{test_utils::TestCryptoProvider, SignatureSecretKey},
893 extension::ExternalSendersExt,
894 external_client::{
895 group::test_utils::make_external_group_with_config,
896 tests_utils::{TestExternalClientBuilder, TestExternalClientConfig},
897 ExternalClient, ExternalGroup, ExternalReceivedMessage, ExternalSnapshot,
898 },
899 group::{
900 framing::{Content, MlsMessagePayload},
901 message_processor::CommitEffect,
902 proposal::{AddProposal, Proposal, ProposalOrRef},
903 proposal_ref::ProposalRef,
904 snapshot::RawGroupState,
905 test_utils::{test_group, TestGroup},
906 CommitMessageDescription, ExportedTree, ProposalMessageDescription,
907 },
908 identity::{test_utils::get_test_signing_identity, SigningIdentity},
909 key_package::test_utils::{test_key_package, test_key_package_message},
910 protocol_version::ProtocolVersion,
911 tree_kem::node::LeafIndex,
912 ExtensionList, MlsMessage,
913 };
914 use assert_matches::assert_matches;
915 use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
916
917 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
918 async fn test_group_with_one_commit(v: ProtocolVersion, cs: CipherSuite) -> TestGroup {
919 let mut group = test_group(v, cs).await;
920 group.commit(Vec::new()).await.unwrap();
921 group.process_pending_commit().await.unwrap();
922 group
923 }
924
925 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
926 async fn test_group_two_members(
927 v: ProtocolVersion,
928 cs: CipherSuite,
929 #[cfg(feature = "by_ref_proposal")] ext_identity: Option<SigningIdentity>,
930 ) -> TestGroup {
931 let mut group = test_group_with_one_commit(v, cs).await;
932
933 let bob_key_package = test_key_package_message(v, cs, "bob").await;
934
935 let mut commit_builder = group.commit_builder().add_member(bob_key_package).unwrap();
936
937 #[cfg(feature = "by_ref_proposal")]
938 if let Some(ext_signer) = ext_identity {
939 let mut ext_list = ExtensionList::new();
940
941 ext_list
942 .set_from(ExternalSendersExt {
943 allowed_senders: vec![ext_signer],
944 })
945 .unwrap();
946
947 commit_builder = commit_builder.set_group_context_ext(ext_list).unwrap();
948 }
949
950 commit_builder.build().await.unwrap();
951
952 group.process_pending_commit().await.unwrap();
953 group
954 }
955
956 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
957 async fn external_group_can_be_created() {
958 for (v, cs) in ProtocolVersion::all().flat_map(|v| {
959 TestCryptoProvider::all_supported_cipher_suites()
960 .into_iter()
961 .map(move |cs| (v, cs))
962 }) {
963 make_external_group(&test_group_with_one_commit(v, cs).await).await;
964 }
965 }
966
967 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
968 async fn external_group_can_process_commit() {
969 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
970 let mut server = make_external_group(&alice).await;
971 let commit_output = alice.commit(Vec::new()).await.unwrap();
972 alice.apply_pending_commit().await.unwrap();
973
974 server
975 .process_incoming_message(commit_output.commit_message)
976 .await
977 .unwrap();
978
979 assert_eq!(alice.state, server.state);
980 }
981
982 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
983 async fn external_group_can_process_proposals_by_reference() {
984 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
985 let mut server = make_external_group(&alice).await;
986
987 let bob_key_package =
988 test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
989
990 let add_proposal = Proposal::Add(Box::new(AddProposal {
991 key_package: bob_key_package,
992 }));
993
994 let packet = alice.propose(add_proposal.clone()).await;
995
996 let proposal_process = server.process_incoming_message(packet).await.unwrap();
997
998 assert_matches!(
999 proposal_process,
1000 ExternalReceivedMessage::Proposal(ProposalMessageDescription { ref proposal, ..}) if proposal == &add_proposal
1001 );
1002
1003 let commit_output = alice.commit(vec![]).await.unwrap();
1004 alice.apply_pending_commit().await.unwrap();
1005
1006 let new_epoch = match server
1007 .process_incoming_message(commit_output.commit_message)
1008 .await
1009 .unwrap()
1010 {
1011 ExternalReceivedMessage::Commit(CommitMessageDescription {
1012 effect: CommitEffect::NewEpoch(new_epoch),
1013 ..
1014 }) => new_epoch,
1015 _ => panic!("Expected processed commit"),
1016 };
1017
1018 assert_eq!(new_epoch.applied_proposals.len(), 1);
1019
1020 assert!(new_epoch
1021 .applied_proposals
1022 .into_iter()
1023 .any(|p| p.proposal == add_proposal));
1024
1025 assert_eq!(alice.state, server.state);
1026 }
1027
1028 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1029 async fn external_group_can_process_commit_adding_member() {
1030 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1031 let mut server = make_external_group(&alice).await;
1032 let (_, commit) = alice.join("bob").await;
1033
1034 let new_epoch = match server.process_incoming_message(commit).await.unwrap() {
1035 ExternalReceivedMessage::Commit(CommitMessageDescription {
1036 effect: CommitEffect::NewEpoch(new_epoch),
1037 ..
1038 }) => new_epoch,
1039 _ => panic!("Expected processed commit"),
1040 };
1041
1042 assert_eq!(new_epoch.applied_proposals.len(), 1);
1043
1044 assert_eq!(
1045 new_epoch
1046 .applied_proposals
1047 .into_iter()
1048 .filter(|p| matches!(p.proposal, Proposal::Add(_)))
1049 .count(),
1050 1
1051 );
1052
1053 assert_eq!(server.state.public_tree.get_leaf_nodes().len(), 2);
1054
1055 assert_eq!(alice.state, server.state);
1056 }
1057
1058 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1059 async fn external_group_rejects_commit_not_for_current_epoch() {
1060 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1061 let mut server = make_external_group(&alice).await;
1062
1063 let mut commit_output = alice.commit(vec![]).await.unwrap();
1064
1065 match commit_output.commit_message.payload {
1066 MlsMessagePayload::Plain(ref mut plain) => plain.content.epoch = 0,
1067 _ => panic!("Unexpected non-plaintext data"),
1068 };
1069
1070 let res = server
1071 .process_incoming_message(commit_output.commit_message)
1072 .await;
1073
1074 assert_matches!(res, Err(MlsError::InvalidEpoch));
1075 }
1076
1077 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1078 async fn external_group_can_reject_message_with_invalid_signature() {
1079 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1080
1081 let mut server = make_external_group_with_config(
1082 &alice,
1083 TestExternalClientBuilder::new_for_test().build_config(),
1084 )
1085 .await;
1086
1087 let mut commit_output = alice.commit(Vec::new()).await.unwrap();
1088
1089 match commit_output.commit_message.payload {
1090 MlsMessagePayload::Plain(ref mut plain) => plain.auth.signature = Vec::new().into(),
1091 _ => panic!("Unexpected non-plaintext data"),
1092 };
1093
1094 let res = server
1095 .process_incoming_message(commit_output.commit_message)
1096 .await;
1097
1098 assert_matches!(res, Err(MlsError::InvalidSignature));
1099 }
1100
1101 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1102 async fn external_group_rejects_unencrypted_application_message() {
1103 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1104 let mut server = make_external_group(&alice).await;
1105
1106 let plaintext = alice
1107 .make_plaintext(Content::Application(b"hello".to_vec().into()))
1108 .await;
1109
1110 let res = server.process_incoming_message(plaintext).await;
1111
1112 assert_matches!(res, Err(MlsError::UnencryptedApplicationMessage));
1113 }
1114
1115 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1116 async fn external_group_will_reject_unsupported_cipher_suites() {
1117 let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1118
1119 let config =
1120 TestExternalClientBuilder::new_for_test_disabling_cipher_suite(TEST_CIPHER_SUITE)
1121 .build_config();
1122
1123 let res = ExternalGroup::join(
1124 config,
1125 None,
1126 alice
1127 .group_info_message_allowing_ext_commit(true)
1128 .await
1129 .unwrap(),
1130 None,
1131 None,
1132 )
1133 .await
1134 .map(|_| ());
1135
1136 assert_matches!(
1137 res,
1138 Err(MlsError::UnsupportedCipherSuite(TEST_CIPHER_SUITE))
1139 );
1140 }
1141
1142 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1143 async fn external_group_will_reject_unsupported_protocol_versions() {
1144 let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1145
1146 let config = TestExternalClientBuilder::new_for_test().build_config();
1147
1148 let mut group_info = alice
1149 .group_info_message_allowing_ext_commit(true)
1150 .await
1151 .unwrap();
1152
1153 group_info.version = ProtocolVersion::from(64);
1154
1155 let res = ExternalGroup::join(config, None, group_info, None, None)
1156 .await
1157 .map(|_| ());
1158
1159 assert_matches!(
1160 res,
1161 Err(MlsError::UnsupportedProtocolVersion(v)) if v ==
1162 ProtocolVersion::from(64)
1163 );
1164 }
1165
1166 #[cfg(feature = "by_ref_proposal")]
1167 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1168 async fn setup_extern_proposal_test(
1169 extern_proposals_allowed: bool,
1170 ) -> (SigningIdentity, SignatureSecretKey, TestGroup) {
1171 let (server_identity, server_key) =
1172 get_test_signing_identity(TEST_CIPHER_SUITE, b"server").await;
1173
1174 let alice = test_group_two_members(
1175 TEST_PROTOCOL_VERSION,
1176 TEST_CIPHER_SUITE,
1177 extern_proposals_allowed.then(|| server_identity.clone()),
1178 )
1179 .await;
1180
1181 (server_identity, server_key, alice)
1182 }
1183
1184 #[cfg(feature = "by_ref_proposal")]
1185 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1186 async fn test_external_proposal(
1187 server: &mut ExternalGroup<TestExternalClientConfig>,
1188 alice: &mut TestGroup,
1189 external_proposal: MlsMessage,
1190 ) {
1191 let auth_content = external_proposal.clone().into_plaintext().unwrap().into();
1192
1193 let proposal_ref = ProposalRef::from_content(&server.cipher_suite_provider, &auth_content)
1194 .await
1195 .unwrap();
1196
1197 alice.process_message(external_proposal).await.unwrap();
1199
1200 let commit_output = alice.commit(vec![]).await.unwrap();
1202
1203 let commit = match commit_output
1204 .commit_message
1205 .clone()
1206 .into_plaintext()
1207 .unwrap()
1208 .content
1209 .content
1210 {
1211 Content::Commit(commit) => commit,
1212 _ => panic!("not a commit"),
1213 };
1214
1215 assert!(commit
1217 .proposals
1218 .contains(&ProposalOrRef::Reference(proposal_ref)));
1219
1220 alice.process_pending_commit().await.unwrap();
1221
1222 server
1223 .process_incoming_message(commit_output.commit_message)
1224 .await
1225 .unwrap();
1226
1227 assert_eq!(alice.state, server.state);
1228 }
1229
1230 #[cfg(feature = "by_ref_proposal")]
1231 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1232 async fn external_group_can_propose_add() {
1233 let (server_identity, server_key, mut alice) = setup_extern_proposal_test(true).await;
1234
1235 let mut server = make_external_group(&alice).await;
1236
1237 server.signing_data = Some((server_key, server_identity));
1238
1239 let charlie_key_package =
1240 test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "charlie").await;
1241
1242 let external_proposal = server
1243 .propose_add(charlie_key_package, vec![])
1244 .await
1245 .unwrap();
1246
1247 test_external_proposal(&mut server, &mut alice, external_proposal).await
1248 }
1249
1250 #[cfg(feature = "by_ref_proposal")]
1251 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1252 async fn external_group_can_propose_remove() {
1253 let (server_identity, server_key, mut alice) = setup_extern_proposal_test(true).await;
1254
1255 let mut server = make_external_group(&alice).await;
1256
1257 server.signing_data = Some((server_key, server_identity));
1258
1259 let external_proposal = server.propose_remove(1, vec![]).await.unwrap();
1260
1261 test_external_proposal(&mut server, &mut alice, external_proposal).await
1262 }
1263
1264 #[cfg(feature = "by_ref_proposal")]
1265 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1266 async fn external_group_external_proposal_not_allowed() {
1267 let (signing_id, secret_key, alice) = setup_extern_proposal_test(false).await;
1268 let mut server = make_external_group(&alice).await;
1269
1270 server.signing_data = Some((secret_key, signing_id));
1271
1272 let charlie_key_package =
1273 test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "charlie").await;
1274
1275 let res = server.propose_add(charlie_key_package, vec![]).await;
1276
1277 assert_matches!(res, Err(MlsError::ExternalProposalsDisabled));
1278 }
1279
1280 #[cfg(feature = "by_ref_proposal")]
1281 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1282 async fn external_group_external_signing_identity_invalid() {
1283 let (server_identity, server_key) =
1284 get_test_signing_identity(TEST_CIPHER_SUITE, b"server").await;
1285
1286 let alice = test_group_two_members(
1287 TEST_PROTOCOL_VERSION,
1288 TEST_CIPHER_SUITE,
1289 Some(
1290 get_test_signing_identity(TEST_CIPHER_SUITE, b"not server")
1291 .await
1292 .0,
1293 ),
1294 )
1295 .await;
1296
1297 let mut server = make_external_group(&alice).await;
1298
1299 server.signing_data = Some((server_key, server_identity));
1300
1301 let res = server.propose_remove(1, vec![]).await;
1302
1303 assert_matches!(res, Err(MlsError::InvalidExternalSigningIdentity));
1304 }
1305
1306 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1307 async fn external_group_errors_on_old_epoch() {
1308 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1309
1310 let mut server = make_external_group_with_config(
1311 &alice,
1312 TestExternalClientBuilder::new_for_test()
1313 .max_epoch_jitter(0)
1314 .build_config(),
1315 )
1316 .await;
1317
1318 let old_application_msg = alice
1319 .encrypt_application_message(&[], vec![])
1320 .await
1321 .unwrap();
1322
1323 let commit_output = alice.commit(vec![]).await.unwrap();
1324
1325 server
1326 .process_incoming_message(commit_output.commit_message)
1327 .await
1328 .unwrap();
1329
1330 let res = server.process_incoming_message(old_application_msg).await;
1331
1332 assert_matches!(res, Err(MlsError::InvalidEpoch));
1333 }
1334
1335 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1336 async fn proposals_can_be_cached_externally() {
1337 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1338
1339 let mut server = make_external_group_with_config(
1340 &alice,
1341 TestExternalClientBuilder::new_for_test()
1342 .cache_proposals(false)
1343 .build_config(),
1344 )
1345 .await;
1346
1347 let proposal = alice.propose_update(vec![]).await.unwrap();
1348
1349 let commit_output = alice.commit(vec![]).await.unwrap();
1350
1351 server
1352 .process_incoming_message(proposal.clone())
1353 .await
1354 .unwrap();
1355
1356 server.insert_proposal_from_message(proposal).await.unwrap();
1357
1358 server
1359 .process_incoming_message(commit_output.commit_message)
1360 .await
1361 .unwrap();
1362 }
1363
1364 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1365 async fn external_group_cached_proposals_returns_pending_proposals() {
1366 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1367 let mut server = make_external_group(&alice).await;
1368
1369 assert!(server.get_cached_proposals().is_empty());
1370
1371 let proposal = alice.propose_update(vec![]).await.unwrap();
1372 server.process_incoming_message(proposal).await.unwrap();
1373
1374 let cached = server.get_cached_proposals();
1375 assert_eq!(cached.len(), 1);
1376 assert!(matches!(cached[0].proposal(), Proposal::Update(_)));
1377 assert!(!cached[0].proposal_ref().is_empty());
1378
1379 let commit_output = alice.commit(vec![]).await.unwrap();
1380 alice.apply_pending_commit().await.unwrap();
1381 server
1382 .process_incoming_message(commit_output.commit_message)
1383 .await
1384 .unwrap();
1385
1386 assert!(server.get_cached_proposals().is_empty());
1387 }
1388
1389 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1390 async fn external_group_cached_proposals_returns_independent_copies() {
1391 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1392 let mut server = make_external_group(&alice).await;
1393
1394 let proposal = alice.propose_update(vec![]).await.unwrap();
1395 server.process_incoming_message(proposal).await.unwrap();
1396
1397 let mut cached1 = server.get_cached_proposals();
1398 let cached2 = server.get_cached_proposals();
1399
1400 assert_eq!(cached1.len(), 1);
1401 assert_eq!(cached2.len(), 1);
1402 assert_eq!(cached1[0].proposal_ref(), cached2[0].proposal_ref());
1403
1404 cached1.clear();
1405 assert!(cached1.is_empty());
1406 assert_eq!(cached2.len(), 1);
1407
1408 let cached3 = server.get_cached_proposals();
1409 assert_eq!(cached3.len(), 1);
1410 assert_eq!(cached2[0].proposal_ref(), cached3[0].proposal_ref());
1411 }
1412
1413 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1414 async fn external_group_exported_tree_matches_export_tree_bytes() {
1415 let mut alice = test_group_two_members(
1416 TEST_PROTOCOL_VERSION,
1417 TEST_CIPHER_SUITE,
1418 #[cfg(feature = "by_ref_proposal")]
1419 None,
1420 )
1421 .await;
1422
1423 let mut server = make_external_group(&alice).await;
1424
1425 let (_, add_commit) = alice.join("carol").await;
1428 server.process_incoming_message(add_commit).await.unwrap();
1429
1430 let commit_output = alice
1433 .commit_builder()
1434 .remove_member(1)
1435 .unwrap()
1436 .build()
1437 .await
1438 .unwrap();
1439 alice.process_pending_commit().await.unwrap();
1440
1441 server
1442 .process_incoming_message(commit_output.commit_message)
1443 .await
1444 .unwrap();
1445
1446 let borrowed = server.exported_tree();
1447
1448 let expected_bytes = server.export_tree().unwrap();
1450 assert_eq!(borrowed.to_bytes().unwrap(), expected_bytes);
1451
1452 assert!(borrowed
1455 .get_leaf(LeafIndex::unchecked(1))
1456 .unwrap()
1457 .is_none());
1458 assert!(borrowed
1459 .get_leaf(LeafIndex::unchecked(0))
1460 .unwrap()
1461 .is_some());
1462 assert!(borrowed
1463 .get_leaf(LeafIndex::unchecked(2))
1464 .unwrap()
1465 .is_some());
1466 }
1467
1468 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1469 async fn external_group_can_observe_since_creation() {
1470 let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1471
1472 let info = alice
1473 .group_info_message_allowing_ext_commit(true)
1474 .await
1475 .unwrap();
1476
1477 let config = TestExternalClientBuilder::new_for_test().build_config();
1478 let mut server = ExternalGroup::join(config, None, info, None, None)
1479 .await
1480 .unwrap();
1481
1482 for _ in 0..2 {
1483 let commit = alice.commit(vec![]).await.unwrap().commit_message;
1484 alice.process_pending_commit().await.unwrap();
1485 server.process_incoming_message(commit).await.unwrap();
1486 }
1487 }
1488
1489 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1490 async fn external_group_can_be_serialized_to_tls_encoding() {
1491 let server =
1492 make_external_group(&test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await).await;
1493
1494 let snapshot = server.snapshot().mls_encode_to_vec().unwrap();
1495 let snapshot_restored = ExternalSnapshot::mls_decode(&mut snapshot.as_slice()).unwrap();
1496
1497 assert_eq!(server.snapshot(), snapshot_restored);
1498 }
1499
1500 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1501 async fn legacy_snapshot_migration() {
1502 #[derive(MlsSize, MlsEncode)]
1503 struct LegacyExternalSnapshot {
1504 version: u16,
1505 state: RawGroupState,
1506 signing_data: Option<(SignatureSecretKey, SigningIdentity)>,
1507 }
1508
1509 let (server_identity, server_key, alice) = setup_extern_proposal_test(true).await;
1510 let server = make_external_group(&alice).await;
1511
1512 let legacy_snapshot = LegacyExternalSnapshot {
1513 version: *TEST_PROTOCOL_VERSION,
1514 state: server.snapshot().state,
1515 signing_data: Some((server_key, server_identity)),
1516 };
1517
1518 let legacy_snapshot_bytes = legacy_snapshot.mls_encode_to_vec().unwrap();
1519 let migrated_snapshot = ExternalSnapshot::mls_decode(&mut &*legacy_snapshot_bytes).unwrap();
1520
1521 assert_eq!(legacy_snapshot.state, migrated_snapshot.state);
1522 assert_eq!(*TEST_PROTOCOL_VERSION, migrated_snapshot.version);
1523 }
1524
1525 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1526 async fn external_group_can_validate_info() {
1527 let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1528 let mut server = make_external_group(&alice).await;
1529
1530 let info = alice
1531 .group_info_message_allowing_ext_commit(false)
1532 .await
1533 .unwrap();
1534
1535 let update = server.process_incoming_message(info.clone()).await.unwrap();
1536 let info = info.into_group_info().unwrap();
1537
1538 assert_matches!(update, ExternalReceivedMessage::GroupInfo(update_info) if update_info == info);
1539 }
1540
1541 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1542 async fn external_group_can_validate_key_package() {
1543 let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1544 let mut server = make_external_group(&alice).await;
1545
1546 let kp = test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "john").await;
1547
1548 let update = server.process_incoming_message(kp.clone()).await.unwrap();
1549 let kp = kp.into_key_package().unwrap();
1550
1551 assert_matches!(update, ExternalReceivedMessage::KeyPackage(update_kp) if update_kp == kp);
1552 }
1553
1554 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1555 async fn external_group_can_validate_welcome() {
1556 let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1557 let mut server = make_external_group(&alice).await;
1558
1559 let [welcome] = alice
1560 .commit_builder()
1561 .add_member(
1562 test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "john").await,
1563 )
1564 .unwrap()
1565 .build()
1566 .await
1567 .unwrap()
1568 .welcome_messages
1569 .try_into()
1570 .unwrap();
1571
1572 let update = server.process_incoming_message(welcome).await.unwrap();
1573
1574 assert_matches!(update, ExternalReceivedMessage::Welcome);
1575 }
1576
1577 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1578 async fn external_group_can_be_stored_without_tree() {
1579 let mut server =
1580 make_external_group(&test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await).await;
1581
1582 let snapshot_with_tree = server.snapshot().mls_encode_to_vec().unwrap();
1583
1584 let snapshot_without_tree = server
1585 .snapshot_without_ratchet_tree()
1586 .mls_encode_to_vec()
1587 .unwrap();
1588
1589 let tree = server.state.public_tree.nodes.mls_encode_to_vec().unwrap();
1590 let empty_tree = Vec::<u8>::new().mls_encode_to_vec().unwrap();
1591
1592 assert_eq!(
1593 snapshot_with_tree.len() - snapshot_without_tree.len(),
1594 tree.len() - empty_tree.len()
1595 );
1596
1597 let exported_tree = server.export_tree().unwrap();
1598
1599 let restored = ExternalClient::new(server.config.clone(), None)
1600 .load_group_with_ratchet_tree(
1601 ExternalSnapshot::from_bytes(&snapshot_without_tree).unwrap(),
1602 ExportedTree::from_bytes(&exported_tree).unwrap(),
1603 )
1604 .await
1605 .unwrap();
1606
1607 assert_eq!(restored.group_state(), server.group_state());
1608 }
1609}