mls_rs/external_client/
group.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// Copyright by contributors to this project.
3// SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5use 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/// The result of processing an [ExternalGroup](ExternalGroup) message using
86/// [process_incoming_message](ExternalGroup::process_incoming_message)
87#[derive(Clone, Debug)]
88#[allow(clippy::large_enum_variant)]
89pub enum ExternalReceivedMessage {
90    /// State update as the result of a successful commit.
91    Commit(CommitMessageDescription),
92    /// Received proposal and its unique identifier.
93    Proposal(ProposalMessageDescription),
94    /// Encrypted message that can not be processed.
95    Ciphertext(ContentType),
96    /// Validated GroupInfo object
97    GroupInfo(GroupInfo),
98    /// Validated welcome message
99    Welcome,
100    /// Validated key package
101    KeyPackage(KeyPackage),
102}
103
104/// A handle to an observed group that can track plaintext control messages
105/// and the resulting group state.
106#[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    /// Process a message that was sent to the group.
172    ///
173    /// * Proposals will be stored in the group state and processed by the
174    /// same rules as a standard group.
175    ///
176    /// * Commits will result in the same outcome as a standard group.
177    /// However, the integrity of the resulting group state can only be partially
178    /// verified, since the external group does have access to the group
179    /// secrets required to do a complete check.
180    ///
181    /// * Application messages are always encrypted so they result in a no-op
182    /// that returns [ExternalReceivedMessage::Ciphertext]
183    ///
184    /// # Warning
185    ///
186    /// Processing an encrypted commit or proposal message has the same result
187    /// as processing an encrypted application message. Proper tracking of
188    /// the group state requires that all proposal and commit messages are
189    /// readable.
190    #[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    /// Process an inbound message for this group, providing additional context
205    /// with a message timestamp.
206    ///
207    /// Providing a timestamp is useful when the
208    /// [`IdentityProvider`](crate::IdentityProvider) in use by the group can
209    /// determine validity based on a timestamp. For example, this allows for
210    /// checking X.509 certificate expiration at the time when `message` was
211    /// received by a server rather than when a specific client asynchronously
212    /// received `message`
213    ///
214    /// See [`process_incoming_message`](Self::process_incoming_message) for
215    /// full details.
216    #[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    /// Replay a proposal message into the group skipping all validation steps.
233    #[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    /// Force insert a proposal directly into the internal state of the group
264    /// with no validation.
265    #[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    /// Create an external proposal to request that a group add a new member
275    ///
276    /// # Warning
277    ///
278    /// In order for the proposal generated by this function to be successfully
279    /// committed, the group needs to have `signing_identity` as an entry
280    /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
281    /// as part of its group context extensions.
282    #[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    /// Create an external proposal to request that a group remove an existing member
301    ///
302    /// # Warning
303    ///
304    /// In order for the proposal generated by this function to be successfully
305    /// committed, the group needs to have `signing_identity` as an entry
306    /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
307    /// as part of its group context extensions.
308    #[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        // Verify that this leaf is actually in the tree
318        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    /// Create an external proposal to request that a group inserts an external
328    /// pre shared key into its state.
329    ///
330    /// # Warning
331    ///
332    /// In order for the proposal generated by this function to be successfully
333    /// committed, the group needs to have `signing_identity` as an entry
334    /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
335    /// as part of its group context extensions.
336    #[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    /// Create an external proposal to request that a group adds a pre shared key
348    /// from a previous epoch to the current group state.
349    ///
350    /// # Warning
351    ///
352    /// In order for the proposal generated by this function to be successfully
353    /// committed, the group needs to have `signing_identity` as an entry
354    /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
355    /// as part of its group context extensions.
356    #[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    /// Create an external proposal to request that a group sets extensions stored in the group
385    /// state.
386    ///
387    /// # Warning
388    ///
389    /// In order for the proposal generated by this function to be successfully
390    /// committed, the group needs to have `signing_identity` as an entry
391    /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
392    /// as part of its group context extensions.
393    #[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    /// Create an external proposal to request that a group is reinitialized.
405    ///
406    /// # Warning
407    ///
408    /// In order for the proposal generated by this function to be successfully
409    /// committed, the group needs to have `signing_identity` as an entry
410    /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
411    /// as part of its group context extensions.
412    #[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    /// Create a custom proposal message.
439    ///
440    /// # Warning
441    ///
442    /// In order for the proposal generated by this function to be successfully
443    /// committed, the group needs to have `signing_identity` as an entry
444    /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
445    /// as part of its group context extensions.
446    #[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    /// Issue an external proposal.
458    ///
459    /// This function is useful for reissuing external proposals that
460    /// are returned in [crate::group::NewEpoch::unused_proposals]
461    /// after a commit is processed.
462    #[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    /// Delete all sent and received proposals cached for commit.
518    #[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    /// Get the current group context summarizing various information about the group.
529    #[inline(always)]
530    pub fn group_context(&self) -> &GroupContext {
531        &self.group_state().context
532    }
533
534    /// Export the current ratchet tree used within the group.
535    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    /// Get the current roster of the group.
544    #[inline(always)]
545    pub fn roster(&self) -> Roster<'_> {
546        self.group_state().public_tree.roster()
547    }
548
549    /// Get the
550    /// [transcript hash](https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol.html#name-transcript-hashes)
551    /// for the current epoch that the group is in.
552    #[inline(always)]
553    pub fn transcript_hash(&self) -> &Vec<u8> {
554        &self.group_state().context.confirmed_transcript_hash
555    }
556
557    /// Get the
558    /// [tree hash](https://www.rfc-editor.org/rfc/rfc9420.html#name-tree-hashes)
559    /// for the current epoch that the group is in.
560    #[inline(always)]
561    pub fn tree_hash(&self) -> &[u8] {
562        &self.group_state().context.tree_hash
563    }
564
565    /// Find a member based on their identity.
566    ///
567    /// Identities are matched based on the
568    /// [IdentityProvider](crate::IdentityProvider)
569    /// that this group was configured with.
570    #[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/// Serializable snapshot of an [ExternalGroup](ExternalGroup) state.
725#[derive(Debug, MlsEncode, MlsSize, MlsDecode, PartialEq, Clone)]
726pub struct ExternalSnapshot {
727    version: u16,
728    pub(crate) state: RawGroupState,
729}
730
731impl ExternalSnapshot {
732    /// Serialize the snapshot
733    pub fn to_bytes(&self) -> Result<Vec<u8>, MlsError> {
734        Ok(self.mls_encode_to_vec()?)
735    }
736
737    /// Deserialize the snapshot
738    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> {
739        Ok(Self::mls_decode(&mut &*bytes)?)
740    }
741
742    /// Group context encoded in the snapshot
743    pub fn context(&self) -> &GroupContext {
744        &self.state.context
745    }
746}
747
748impl<C> ExternalGroup<C>
749where
750    C: ExternalClientConfig + Clone,
751{
752    /// Create a snapshot of this group's current internal state.
753    pub fn snapshot(&self) -> ExternalSnapshot {
754        ExternalSnapshot {
755            state: RawGroupState::export(self.group_state()),
756            version: 1,
757        }
758    }
759
760    /// Create a snapshot of this group's current internal state.
761    /// The tree is not included in the state and can be stored
762    /// separately by calling [`Group::export_tree`].
763    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 receives the proposal
1169        alice.process_message(external_proposal).await.unwrap();
1170
1171        // Alice commits the proposal
1172        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        // The proposal should be in the resulting commit
1187        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}