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