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