Skip to main content

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