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 
5 use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
6 use mls_rs_core::{
7     crypto::SignatureSecretKey, error::IntoAnyError, extension::ExtensionList, group::Member,
8     identity::IdentityProvider,
9 };
10 
11 use crate::{
12     cipher_suite::CipherSuite,
13     client::MlsError,
14     external_client::ExternalClientConfig,
15     group::{
16         cipher_suite_provider,
17         confirmation_tag::ConfirmationTag,
18         framing::PublicMessage,
19         member_from_leaf_node,
20         message_processor::{
21             ApplicationMessageDescription, CommitMessageDescription, EventOrContent,
22             MessageProcessor, ProposalMessageDescription, ProvisionalState,
23         },
24         snapshot::RawGroupState,
25         state::GroupState,
26         transcript_hash::InterimTranscriptHash,
27         validate_group_info_joiner, ContentType, ExportedTree, GroupContext, GroupInfo, Roster,
28         Welcome,
29     },
30     identity::SigningIdentity,
31     protocol_version::ProtocolVersion,
32     psk::AlwaysFoundPskStorage,
33     tree_kem::{node::LeafIndex, path_secret::PathSecret, TreeKemPrivate},
34     CryptoProvider, KeyPackage, MlsMessage,
35 };
36 
37 #[cfg(feature = "by_ref_proposal")]
38 use crate::{
39     group::{
40         framing::{Content, MlsMessagePayload},
41         message_processor::CachedProposal,
42         message_signature::AuthenticatedContent,
43         proposal::Proposal,
44         proposal_ref::ProposalRef,
45         Sender,
46     },
47     WireFormat,
48 };
49 
50 #[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))]
51 use crate::group::proposal::CustomProposal;
52 
53 #[cfg(feature = "by_ref_proposal")]
54 use mls_rs_core::{crypto::CipherSuiteProvider, psk::ExternalPskId};
55 
56 #[cfg(feature = "by_ref_proposal")]
57 use crate::{
58     extension::ExternalSendersExt,
59     group::proposal::{AddProposal, ReInitProposal, RemoveProposal},
60 };
61 
62 #[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
63 use crate::{
64     group::proposal::PreSharedKeyProposal,
65     psk::{
66         JustPreSharedKeyID, PreSharedKeyID, PskGroupId, PskNonce, ResumptionPSKUsage, ResumptionPsk,
67     },
68 };
69 
70 #[cfg(feature = "private_message")]
71 use crate::group::framing::PrivateMessage;
72 
73 use alloc::boxed::Box;
74 
75 /// The result of processing an [ExternalGroup](ExternalGroup) message using
76 /// [process_incoming_message](ExternalGroup::process_incoming_message)
77 #[derive(Clone, Debug)]
78 #[allow(clippy::large_enum_variant)]
79 pub enum ExternalReceivedMessage {
80     /// State update as the result of a successful commit.
81     Commit(CommitMessageDescription),
82     /// Received proposal and its unique identifier.
83     Proposal(ProposalMessageDescription),
84     /// Encrypted message that can not be processed.
85     Ciphertext(ContentType),
86     /// Validated GroupInfo object
87     GroupInfo(GroupInfo),
88     /// Validated welcome message
89     Welcome,
90     /// Validated key package
91     KeyPackage(KeyPackage),
92 }
93 
94 /// A handle to an observed group that can track plaintext control messages
95 /// and the resulting group state.
96 #[derive(Clone)]
97 pub struct ExternalGroup<C>
98 where
99     C: ExternalClientConfig,
100 {
101     pub(crate) config: C,
102     pub(crate) cipher_suite_provider: <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider,
103     pub(crate) state: GroupState,
104     pub(crate) signing_data: Option<(SignatureSecretKey, SigningIdentity)>,
105 }
106 
107 impl<C: ExternalClientConfig + Clone> ExternalGroup<C> {
108     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
join( config: C, signing_data: Option<(SignatureSecretKey, SigningIdentity)>, group_info: MlsMessage, tree_data: Option<ExportedTree<'_>>, ) -> Result<Self, MlsError>109     pub(crate) async fn join(
110         config: C,
111         signing_data: Option<(SignatureSecretKey, SigningIdentity)>,
112         group_info: MlsMessage,
113         tree_data: Option<ExportedTree<'_>>,
114     ) -> Result<Self, MlsError> {
115         let protocol_version = group_info.version;
116 
117         if !config.version_supported(protocol_version) {
118             return Err(MlsError::UnsupportedProtocolVersion(protocol_version));
119         }
120 
121         let group_info = group_info
122             .into_group_info()
123             .ok_or(MlsError::UnexpectedMessageType)?;
124 
125         let cipher_suite_provider = cipher_suite_provider(
126             config.crypto_provider(),
127             group_info.group_context.cipher_suite,
128         )?;
129 
130         let public_tree = validate_group_info_joiner(
131             protocol_version,
132             &group_info,
133             tree_data,
134             &config.identity_provider(),
135             &cipher_suite_provider,
136         )
137         .await?;
138 
139         let interim_transcript_hash = InterimTranscriptHash::create(
140             &cipher_suite_provider,
141             &group_info.group_context.confirmed_transcript_hash,
142             &group_info.confirmation_tag,
143         )
144         .await?;
145 
146         Ok(Self {
147             config,
148             signing_data,
149             state: GroupState::new(
150                 group_info.group_context,
151                 public_tree,
152                 interim_transcript_hash,
153                 group_info.confirmation_tag,
154             ),
155             cipher_suite_provider,
156         })
157     }
158 
159     /// Process a message that was sent to the group.
160     ///
161     /// * Proposals will be stored in the group state and processed by the
162     /// same rules as a standard group.
163     ///
164     /// * Commits will result in the same outcome as a standard group.
165     /// However, the integrity of the resulting group state can only be partially
166     /// verified, since the external group does have access to the group
167     /// secrets required to do a complete check.
168     ///
169     /// * Application messages are always encrypted so they result in a no-op
170     /// that returns [ExternalReceivedMessage::Ciphertext]
171     ///
172     /// # Warning
173     ///
174     /// Processing an encrypted commit or proposal message has the same result
175     /// as processing an encrypted application message. Proper tracking of
176     /// the group state requires that all proposal and commit messages are
177     /// readable.
178     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
process_incoming_message( &mut self, message: MlsMessage, ) -> Result<ExternalReceivedMessage, MlsError>179     pub async fn process_incoming_message(
180         &mut self,
181         message: MlsMessage,
182     ) -> Result<ExternalReceivedMessage, MlsError> {
183         MessageProcessor::process_incoming_message(
184             self,
185             message,
186             #[cfg(feature = "by_ref_proposal")]
187             self.config.cache_proposals(),
188         )
189         .await
190     }
191 
192     /// Replay a proposal message into the group skipping all validation steps.
193     #[cfg(feature = "by_ref_proposal")]
194     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
insert_proposal_from_message( &mut self, message: MlsMessage, ) -> Result<(), MlsError>195     pub async fn insert_proposal_from_message(
196         &mut self,
197         message: MlsMessage,
198     ) -> Result<(), MlsError> {
199         let ptxt = match message.payload {
200             MlsMessagePayload::Plain(p) => Ok(p),
201             _ => Err(MlsError::UnexpectedMessageType),
202         }?;
203 
204         let auth_content: AuthenticatedContent = ptxt.into();
205 
206         let proposal_ref =
207             ProposalRef::from_content(&self.cipher_suite_provider, &auth_content).await?;
208 
209         let sender = auth_content.content.sender;
210 
211         let proposal = match auth_content.content.content {
212             Content::Proposal(p) => Ok(*p),
213             _ => Err(MlsError::UnexpectedMessageType),
214         }?;
215 
216         self.group_state_mut()
217             .proposals
218             .insert(proposal_ref, proposal, sender);
219 
220         Ok(())
221     }
222 
223     /// Force insert a proposal directly into the internal state of the group
224     /// with no validation.
225     #[cfg(feature = "by_ref_proposal")]
insert_proposal(&mut self, proposal: CachedProposal)226     pub fn insert_proposal(&mut self, proposal: CachedProposal) {
227         self.group_state_mut().proposals.insert(
228             proposal.proposal_ref,
229             proposal.proposal,
230             proposal.sender,
231         )
232     }
233 
234     /// Create an external proposal to request that a group add a new member
235     ///
236     /// # Warning
237     ///
238     /// In order for the proposal generated by this function to be successfully
239     /// committed, the group needs to have `signing_identity` as an entry
240     /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
241     /// as part of its group context extensions.
242     #[cfg(feature = "by_ref_proposal")]
243     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
propose_add( &mut self, key_package: MlsMessage, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>244     pub async fn propose_add(
245         &mut self,
246         key_package: MlsMessage,
247         authenticated_data: Vec<u8>,
248     ) -> Result<MlsMessage, MlsError> {
249         let key_package = key_package
250             .into_key_package()
251             .ok_or(MlsError::UnexpectedMessageType)?;
252 
253         self.propose(
254             Proposal::Add(alloc::boxed::Box::new(AddProposal { key_package })),
255             authenticated_data,
256         )
257         .await
258     }
259 
260     /// Create an external proposal to request that a group remove an existing member
261     ///
262     /// # Warning
263     ///
264     /// In order for the proposal generated by this function to be successfully
265     /// committed, the group needs to have `signing_identity` as an entry
266     /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
267     /// as part of its group context extensions.
268     #[cfg(feature = "by_ref_proposal")]
269     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
propose_remove( &mut self, index: u32, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>270     pub async fn propose_remove(
271         &mut self,
272         index: u32,
273         authenticated_data: Vec<u8>,
274     ) -> Result<MlsMessage, MlsError> {
275         let to_remove = LeafIndex(index);
276 
277         // Verify that this leaf is actually in the tree
278         self.group_state().public_tree.get_leaf_node(to_remove)?;
279 
280         self.propose(
281             Proposal::Remove(RemoveProposal { to_remove }),
282             authenticated_data,
283         )
284         .await
285     }
286 
287     /// Create an external proposal to request that a group inserts an external
288     /// pre shared key into its state.
289     ///
290     /// # Warning
291     ///
292     /// In order for the proposal generated by this function to be successfully
293     /// committed, the group needs to have `signing_identity` as an entry
294     /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
295     /// as part of its group context extensions.
296     #[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
297     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
propose_external_psk( &mut self, psk: ExternalPskId, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>298     pub async fn propose_external_psk(
299         &mut self,
300         psk: ExternalPskId,
301         authenticated_data: Vec<u8>,
302     ) -> Result<MlsMessage, MlsError> {
303         let proposal = self.psk_proposal(JustPreSharedKeyID::External(psk))?;
304         self.propose(proposal, authenticated_data).await
305     }
306 
307     /// Create an external proposal to request that a group adds a pre shared key
308     /// from a previous epoch to the current group state.
309     ///
310     /// # Warning
311     ///
312     /// In order for the proposal generated by this function to be successfully
313     /// committed, the group needs to have `signing_identity` as an entry
314     /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
315     /// as part of its group context extensions.
316     #[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
317     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
propose_resumption_psk( &mut self, psk_epoch: u64, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>318     pub async fn propose_resumption_psk(
319         &mut self,
320         psk_epoch: u64,
321         authenticated_data: Vec<u8>,
322     ) -> Result<MlsMessage, MlsError> {
323         let key_id = ResumptionPsk {
324             psk_epoch,
325             usage: ResumptionPSKUsage::Application,
326             psk_group_id: PskGroupId(self.group_context().group_id().to_vec()),
327         };
328 
329         let proposal = self.psk_proposal(JustPreSharedKeyID::Resumption(key_id))?;
330         self.propose(proposal, authenticated_data).await
331     }
332 
333     #[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
psk_proposal(&self, key_id: JustPreSharedKeyID) -> Result<Proposal, MlsError>334     fn psk_proposal(&self, key_id: JustPreSharedKeyID) -> Result<Proposal, MlsError> {
335         Ok(Proposal::Psk(PreSharedKeyProposal {
336             psk: PreSharedKeyID {
337                 key_id,
338                 psk_nonce: PskNonce::random(&self.cipher_suite_provider)
339                     .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))?,
340             },
341         }))
342     }
343 
344     /// Create an external proposal to request that a group sets extensions stored in the group
345     /// state.
346     ///
347     /// # Warning
348     ///
349     /// In order for the proposal generated by this function to be successfully
350     /// committed, the group needs to have `signing_identity` as an entry
351     /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
352     /// as part of its group context extensions.
353     #[cfg(feature = "by_ref_proposal")]
354     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
propose_group_context_extensions( &mut self, extensions: ExtensionList, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>355     pub async fn propose_group_context_extensions(
356         &mut self,
357         extensions: ExtensionList,
358         authenticated_data: Vec<u8>,
359     ) -> Result<MlsMessage, MlsError> {
360         let proposal = Proposal::GroupContextExtensions(extensions);
361         self.propose(proposal, authenticated_data).await
362     }
363 
364     /// Create an external proposal to request that a group is reinitialized.
365     ///
366     /// # Warning
367     ///
368     /// In order for the proposal generated by this function to be successfully
369     /// committed, the group needs to have `signing_identity` as an entry
370     /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
371     /// as part of its group context extensions.
372     #[cfg(feature = "by_ref_proposal")]
373     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
propose_reinit( &mut self, group_id: Option<Vec<u8>>, version: ProtocolVersion, cipher_suite: CipherSuite, extensions: ExtensionList, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>374     pub async fn propose_reinit(
375         &mut self,
376         group_id: Option<Vec<u8>>,
377         version: ProtocolVersion,
378         cipher_suite: CipherSuite,
379         extensions: ExtensionList,
380         authenticated_data: Vec<u8>,
381     ) -> Result<MlsMessage, MlsError> {
382         let group_id = group_id.map(Ok).unwrap_or_else(|| {
383             self.cipher_suite_provider
384                 .random_bytes_vec(self.cipher_suite_provider.kdf_extract_size())
385                 .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))
386         })?;
387 
388         let proposal = Proposal::ReInit(ReInitProposal {
389             group_id,
390             version,
391             cipher_suite,
392             extensions,
393         });
394 
395         self.propose(proposal, authenticated_data).await
396     }
397 
398     /// Create a custom proposal message.
399     ///
400     /// # Warning
401     ///
402     /// In order for the proposal generated by this function to be successfully
403     /// committed, the group needs to have `signing_identity` as an entry
404     /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt)
405     /// as part of its group context extensions.
406     #[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))]
407     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
propose_custom( &mut self, proposal: CustomProposal, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>408     pub async fn propose_custom(
409         &mut self,
410         proposal: CustomProposal,
411         authenticated_data: Vec<u8>,
412     ) -> Result<MlsMessage, MlsError> {
413         self.propose(Proposal::Custom(proposal), authenticated_data)
414             .await
415     }
416 
417     #[cfg(feature = "by_ref_proposal")]
418     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
propose( &mut self, proposal: Proposal, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError>419     async fn propose(
420         &mut self,
421         proposal: Proposal,
422         authenticated_data: Vec<u8>,
423     ) -> Result<MlsMessage, MlsError> {
424         let (signer, signing_identity) =
425             self.signing_data.as_ref().ok_or(MlsError::SignerNotFound)?;
426 
427         let external_senders_ext = self
428             .state
429             .context
430             .extensions
431             .get_as::<ExternalSendersExt>()?
432             .ok_or(MlsError::ExternalProposalsDisabled)?;
433 
434         let sender_index = external_senders_ext
435             .allowed_senders
436             .iter()
437             .position(|allowed_signer| signing_identity == allowed_signer)
438             .ok_or(MlsError::InvalidExternalSigningIdentity)?;
439 
440         let sender = Sender::External(sender_index as u32);
441 
442         let auth_content = AuthenticatedContent::new_signed(
443             &self.cipher_suite_provider,
444             &self.state.context,
445             sender,
446             Content::Proposal(Box::new(proposal.clone())),
447             signer,
448             WireFormat::PublicMessage,
449             authenticated_data,
450         )
451         .await?;
452 
453         self.state.proposals.insert(
454             ProposalRef::from_content(&self.cipher_suite_provider, &auth_content).await?,
455             proposal,
456             sender,
457         );
458 
459         let plaintext = PublicMessage {
460             content: auth_content.content,
461             auth: auth_content.auth,
462             membership_tag: None,
463         };
464 
465         Ok(MlsMessage::new(
466             self.group_context().version(),
467             MlsMessagePayload::Plain(plaintext),
468         ))
469     }
470 
471     /// Delete all sent and received proposals cached for commit.
472     #[cfg(feature = "by_ref_proposal")]
clear_proposal_cache(&mut self)473     pub fn clear_proposal_cache(&mut self) {
474         self.state.proposals.clear()
475     }
476 
477     #[inline(always)]
group_state(&self) -> &GroupState478     pub(crate) fn group_state(&self) -> &GroupState {
479         &self.state
480     }
481 
482     /// Get the current group context summarizing various information about the group.
483     #[inline(always)]
group_context(&self) -> &GroupContext484     pub fn group_context(&self) -> &GroupContext {
485         &self.group_state().context
486     }
487 
488     /// Export the current ratchet tree used within the group.
export_tree(&self) -> Result<Vec<u8>, MlsError>489     pub fn export_tree(&self) -> Result<Vec<u8>, MlsError> {
490         self.group_state()
491             .public_tree
492             .nodes
493             .mls_encode_to_vec()
494             .map_err(Into::into)
495     }
496 
497     /// Get the current roster of the group.
498     #[inline(always)]
roster(&self) -> Roster499     pub fn roster(&self) -> Roster {
500         self.group_state().public_tree.roster()
501     }
502 
503     /// Get the
504     /// [transcript hash](https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol.html#name-transcript-hashes)
505     /// for the current epoch that the group is in.
506     #[inline(always)]
transcript_hash(&self) -> &Vec<u8>507     pub fn transcript_hash(&self) -> &Vec<u8> {
508         &self.group_state().context.confirmed_transcript_hash
509     }
510 
511     /// Get the
512     /// [tree hash](https://www.rfc-editor.org/rfc/rfc9420.html#name-tree-hashes)
513     /// for the current epoch that the group is in.
514     #[inline(always)]
tree_hash(&self) -> &[u8]515     pub fn tree_hash(&self) -> &[u8] {
516         &self.group_state().context.tree_hash
517     }
518 
519     /// Find a member based on their identity.
520     ///
521     /// Identities are matched based on the
522     /// [IdentityProvider](crate::IdentityProvider)
523     /// that this group was configured with.
524     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
get_member_with_identity( &self, identity_id: &SigningIdentity, ) -> Result<Member, MlsError>525     pub async fn get_member_with_identity(
526         &self,
527         identity_id: &SigningIdentity,
528     ) -> Result<Member, MlsError> {
529         let identity = self
530             .identity_provider()
531             .identity(identity_id, self.group_context().extensions())
532             .await
533             .map_err(|error| MlsError::IdentityProviderError(error.into_any_error()))?;
534 
535         let tree = &self.group_state().public_tree;
536 
537         #[cfg(feature = "tree_index")]
538         let index = tree.get_leaf_node_with_identity(&identity);
539 
540         #[cfg(not(feature = "tree_index"))]
541         let index = tree
542             .get_leaf_node_with_identity(
543                 &identity,
544                 &self.identity_provider(),
545                 self.group_context().extensions(),
546             )
547             .await?;
548 
549         let index = index.ok_or(MlsError::MemberNotFound)?;
550         let node = self.group_state().public_tree.get_leaf_node(index)?;
551 
552         Ok(member_from_leaf_node(node, index))
553     }
554 }
555 
556 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
557 #[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
558 #[cfg_attr(
559     all(not(target_arch = "wasm32"), mls_build_async),
560     maybe_async::must_be_async
561 )]
562 impl<C> MessageProcessor for ExternalGroup<C>
563 where
564     C: ExternalClientConfig + Clone,
565 {
566     type MlsRules = C::MlsRules;
567     type IdentityProvider = C::IdentityProvider;
568     type PreSharedKeyStorage = AlwaysFoundPskStorage;
569     type OutputType = ExternalReceivedMessage;
570     type CipherSuiteProvider = <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider;
571 
572     #[cfg(feature = "private_message")]
self_index(&self) -> Option<LeafIndex>573     fn self_index(&self) -> Option<LeafIndex> {
574         None
575     }
576 
mls_rules(&self) -> Self::MlsRules577     fn mls_rules(&self) -> Self::MlsRules {
578         self.config.mls_rules()
579     }
580 
581     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
verify_plaintext_authentication( &self, message: PublicMessage, ) -> Result<EventOrContent<Self::OutputType>, MlsError>582     async fn verify_plaintext_authentication(
583         &self,
584         message: PublicMessage,
585     ) -> Result<EventOrContent<Self::OutputType>, MlsError> {
586         let auth_content = crate::group::message_verifier::verify_plaintext_authentication(
587             &self.cipher_suite_provider,
588             message,
589             None,
590             None,
591             &self.state,
592         )
593         .await?;
594 
595         Ok(EventOrContent::Content(auth_content))
596     }
597 
598     #[cfg(feature = "private_message")]
process_ciphertext( &mut self, cipher_text: &PrivateMessage, ) -> Result<EventOrContent<Self::OutputType>, MlsError>599     async fn process_ciphertext(
600         &mut self,
601         cipher_text: &PrivateMessage,
602     ) -> Result<EventOrContent<Self::OutputType>, MlsError> {
603         Ok(EventOrContent::Event(ExternalReceivedMessage::Ciphertext(
604             cipher_text.content_type,
605         )))
606     }
607 
update_key_schedule( &mut self, _secrets: Option<(TreeKemPrivate, PathSecret)>, interim_transcript_hash: InterimTranscriptHash, confirmation_tag: &ConfirmationTag, provisional_public_state: ProvisionalState, ) -> Result<(), MlsError>608     async fn update_key_schedule(
609         &mut self,
610         _secrets: Option<(TreeKemPrivate, PathSecret)>,
611         interim_transcript_hash: InterimTranscriptHash,
612         confirmation_tag: &ConfirmationTag,
613         provisional_public_state: ProvisionalState,
614     ) -> Result<(), MlsError> {
615         self.state.context = provisional_public_state.group_context;
616         #[cfg(feature = "by_ref_proposal")]
617         self.state.proposals.clear();
618         self.state.interim_transcript_hash = interim_transcript_hash;
619         self.state.public_tree = provisional_public_state.public_tree;
620         self.state.confirmation_tag = confirmation_tag.clone();
621 
622         Ok(())
623     }
624 
identity_provider(&self) -> Self::IdentityProvider625     fn identity_provider(&self) -> Self::IdentityProvider {
626         self.config.identity_provider()
627     }
628 
psk_storage(&self) -> Self::PreSharedKeyStorage629     fn psk_storage(&self) -> Self::PreSharedKeyStorage {
630         AlwaysFoundPskStorage
631     }
632 
group_state(&self) -> &GroupState633     fn group_state(&self) -> &GroupState {
634         &self.state
635     }
636 
group_state_mut(&mut self) -> &mut GroupState637     fn group_state_mut(&mut self) -> &mut GroupState {
638         &mut self.state
639     }
640 
can_continue_processing(&self, _provisional_state: &ProvisionalState) -> bool641     fn can_continue_processing(&self, _provisional_state: &ProvisionalState) -> bool {
642         true
643     }
644 
645     #[cfg(feature = "private_message")]
min_epoch_available(&self) -> Option<u64>646     fn min_epoch_available(&self) -> Option<u64> {
647         self.config
648             .max_epoch_jitter()
649             .map(|j| self.state.context.epoch - j)
650     }
651 
cipher_suite_provider(&self) -> &Self::CipherSuiteProvider652     fn cipher_suite_provider(&self) -> &Self::CipherSuiteProvider {
653         &self.cipher_suite_provider
654     }
655 }
656 
657 /// Serializable snapshot of an [ExternalGroup](ExternalGroup) state.
658 #[derive(Debug, MlsEncode, MlsSize, MlsDecode, PartialEq, Clone)]
659 pub struct ExternalSnapshot {
660     version: u16,
661     state: RawGroupState,
662     signing_data: Option<(SignatureSecretKey, SigningIdentity)>,
663 }
664 
665 impl ExternalSnapshot {
666     /// Serialize the snapshot
to_bytes(&self) -> Result<Vec<u8>, MlsError>667     pub fn to_bytes(&self) -> Result<Vec<u8>, MlsError> {
668         Ok(self.mls_encode_to_vec()?)
669     }
670 
671     /// Deserialize the snapshot
from_bytes(bytes: &[u8]) -> Result<Self, MlsError>672     pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> {
673         Ok(Self::mls_decode(&mut &*bytes)?)
674     }
675 }
676 
677 impl<C> ExternalGroup<C>
678 where
679     C: ExternalClientConfig + Clone,
680 {
681     /// Create a snapshot of this group's current internal state.
snapshot(&self) -> ExternalSnapshot682     pub fn snapshot(&self) -> ExternalSnapshot {
683         ExternalSnapshot {
684             state: RawGroupState::export(self.group_state()),
685             version: 1,
686             signing_data: self.signing_data.clone(),
687         }
688     }
689 
690     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
from_snapshot( config: C, snapshot: ExternalSnapshot, ) -> Result<Self, MlsError>691     pub(crate) async fn from_snapshot(
692         config: C,
693         snapshot: ExternalSnapshot,
694     ) -> Result<Self, MlsError> {
695         #[cfg(feature = "tree_index")]
696         let identity_provider = config.identity_provider();
697 
698         let cipher_suite_provider = cipher_suite_provider(
699             config.crypto_provider(),
700             snapshot.state.context.cipher_suite,
701         )?;
702 
703         Ok(ExternalGroup {
704             config,
705             signing_data: snapshot.signing_data,
706             state: snapshot
707                 .state
708                 .import(
709                     #[cfg(feature = "tree_index")]
710                     &identity_provider,
711                 )
712                 .await?,
713             cipher_suite_provider,
714         })
715     }
716 }
717 
718 impl From<CommitMessageDescription> for ExternalReceivedMessage {
from(value: CommitMessageDescription) -> Self719     fn from(value: CommitMessageDescription) -> Self {
720         ExternalReceivedMessage::Commit(value)
721     }
722 }
723 
724 impl TryFrom<ApplicationMessageDescription> for ExternalReceivedMessage {
725     type Error = MlsError;
726 
try_from(_: ApplicationMessageDescription) -> Result<Self, Self::Error>727     fn try_from(_: ApplicationMessageDescription) -> Result<Self, Self::Error> {
728         Err(MlsError::UnencryptedApplicationMessage)
729     }
730 }
731 
732 impl From<ProposalMessageDescription> for ExternalReceivedMessage {
from(value: ProposalMessageDescription) -> Self733     fn from(value: ProposalMessageDescription) -> Self {
734         ExternalReceivedMessage::Proposal(value)
735     }
736 }
737 
738 impl From<GroupInfo> for ExternalReceivedMessage {
from(value: GroupInfo) -> Self739     fn from(value: GroupInfo) -> Self {
740         ExternalReceivedMessage::GroupInfo(value)
741     }
742 }
743 
744 impl From<Welcome> for ExternalReceivedMessage {
from(_: Welcome) -> Self745     fn from(_: Welcome) -> Self {
746         ExternalReceivedMessage::Welcome
747     }
748 }
749 
750 impl From<KeyPackage> for ExternalReceivedMessage {
from(value: KeyPackage) -> Self751     fn from(value: KeyPackage) -> Self {
752         ExternalReceivedMessage::KeyPackage(value)
753     }
754 }
755 
756 #[cfg(test)]
757 pub(crate) mod test_utils {
758     use crate::{
759         external_client::tests_utils::{TestExternalClientBuilder, TestExternalClientConfig},
760         group::test_utils::TestGroup,
761     };
762 
763     use super::ExternalGroup;
764 
765     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
make_external_group( group: &TestGroup, ) -> ExternalGroup<TestExternalClientConfig>766     pub(crate) async fn make_external_group(
767         group: &TestGroup,
768     ) -> ExternalGroup<TestExternalClientConfig> {
769         make_external_group_with_config(
770             group,
771             TestExternalClientBuilder::new_for_test().build_config(),
772         )
773         .await
774     }
775 
776     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
make_external_group_with_config( group: &TestGroup, config: TestExternalClientConfig, ) -> ExternalGroup<TestExternalClientConfig>777     pub(crate) async fn make_external_group_with_config(
778         group: &TestGroup,
779         config: TestExternalClientConfig,
780     ) -> ExternalGroup<TestExternalClientConfig> {
781         ExternalGroup::join(
782             config,
783             None,
784             group
785                 .group
786                 .group_info_message_allowing_ext_commit(true)
787                 .await
788                 .unwrap(),
789             None,
790         )
791         .await
792         .unwrap()
793     }
794 }
795 
796 #[cfg(test)]
797 mod tests {
798     use super::test_utils::make_external_group;
799     use crate::{
800         cipher_suite::CipherSuite,
801         client::{
802             test_utils::{TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION},
803             MlsError,
804         },
805         crypto::{test_utils::TestCryptoProvider, SignatureSecretKey},
806         extension::ExternalSendersExt,
807         external_client::{
808             group::test_utils::make_external_group_with_config,
809             tests_utils::{TestExternalClientBuilder, TestExternalClientConfig},
810             ExternalGroup, ExternalReceivedMessage, ExternalSnapshot,
811         },
812         group::{
813             framing::{Content, MlsMessagePayload},
814             proposal::{AddProposal, Proposal, ProposalOrRef},
815             proposal_ref::ProposalRef,
816             test_utils::{test_group, TestGroup},
817             ProposalMessageDescription,
818         },
819         identity::{test_utils::get_test_signing_identity, SigningIdentity},
820         key_package::test_utils::{test_key_package, test_key_package_message},
821         protocol_version::ProtocolVersion,
822         ExtensionList, MlsMessage,
823     };
824     use assert_matches::assert_matches;
825     use mls_rs_codec::{MlsDecode, MlsEncode};
826 
827     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
test_group_with_one_commit(v: ProtocolVersion, cs: CipherSuite) -> TestGroup828     async fn test_group_with_one_commit(v: ProtocolVersion, cs: CipherSuite) -> TestGroup {
829         let mut group = test_group(v, cs).await;
830         group.group.commit(Vec::new()).await.unwrap();
831         group.process_pending_commit().await.unwrap();
832         group
833     }
834 
835     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
test_group_two_members( v: ProtocolVersion, cs: CipherSuite, #[cfg(feature = "by_ref_proposal")] ext_identity: Option<SigningIdentity>, ) -> TestGroup836     async fn test_group_two_members(
837         v: ProtocolVersion,
838         cs: CipherSuite,
839         #[cfg(feature = "by_ref_proposal")] ext_identity: Option<SigningIdentity>,
840     ) -> TestGroup {
841         let mut group = test_group_with_one_commit(v, cs).await;
842 
843         let bob_key_package = test_key_package_message(v, cs, "bob").await;
844 
845         let mut commit_builder = group
846             .group
847             .commit_builder()
848             .add_member(bob_key_package)
849             .unwrap();
850 
851         #[cfg(feature = "by_ref_proposal")]
852         if let Some(ext_signer) = ext_identity {
853             let mut ext_list = ExtensionList::new();
854 
855             ext_list
856                 .set_from(ExternalSendersExt {
857                     allowed_senders: vec![ext_signer],
858                 })
859                 .unwrap();
860 
861             commit_builder = commit_builder.set_group_context_ext(ext_list).unwrap();
862         }
863 
864         commit_builder.build().await.unwrap();
865 
866         group.process_pending_commit().await.unwrap();
867         group
868     }
869 
870     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_can_be_created()871     async fn external_group_can_be_created() {
872         for (v, cs) in ProtocolVersion::all().flat_map(|v| {
873             TestCryptoProvider::all_supported_cipher_suites()
874                 .into_iter()
875                 .map(move |cs| (v, cs))
876         }) {
877             make_external_group(&test_group_with_one_commit(v, cs).await).await;
878         }
879     }
880 
881     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_can_process_commit()882     async fn external_group_can_process_commit() {
883         let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
884         let mut server = make_external_group(&alice).await;
885         let commit_output = alice.group.commit(Vec::new()).await.unwrap();
886         alice.group.apply_pending_commit().await.unwrap();
887 
888         server
889             .process_incoming_message(commit_output.commit_message)
890             .await
891             .unwrap();
892 
893         assert_eq!(alice.group.state, server.state);
894     }
895 
896     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_can_process_proposals_by_reference()897     async fn external_group_can_process_proposals_by_reference() {
898         let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
899         let mut server = make_external_group(&alice).await;
900 
901         let bob_key_package =
902             test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
903 
904         let add_proposal = Proposal::Add(Box::new(AddProposal {
905             key_package: bob_key_package,
906         }));
907 
908         let packet = alice.propose(add_proposal.clone()).await;
909 
910         let proposal_process = server.process_incoming_message(packet).await.unwrap();
911 
912         assert_matches!(
913             proposal_process,
914             ExternalReceivedMessage::Proposal(ProposalMessageDescription { ref proposal, ..}) if proposal == &add_proposal
915         );
916 
917         let commit_output = alice.group.commit(vec![]).await.unwrap();
918         alice.group.apply_pending_commit().await.unwrap();
919 
920         let commit_result = server
921             .process_incoming_message(commit_output.commit_message)
922             .await
923             .unwrap();
924 
925         #[cfg(feature = "state_update")]
926         assert_matches!(
927             commit_result,
928             ExternalReceivedMessage::Commit(commit_description)
929                 if commit_description.state_update.roster_update.added().iter().any(|added| added.index == 1)
930         );
931 
932         #[cfg(not(feature = "state_update"))]
933         assert_matches!(commit_result, ExternalReceivedMessage::Commit(_));
934 
935         assert_eq!(alice.group.state, server.state);
936     }
937 
938     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_can_process_commit_adding_member()939     async fn external_group_can_process_commit_adding_member() {
940         let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
941         let mut server = make_external_group(&alice).await;
942         let (_, commit) = alice.join("bob").await;
943 
944         let update = match server.process_incoming_message(commit).await.unwrap() {
945             ExternalReceivedMessage::Commit(update) => update.state_update,
946             _ => panic!("Expected processed commit"),
947         };
948 
949         #[cfg(feature = "state_update")]
950         assert_eq!(update.roster_update.added().len(), 1);
951 
952         assert_eq!(server.state.public_tree.get_leaf_nodes().len(), 2);
953 
954         assert_eq!(alice.group.state, server.state);
955     }
956 
957     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_rejects_commit_not_for_current_epoch()958     async fn external_group_rejects_commit_not_for_current_epoch() {
959         let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
960         let mut server = make_external_group(&alice).await;
961 
962         let mut commit_output = alice.group.commit(vec![]).await.unwrap();
963 
964         match commit_output.commit_message.payload {
965             MlsMessagePayload::Plain(ref mut plain) => plain.content.epoch = 0,
966             _ => panic!("Unexpected non-plaintext data"),
967         };
968 
969         let res = server
970             .process_incoming_message(commit_output.commit_message)
971             .await;
972 
973         assert_matches!(res, Err(MlsError::InvalidEpoch));
974     }
975 
976     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_can_reject_message_with_invalid_signature()977     async fn external_group_can_reject_message_with_invalid_signature() {
978         let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
979 
980         let mut server = make_external_group_with_config(
981             &alice,
982             TestExternalClientBuilder::new_for_test().build_config(),
983         )
984         .await;
985 
986         let mut commit_output = alice.group.commit(Vec::new()).await.unwrap();
987 
988         match commit_output.commit_message.payload {
989             MlsMessagePayload::Plain(ref mut plain) => plain.auth.signature = Vec::new().into(),
990             _ => panic!("Unexpected non-plaintext data"),
991         };
992 
993         let res = server
994             .process_incoming_message(commit_output.commit_message)
995             .await;
996 
997         assert_matches!(res, Err(MlsError::InvalidSignature));
998     }
999 
1000     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_rejects_unencrypted_application_message()1001     async fn external_group_rejects_unencrypted_application_message() {
1002         let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1003         let mut server = make_external_group(&alice).await;
1004 
1005         let plaintext = alice
1006             .make_plaintext(Content::Application(b"hello".to_vec().into()))
1007             .await;
1008 
1009         let res = server.process_incoming_message(plaintext).await;
1010 
1011         assert_matches!(res, Err(MlsError::UnencryptedApplicationMessage));
1012     }
1013 
1014     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_will_reject_unsupported_cipher_suites()1015     async fn external_group_will_reject_unsupported_cipher_suites() {
1016         let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1017 
1018         let config =
1019             TestExternalClientBuilder::new_for_test_disabling_cipher_suite(TEST_CIPHER_SUITE)
1020                 .build_config();
1021 
1022         let res = ExternalGroup::join(
1023             config,
1024             None,
1025             alice
1026                 .group
1027                 .group_info_message_allowing_ext_commit(true)
1028                 .await
1029                 .unwrap(),
1030             None,
1031         )
1032         .await
1033         .map(|_| ());
1034 
1035         assert_matches!(
1036             res,
1037             Err(MlsError::UnsupportedCipherSuite(TEST_CIPHER_SUITE))
1038         );
1039     }
1040 
1041     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_will_reject_unsupported_protocol_versions()1042     async fn external_group_will_reject_unsupported_protocol_versions() {
1043         let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1044 
1045         let config = TestExternalClientBuilder::new_for_test().build_config();
1046 
1047         let mut group_info = alice
1048             .group
1049             .group_info_message_allowing_ext_commit(true)
1050             .await
1051             .unwrap();
1052 
1053         group_info.version = ProtocolVersion::from(64);
1054 
1055         let res = ExternalGroup::join(config, None, group_info, None)
1056             .await
1057             .map(|_| ());
1058 
1059         assert_matches!(
1060             res,
1061             Err(MlsError::UnsupportedProtocolVersion(v)) if v ==
1062                 ProtocolVersion::from(64)
1063         );
1064     }
1065 
1066     #[cfg(feature = "by_ref_proposal")]
1067     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
setup_extern_proposal_test( extern_proposals_allowed: bool, ) -> (SigningIdentity, SignatureSecretKey, TestGroup)1068     async fn setup_extern_proposal_test(
1069         extern_proposals_allowed: bool,
1070     ) -> (SigningIdentity, SignatureSecretKey, TestGroup) {
1071         let (server_identity, server_key) =
1072             get_test_signing_identity(TEST_CIPHER_SUITE, b"server").await;
1073 
1074         let alice = test_group_two_members(
1075             TEST_PROTOCOL_VERSION,
1076             TEST_CIPHER_SUITE,
1077             extern_proposals_allowed.then(|| server_identity.clone()),
1078         )
1079         .await;
1080 
1081         (server_identity, server_key, alice)
1082     }
1083 
1084     #[cfg(feature = "by_ref_proposal")]
1085     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
test_external_proposal( server: &mut ExternalGroup<TestExternalClientConfig>, alice: &mut TestGroup, external_proposal: MlsMessage, )1086     async fn test_external_proposal(
1087         server: &mut ExternalGroup<TestExternalClientConfig>,
1088         alice: &mut TestGroup,
1089         external_proposal: MlsMessage,
1090     ) {
1091         let auth_content = external_proposal.clone().into_plaintext().unwrap().into();
1092 
1093         let proposal_ref = ProposalRef::from_content(&server.cipher_suite_provider, &auth_content)
1094             .await
1095             .unwrap();
1096 
1097         // Alice receives the proposal
1098         alice.process_message(external_proposal).await.unwrap();
1099 
1100         // Alice commits the proposal
1101         let commit_output = alice.group.commit(vec![]).await.unwrap();
1102 
1103         let commit = match commit_output
1104             .commit_message
1105             .clone()
1106             .into_plaintext()
1107             .unwrap()
1108             .content
1109             .content
1110         {
1111             Content::Commit(commit) => commit,
1112             _ => panic!("not a commit"),
1113         };
1114 
1115         // The proposal should be in the resulting commit
1116         assert!(commit
1117             .proposals
1118             .contains(&ProposalOrRef::Reference(proposal_ref)));
1119 
1120         alice.process_pending_commit().await.unwrap();
1121 
1122         server
1123             .process_incoming_message(commit_output.commit_message)
1124             .await
1125             .unwrap();
1126 
1127         assert_eq!(alice.group.state, server.state);
1128     }
1129 
1130     #[cfg(feature = "by_ref_proposal")]
1131     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_can_propose_add()1132     async fn external_group_can_propose_add() {
1133         let (server_identity, server_key, mut alice) = setup_extern_proposal_test(true).await;
1134 
1135         let mut server = make_external_group(&alice).await;
1136 
1137         server.signing_data = Some((server_key, server_identity));
1138 
1139         let charlie_key_package =
1140             test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "charlie").await;
1141 
1142         let external_proposal = server
1143             .propose_add(charlie_key_package, vec![])
1144             .await
1145             .unwrap();
1146 
1147         test_external_proposal(&mut server, &mut alice, external_proposal).await
1148     }
1149 
1150     #[cfg(feature = "by_ref_proposal")]
1151     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_can_propose_remove()1152     async fn external_group_can_propose_remove() {
1153         let (server_identity, server_key, mut alice) = setup_extern_proposal_test(true).await;
1154 
1155         let mut server = make_external_group(&alice).await;
1156 
1157         server.signing_data = Some((server_key, server_identity));
1158 
1159         let external_proposal = server.propose_remove(1, vec![]).await.unwrap();
1160 
1161         test_external_proposal(&mut server, &mut alice, external_proposal).await
1162     }
1163 
1164     #[cfg(feature = "by_ref_proposal")]
1165     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_external_proposal_not_allowed()1166     async fn external_group_external_proposal_not_allowed() {
1167         let (signing_id, secret_key, alice) = setup_extern_proposal_test(false).await;
1168         let mut server = make_external_group(&alice).await;
1169 
1170         server.signing_data = Some((secret_key, signing_id));
1171 
1172         let charlie_key_package =
1173             test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "charlie").await;
1174 
1175         let res = server.propose_add(charlie_key_package, vec![]).await;
1176 
1177         assert_matches!(res, Err(MlsError::ExternalProposalsDisabled));
1178     }
1179 
1180     #[cfg(feature = "by_ref_proposal")]
1181     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_external_signing_identity_invalid()1182     async fn external_group_external_signing_identity_invalid() {
1183         let (server_identity, server_key) =
1184             get_test_signing_identity(TEST_CIPHER_SUITE, b"server").await;
1185 
1186         let alice = test_group_two_members(
1187             TEST_PROTOCOL_VERSION,
1188             TEST_CIPHER_SUITE,
1189             Some(
1190                 get_test_signing_identity(TEST_CIPHER_SUITE, b"not server")
1191                     .await
1192                     .0,
1193             ),
1194         )
1195         .await;
1196 
1197         let mut server = make_external_group(&alice).await;
1198 
1199         server.signing_data = Some((server_key, server_identity));
1200 
1201         let res = server.propose_remove(1, vec![]).await;
1202 
1203         assert_matches!(res, Err(MlsError::InvalidExternalSigningIdentity));
1204     }
1205 
1206     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_errors_on_old_epoch()1207     async fn external_group_errors_on_old_epoch() {
1208         let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1209 
1210         let mut server = make_external_group_with_config(
1211             &alice,
1212             TestExternalClientBuilder::new_for_test()
1213                 .max_epoch_jitter(0)
1214                 .build_config(),
1215         )
1216         .await;
1217 
1218         let old_application_msg = alice
1219             .group
1220             .encrypt_application_message(&[], vec![])
1221             .await
1222             .unwrap();
1223 
1224         let commit_output = alice.group.commit(vec![]).await.unwrap();
1225 
1226         server
1227             .process_incoming_message(commit_output.commit_message)
1228             .await
1229             .unwrap();
1230 
1231         let res = server.process_incoming_message(old_application_msg).await;
1232 
1233         assert_matches!(res, Err(MlsError::InvalidEpoch));
1234     }
1235 
1236     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
proposals_can_be_cached_externally()1237     async fn proposals_can_be_cached_externally() {
1238         let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1239 
1240         let mut server = make_external_group_with_config(
1241             &alice,
1242             TestExternalClientBuilder::new_for_test()
1243                 .cache_proposals(false)
1244                 .build_config(),
1245         )
1246         .await;
1247 
1248         let proposal = alice.group.propose_update(vec![]).await.unwrap();
1249 
1250         let commit_output = alice.group.commit(vec![]).await.unwrap();
1251 
1252         server
1253             .process_incoming_message(proposal.clone())
1254             .await
1255             .unwrap();
1256 
1257         server.insert_proposal_from_message(proposal).await.unwrap();
1258 
1259         server
1260             .process_incoming_message(commit_output.commit_message)
1261             .await
1262             .unwrap();
1263     }
1264 
1265     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_can_observe_since_creation()1266     async fn external_group_can_observe_since_creation() {
1267         let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1268 
1269         let info = alice
1270             .group
1271             .group_info_message_allowing_ext_commit(true)
1272             .await
1273             .unwrap();
1274 
1275         let config = TestExternalClientBuilder::new_for_test().build_config();
1276         let mut server = ExternalGroup::join(config, None, info, None).await.unwrap();
1277 
1278         for _ in 0..2 {
1279             let commit = alice.group.commit(vec![]).await.unwrap().commit_message;
1280             alice.process_pending_commit().await.unwrap();
1281             server.process_incoming_message(commit).await.unwrap();
1282         }
1283     }
1284 
1285     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_can_be_serialized_to_tls_encoding()1286     async fn external_group_can_be_serialized_to_tls_encoding() {
1287         let server =
1288             make_external_group(&test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await).await;
1289 
1290         let snapshot = server.snapshot().mls_encode_to_vec().unwrap();
1291         let snapshot_restored = ExternalSnapshot::mls_decode(&mut snapshot.as_slice()).unwrap();
1292 
1293         let server_restored =
1294             ExternalGroup::from_snapshot(server.config.clone(), snapshot_restored)
1295                 .await
1296                 .unwrap();
1297 
1298         assert_eq!(server.group_state(), server_restored.group_state());
1299     }
1300 
1301     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_can_validate_info()1302     async fn external_group_can_validate_info() {
1303         let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1304         let mut server = make_external_group(&alice).await;
1305 
1306         let info = alice
1307             .group
1308             .group_info_message_allowing_ext_commit(false)
1309             .await
1310             .unwrap();
1311 
1312         let update = server.process_incoming_message(info.clone()).await.unwrap();
1313         let info = info.into_group_info().unwrap();
1314 
1315         assert_matches!(update, ExternalReceivedMessage::GroupInfo(update_info) if update_info == info);
1316     }
1317 
1318     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_can_validate_key_package()1319     async fn external_group_can_validate_key_package() {
1320         let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1321         let mut server = make_external_group(&alice).await;
1322 
1323         let kp = test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "john").await;
1324 
1325         let update = server.process_incoming_message(kp.clone()).await.unwrap();
1326         let kp = kp.into_key_package().unwrap();
1327 
1328         assert_matches!(update, ExternalReceivedMessage::KeyPackage(update_kp) if update_kp == kp);
1329     }
1330 
1331     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_group_can_validate_welcome()1332     async fn external_group_can_validate_welcome() {
1333         let mut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1334         let mut server = make_external_group(&alice).await;
1335 
1336         let [welcome] = alice
1337             .group
1338             .commit_builder()
1339             .add_member(
1340                 test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "john").await,
1341             )
1342             .unwrap()
1343             .build()
1344             .await
1345             .unwrap()
1346             .welcome_messages
1347             .try_into()
1348             .unwrap();
1349 
1350         let update = server.process_incoming_message(welcome).await.unwrap();
1351 
1352         assert_matches!(update, ExternalReceivedMessage::Welcome);
1353     }
1354 }
1355