core_crypto/mls/conversation/
commit.rs

1//! This table summarizes when a MLS group can create a commit or proposal:
2//!
3//! | can create handshake ? | 0 pend. Commit | 1 pend. Commit |
4//! |------------------------|----------------|----------------|
5//! | 0 pend. Proposal       | ✅              | ❌              |
6//! | 1+ pend. Proposal      | ✅              | ❌              |
7
8use openmls::prelude::MlsMessageOut;
9
10use mls_crypto_provider::MlsCryptoProvider;
11
12use super::{Error, Result};
13use crate::{
14    mls::MlsConversation,
15    prelude::{MlsError, MlsGroupInfoBundle, Session},
16};
17
18impl MlsConversation {
19    /// see [Client::commit_pending_proposals]
20    #[cfg_attr(test, crate::durable)]
21    pub(crate) async fn commit_pending_proposals(
22        &mut self,
23        client: &Session,
24        backend: &MlsCryptoProvider,
25    ) -> Result<Option<MlsCommitBundle>> {
26        if self.group.pending_proposals().count() == 0 {
27            return Ok(None);
28        }
29        let signer = &self.find_most_recent_credential_bundle(client).await?.signature_key;
30
31        let (commit, welcome, gi) = self
32            .group
33            .commit_to_pending_proposals(backend, signer)
34            .await
35            .map_err(MlsError::wrap("group commit to pending proposals"))?;
36        let group_info = MlsGroupInfoBundle::try_new_full_plaintext(gi.unwrap())?;
37
38        self.persist_group_when_changed(&backend.keystore(), false).await?;
39
40        Ok(Some(MlsCommitBundle {
41            welcome,
42            commit,
43            group_info,
44        }))
45    }
46}
47
48/// Returned when a commit is created
49#[derive(Debug, Clone)]
50pub struct MlsCommitBundle {
51    /// A welcome message if there are pending Add proposals
52    pub welcome: Option<MlsMessageOut>,
53    /// The commit message
54    pub commit: MlsMessageOut,
55    /// `GroupInfo` if the commit is merged
56    pub group_info: MlsGroupInfoBundle,
57}
58
59impl MlsCommitBundle {
60    /// Serializes both wrapped objects into TLS and return them as a tuple of byte arrays.
61    /// 0 -> welcome
62    /// 1 -> message
63    /// 2 -> public group state
64    #[allow(clippy::type_complexity)]
65    pub fn to_bytes_triple(self) -> Result<(Option<Vec<u8>>, Vec<u8>, MlsGroupInfoBundle)> {
66        use openmls::prelude::TlsSerializeTrait as _;
67        let welcome = self
68            .welcome
69            .as_ref()
70            .map(|w| {
71                w.tls_serialize_detached()
72                    .map_err(Error::tls_serialize("serialize welcome"))
73            })
74            .transpose()?;
75        let commit = self
76            .commit
77            .tls_serialize_detached()
78            .map_err(Error::tls_serialize("serialize commit"))?;
79        Ok((welcome, commit, self.group_info))
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use itertools::Itertools;
86    use openmls::prelude::SignaturePublicKey;
87    use wasm_bindgen_test::*;
88
89    use crate::test_utils::*;
90    use crate::transaction_context::Error as TransactionError;
91
92    use super::{Error, *};
93
94    wasm_bindgen_test_configure!(run_in_browser);
95
96    mod transport {
97        use super::*;
98        use std::sync::Arc;
99
100        #[apply(all_cred_cipher)]
101        #[wasm_bindgen_test]
102        async fn retry_should_work(case: TestContext) {
103            use crate::mls::conversation::Conversation as _;
104
105            let [alice, bob, charlie] = case.sessions().await;
106            Box::pin(async move {
107                // Create conversation
108                let conversation = case.create_conversation([&alice, &bob]).await;
109
110                // Bob produces a commit that Alice will receive only after she tried sending a commit
111                let commit = conversation.update_guarded_with(&bob).await;
112                let bob_epoch = commit.conversation().guard_of(&bob).await.epoch().await;
113                assert_eq!(2, bob_epoch);
114                let alice_epoch = commit.conversation().guard_of(&alice).await.epoch().await;
115                assert_eq!(1, alice_epoch);
116                let intermediate_commit = commit.message();
117                // Next time a commit is sent, process the intermediate commit and return retry, success the second time
118                let retry_provider = Arc::new(
119                    CoreCryptoTransportRetrySuccessProvider::default().with_intermediate_commits(
120                        alice.clone(),
121                        &[intermediate_commit],
122                        commit.conversation().id(),
123                    ),
124                );
125
126                alice.replace_transport(retry_provider.clone()).await;
127
128                // Send two commits and process them on bobs side
129                // For this second commit, the retry provider will first return retry and
130                // then success, but now without an intermediate commit
131                let id = commit.finish().advance_epoch().await.invite([&charlie]).await.id;
132
133                // Retry should have been returned twice
134                assert_eq!(retry_provider.retry_count().await, 2);
135                // Success should have been returned twice
136                assert_eq!(retry_provider.success_count().await, 2);
137
138                // Group is still in valid state
139                assert!(alice.try_talk_to(&id, &bob).await.is_ok());
140            })
141            .await;
142        }
143    }
144
145    mod add_members {
146        use super::*;
147        use std::sync::Arc;
148
149        #[apply(all_cred_cipher)]
150        #[wasm_bindgen_test]
151        async fn can_add_members_to_conversation(case: TestContext) {
152            let [alice_central, bob_central] = case.sessions().await;
153            Box::pin(async move {
154                let id = conversation_id();
155
156                alice_central
157                    .transaction
158                    .new_conversation(&id, case.credential_type, case.cfg.clone())
159                    .await
160                    .unwrap();
161                let bob = bob_central.rand_key_package(&case).await;
162                // First, abort commit transport
163                alice_central
164                    .transaction
165                    .set_transport_callbacks(Some(Arc::<CoreCryptoTransportAbortProvider>::default()))
166                    .await
167                    .unwrap();
168                alice_central
169                    .transaction
170                    .conversation(&id)
171                    .await
172                    .unwrap()
173                    .add_members(vec![bob.clone()])
174                    .await
175                    .unwrap_err();
176
177                // commit is not applied
178                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
179
180                let success_provider = Arc::<CoreCryptoTransportSuccessProvider>::default();
181                alice_central
182                    .transaction
183                    .set_transport_callbacks(Some(success_provider.clone()))
184                    .await
185                    .unwrap();
186                alice_central
187                    .transaction
188                    .conversation(&id)
189                    .await
190                    .unwrap()
191                    .add_members(vec![bob])
192                    .await
193                    .unwrap();
194
195                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
196                assert_eq!(
197                    alice_central
198                        .get_conversation_unchecked(&id)
199                        .await
200                        .group
201                        .group_id()
202                        .as_slice(),
203                    id
204                );
205                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
206                let commit = success_provider.latest_commit_bundle().await;
207                bob_central
208                    .transaction
209                    .process_welcome_message(commit.welcome.unwrap().into(), case.custom_cfg())
210                    .await
211                    .unwrap();
212                assert_eq!(
213                    alice_central.get_conversation_unchecked(&id).await.id(),
214                    bob_central.get_conversation_unchecked(&id).await.id()
215                );
216                assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
217                assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
218            })
219            .await
220        }
221
222        #[apply(all_cred_cipher)]
223        #[wasm_bindgen_test]
224        async fn should_return_valid_welcome(case: TestContext) {
225            let [alice_central, bob_central] = case.sessions().await;
226            Box::pin(async move {
227                let id = conversation_id();
228                alice_central
229                    .transaction
230                    .new_conversation(&id, case.credential_type, case.cfg.clone())
231                    .await
232                    .unwrap();
233
234                let bob = bob_central.rand_key_package(&case).await;
235                alice_central
236                    .transaction
237                    .conversation(&id)
238                    .await
239                    .unwrap()
240                    .add_members(vec![bob])
241                    .await
242                    .unwrap();
243
244                let welcome = alice_central
245                    .mls_transport()
246                    .await
247                    .latest_commit_bundle()
248                    .await
249                    .welcome
250                    .unwrap();
251
252                bob_central
253                    .transaction
254                    .process_welcome_message(welcome.into(), case.custom_cfg())
255                    .await
256                    .unwrap();
257                assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
258            })
259            .await
260        }
261
262        #[apply(all_cred_cipher)]
263        #[wasm_bindgen_test]
264        async fn should_return_valid_group_info(case: TestContext) {
265            let [alice_central, bob_central, mut guest_central] = case.sessions().await;
266            Box::pin(async move {
267                let id = conversation_id();
268                alice_central
269                    .transaction
270                    .new_conversation(&id, case.credential_type, case.cfg.clone())
271                    .await
272                    .unwrap();
273
274                let bob = bob_central.rand_key_package(&case).await;
275                alice_central
276                    .transaction
277                    .conversation(&id)
278                    .await
279                    .unwrap()
280                    .add_members(vec![bob])
281                    .await
282                    .unwrap();
283                let commit_bundle = alice_central.mls_transport().await.latest_commit_bundle().await;
284                let group_info = commit_bundle.group_info.get_group_info();
285
286                assert!(
287                    guest_central
288                        .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
289                        .await
290                        .is_ok()
291                );
292            })
293            .await
294        }
295    }
296
297    mod remove_members {
298        use super::*;
299
300        #[apply(all_cred_cipher)]
301        #[wasm_bindgen_test]
302        async fn alice_can_remove_bob_from_conversation(case: TestContext) {
303            let [alice, bob] = case.sessions().await;
304            Box::pin(async move {
305                let conversation = case.create_conversation([&alice, &bob]).await;
306                let id = conversation.remove(&bob).await.id;
307
308                let MlsCommitBundle { welcome, .. } = alice.mls_transport().await.latest_commit_bundle().await;
309                assert!(welcome.is_none());
310
311                assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 1);
312
313                // But has been removed from the conversation
314                assert!(matches!(
315                bob.transaction.conversation(&id).await.unwrap_err(),
316                TransactionError::Leaf(crate::LeafError::ConversationNotFound(ref i))
317                    if i == &id
318                ));
319                assert!(alice.try_talk_to(&id, &bob).await.is_err());
320            })
321            .await;
322        }
323
324        #[apply(all_cred_cipher)]
325        #[wasm_bindgen_test]
326        async fn should_return_valid_welcome(case: TestContext) {
327            let [alice_central, bob_central, mut guest_central] = case.sessions().await;
328            Box::pin(async move {
329                let id = conversation_id();
330
331                alice_central
332                    .transaction
333                    .new_conversation(&id, case.credential_type, case.cfg.clone())
334                    .await
335                    .unwrap();
336                alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
337
338                let proposal = alice_central
339                    .transaction
340                    .new_add_proposal(&id, guest_central.get_one_key_package(&case).await)
341                    .await
342                    .unwrap();
343                bob_central
344                    .transaction
345                    .conversation(&id)
346                    .await
347                    .unwrap()
348                    .decrypt_message(proposal.proposal.to_bytes().unwrap())
349                    .await
350                    .unwrap();
351
352                alice_central
353                    .transaction
354                    .conversation(&id)
355                    .await
356                    .unwrap()
357                    .remove_members(&[bob_central.get_client_id().await])
358                    .await
359                    .unwrap();
360
361                let welcome = alice_central.mls_transport().await.latest_welcome_message().await;
362
363                assert!(
364                    guest_central
365                        .try_join_from_welcome(&id, welcome.into(), case.custom_cfg(), vec![&alice_central])
366                        .await
367                        .is_ok()
368                );
369                // because Bob has been removed from the group
370                assert!(guest_central.try_talk_to(&id, &bob_central).await.is_err());
371            })
372            .await;
373        }
374
375        #[apply(all_cred_cipher)]
376        #[wasm_bindgen_test]
377        async fn should_return_valid_group_info(case: TestContext) {
378            let [alice_central, bob_central, mut guest_central] = case.sessions().await;
379            Box::pin(async move {
380                let id = conversation_id();
381
382                alice_central
383                    .transaction
384                    .new_conversation(&id, case.credential_type, case.cfg.clone())
385                    .await
386                    .unwrap();
387                alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
388
389                alice_central
390                    .transaction
391                    .conversation(&id)
392                    .await
393                    .unwrap()
394                    .remove_members(&[bob_central.get_client_id().await])
395                    .await
396                    .unwrap();
397
398                let commit_bundle = alice_central.mls_transport().await.latest_commit_bundle().await;
399
400                let group_info = commit_bundle.group_info.get_group_info();
401
402                assert!(
403                    guest_central
404                        .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
405                        .await
406                        .is_ok()
407                );
408                // because Bob has been removed from the group
409                assert!(guest_central.try_talk_to(&id, &bob_central).await.is_err());
410            })
411            .await;
412        }
413    }
414
415    mod update_keying_material {
416        use super::*;
417
418        #[apply(all_cred_cipher)]
419        #[wasm_bindgen_test]
420        async fn should_succeed(case: TestContext) {
421            let [alice_central, bob_central] = case.sessions().await;
422            Box::pin(async move {
423                let id = conversation_id();
424                alice_central
425                    .transaction
426                    .new_conversation(&id, case.credential_type, case.cfg.clone())
427                    .await
428                    .unwrap();
429                alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
430
431                let init_count = alice_central.transaction.count_entities().await;
432
433                let bob_keys = bob_central
434                    .get_conversation_unchecked(&id)
435                    .await
436                    .encryption_keys()
437                    .collect::<Vec<Vec<u8>>>();
438                let alice_keys = alice_central
439                    .get_conversation_unchecked(&id)
440                    .await
441                    .encryption_keys()
442                    .collect::<Vec<Vec<u8>>>();
443                assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
444
445                let alice_key = alice_central
446                    .encryption_key_of(&id, alice_central.get_client_id().await)
447                    .await;
448
449                // proposing the key update for alice
450                alice_central
451                    .transaction
452                    .conversation(&id)
453                    .await
454                    .unwrap()
455                    .update_key_material()
456                    .await
457                    .unwrap();
458                let MlsCommitBundle { commit, welcome, .. } =
459                    alice_central.mls_transport().await.latest_commit_bundle().await;
460                assert!(welcome.is_none());
461
462                assert!(
463                    !alice_central
464                        .get_conversation_unchecked(&id)
465                        .await
466                        .encryption_keys()
467                        .contains(&alice_key)
468                );
469
470                let alice_new_keys = alice_central
471                    .get_conversation_unchecked(&id)
472                    .await
473                    .encryption_keys()
474                    .collect::<Vec<_>>();
475                assert!(!alice_new_keys.contains(&alice_key));
476
477                // receiving the commit on bob's side (updating key from alice)
478                bob_central
479                    .transaction
480                    .conversation(&id)
481                    .await
482                    .unwrap()
483                    .decrypt_message(&commit.to_bytes().unwrap())
484                    .await
485                    .unwrap();
486
487                let bob_new_keys = bob_central
488                    .get_conversation_unchecked(&id)
489                    .await
490                    .encryption_keys()
491                    .collect::<Vec<_>>();
492                assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
493
494                // ensuring both can encrypt messages
495                assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
496
497                // make sure inline update commit + merge does not leak anything
498                // that's obvious since no new encryption keypair is created in this case
499                let final_count = alice_central.transaction.count_entities().await;
500                assert_eq!(init_count, final_count);
501            })
502            .await;
503        }
504
505        #[apply(all_cred_cipher)]
506        #[wasm_bindgen_test]
507        async fn should_create_welcome_for_pending_add_proposals(case: TestContext) {
508            let [alice_central, bob_central, charlie_central] = case.sessions().await;
509            Box::pin(async move {
510                let id = conversation_id();
511                alice_central
512                    .transaction
513                    .new_conversation(&id, case.credential_type, case.cfg.clone())
514                    .await
515                    .unwrap();
516                alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
517
518                let bob_keys = bob_central
519                    .get_conversation_unchecked(&id)
520                    .await
521                    .signature_keys()
522                    .collect::<Vec<SignaturePublicKey>>();
523                let alice_keys = alice_central
524                    .get_conversation_unchecked(&id)
525                    .await
526                    .signature_keys()
527                    .collect::<Vec<SignaturePublicKey>>();
528
529                // checking that the members on both sides are the same
530                assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
531
532                let alice_key = alice_central
533                    .encryption_key_of(&id, alice_central.get_client_id().await)
534                    .await;
535
536                // proposing adding charlie
537                let charlie_kp = charlie_central.get_one_key_package(&case).await;
538                let add_charlie_proposal = alice_central
539                    .transaction
540                    .new_add_proposal(&id, charlie_kp)
541                    .await
542                    .unwrap();
543
544                // receiving the proposal on Bob's side
545                bob_central
546                    .transaction
547                    .conversation(&id)
548                    .await
549                    .unwrap()
550                    .decrypt_message(add_charlie_proposal.proposal.to_bytes().unwrap())
551                    .await
552                    .unwrap();
553
554                assert!(
555                    alice_central
556                        .get_conversation_unchecked(&id)
557                        .await
558                        .encryption_keys()
559                        .contains(&alice_key)
560                );
561
562                // performing an update on Alice's key. this should generate a welcome for Charlie
563                alice_central
564                    .transaction
565                    .conversation(&id)
566                    .await
567                    .unwrap()
568                    .update_key_material()
569                    .await
570                    .unwrap();
571                let MlsCommitBundle { commit, welcome, .. } =
572                    alice_central.mls_transport().await.latest_commit_bundle().await;
573                assert!(welcome.is_some());
574                assert!(
575                    !alice_central
576                        .get_conversation_unchecked(&id)
577                        .await
578                        .encryption_keys()
579                        .contains(&alice_key)
580                );
581
582                // create the group on charlie's side
583                charlie_central
584                    .transaction
585                    .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
586                    .await
587                    .unwrap();
588
589                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 3);
590                assert_eq!(charlie_central.get_conversation_unchecked(&id).await.members().len(), 3);
591                // bob still didn't receive the message with the updated key and charlie's addition
592                assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
593
594                let alice_new_keys = alice_central
595                    .get_conversation_unchecked(&id)
596                    .await
597                    .encryption_keys()
598                    .collect::<Vec<Vec<u8>>>();
599                assert!(!alice_new_keys.contains(&alice_key));
600
601                // receiving the key update and the charlie's addition to the group
602                bob_central
603                    .transaction
604                    .conversation(&id)
605                    .await
606                    .unwrap()
607                    .decrypt_message(&commit.to_bytes().unwrap())
608                    .await
609                    .unwrap();
610                assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 3);
611
612                let bob_new_keys = bob_central
613                    .get_conversation_unchecked(&id)
614                    .await
615                    .encryption_keys()
616                    .collect::<Vec<Vec<u8>>>();
617                assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
618
619                // ensure all parties can encrypt messages
620                assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
621                assert!(bob_central.try_talk_to(&id, &charlie_central).await.is_ok());
622                assert!(charlie_central.try_talk_to(&id, &alice_central).await.is_ok());
623            })
624            .await;
625        }
626
627        #[apply(all_cred_cipher)]
628        #[wasm_bindgen_test]
629        async fn should_return_valid_welcome(case: TestContext) {
630            let [alice_central, bob_central, mut guest_central] = case.sessions().await;
631            Box::pin(async move {
632                let id = conversation_id();
633                alice_central
634                    .transaction
635                    .new_conversation(&id, case.credential_type, case.cfg.clone())
636                    .await
637                    .unwrap();
638                alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
639
640                let proposal = alice_central
641                    .transaction
642                    .new_add_proposal(&id, guest_central.get_one_key_package(&case).await)
643                    .await
644                    .unwrap()
645                    .proposal;
646                bob_central
647                    .transaction
648                    .conversation(&id)
649                    .await
650                    .unwrap()
651                    .decrypt_message(proposal.to_bytes().unwrap())
652                    .await
653                    .unwrap();
654
655                alice_central
656                    .transaction
657                    .conversation(&id)
658                    .await
659                    .unwrap()
660                    .update_key_material()
661                    .await
662                    .unwrap();
663                let MlsCommitBundle { commit, welcome, .. } =
664                    alice_central.mls_transport().await.latest_commit_bundle().await;
665
666                bob_central
667                    .transaction
668                    .conversation(&id)
669                    .await
670                    .unwrap()
671                    .decrypt_message(commit.to_bytes().unwrap())
672                    .await
673                    .unwrap();
674
675                assert!(
676                    guest_central
677                        .try_join_from_welcome(
678                            &id,
679                            welcome.unwrap().into(),
680                            case.custom_cfg(),
681                            vec![&alice_central, &bob_central]
682                        )
683                        .await
684                        .is_ok()
685                );
686            })
687            .await;
688        }
689
690        #[apply(all_cred_cipher)]
691        #[wasm_bindgen_test]
692        async fn should_return_valid_group_info(case: TestContext) {
693            let [alice_central, bob_central, mut guest_central] = case.sessions().await;
694            Box::pin(async move {
695                let id = conversation_id();
696                alice_central
697                    .transaction
698                    .new_conversation(&id, case.credential_type, case.cfg.clone())
699                    .await
700                    .unwrap();
701                alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
702
703                alice_central
704                    .transaction
705                    .conversation(&id)
706                    .await
707                    .unwrap()
708                    .update_key_material()
709                    .await
710                    .unwrap();
711                let group_info = alice_central.mls_transport().await.latest_group_info().await;
712                let group_info = group_info.get_group_info();
713
714                assert!(
715                    guest_central
716                        .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
717                        .await
718                        .is_ok()
719                );
720            })
721            .await;
722        }
723    }
724
725    mod commit_pending_proposals {
726        use super::*;
727
728        #[apply(all_cred_cipher)]
729        #[wasm_bindgen_test]
730        async fn should_create_a_commit_out_of_self_pending_proposals(case: TestContext) {
731            let [mut alice_central, bob_central] = case.sessions().await;
732            Box::pin(async move {
733                let id = conversation_id();
734                alice_central
735                    .transaction
736                    .new_conversation(&id, case.credential_type, case.cfg.clone())
737                    .await
738                    .unwrap();
739                alice_central
740                    .transaction
741                    .new_add_proposal(&id, bob_central.get_one_key_package(&case).await)
742                    .await
743                    .unwrap();
744                assert!(!alice_central.pending_proposals(&id).await.is_empty());
745                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
746                alice_central
747                    .transaction
748                    .conversation(&id)
749                    .await
750                    .unwrap()
751                    .commit_pending_proposals()
752                    .await
753                    .unwrap();
754                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
755
756                let welcome = alice_central.mls_transport().await.latest_commit_bundle().await.welcome;
757                bob_central
758                    .transaction
759                    .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
760                    .await
761                    .unwrap();
762                assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
763            })
764            .await;
765        }
766
767        #[apply(all_cred_cipher)]
768        #[wasm_bindgen_test]
769        async fn should_create_a_commit_out_of_pending_proposals_by_ref(case: TestContext) {
770            let [alice_central, mut bob_central, charlie_central] = case.sessions().await;
771            Box::pin(async move {
772                let id = conversation_id();
773                alice_central
774                    .transaction
775                    .new_conversation(&id, case.credential_type, case.cfg.clone())
776                    .await
777                    .unwrap();
778                alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
779                let proposal = bob_central
780                    .transaction
781                    .new_add_proposal(&id, charlie_central.get_one_key_package(&case).await)
782                    .await
783                    .unwrap();
784                assert!(!bob_central.pending_proposals(&id).await.is_empty());
785                assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
786                alice_central
787                    .transaction
788                    .conversation(&id)
789                    .await
790                    .unwrap()
791                    .decrypt_message(proposal.proposal.to_bytes().unwrap())
792                    .await
793                    .unwrap();
794
795                alice_central
796                    .transaction
797                    .conversation(&id)
798                    .await
799                    .unwrap()
800                    .commit_pending_proposals()
801                    .await
802                    .unwrap();
803                let commit = alice_central.mls_transport().await.latest_commit_bundle().await.commit;
804                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 3);
805
806                bob_central
807                    .transaction
808                    .conversation(&id)
809                    .await
810                    .unwrap()
811                    .decrypt_message(commit.to_bytes().unwrap())
812                    .await
813                    .unwrap();
814                assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 3);
815                assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
816            })
817            .await;
818        }
819
820        #[apply(all_cred_cipher)]
821        #[wasm_bindgen_test]
822        async fn should_return_valid_welcome(case: TestContext) {
823            let [alice_central, bob_central] = case.sessions().await;
824            Box::pin(async move {
825                let id = conversation_id();
826                alice_central
827                    .transaction
828                    .new_conversation(&id, case.credential_type, case.cfg.clone())
829                    .await
830                    .unwrap();
831                alice_central
832                    .transaction
833                    .new_add_proposal(&id, bob_central.get_one_key_package(&case).await)
834                    .await
835                    .unwrap();
836                alice_central
837                    .transaction
838                    .conversation(&id)
839                    .await
840                    .unwrap()
841                    .commit_pending_proposals()
842                    .await
843                    .unwrap();
844
845                let welcome = alice_central.mls_transport().await.latest_commit_bundle().await.welcome;
846
847                bob_central
848                    .transaction
849                    .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
850                    .await
851                    .unwrap();
852                assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
853            })
854            .await;
855        }
856
857        #[apply(all_cred_cipher)]
858        #[wasm_bindgen_test]
859        async fn should_return_valid_group_info(case: TestContext) {
860            let [alice_central, bob_central, mut guest_central] = case.sessions().await;
861            Box::pin(async move {
862                let id = conversation_id();
863                alice_central
864                    .transaction
865                    .new_conversation(&id, case.credential_type, case.cfg.clone())
866                    .await
867                    .unwrap();
868                alice_central
869                    .transaction
870                    .new_add_proposal(&id, bob_central.get_one_key_package(&case).await)
871                    .await
872                    .unwrap();
873                alice_central
874                    .transaction
875                    .conversation(&id)
876                    .await
877                    .unwrap()
878                    .commit_pending_proposals()
879                    .await
880                    .unwrap();
881                let commit_bundle = alice_central.mls_transport().await.latest_commit_bundle().await;
882                let group_info = commit_bundle.group_info.get_group_info();
883
884                assert!(
885                    guest_central
886                        .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
887                        .await
888                        .is_ok()
889                );
890            })
891            .await;
892        }
893    }
894
895    mod delivery_semantics {
896        use super::*;
897
898        #[apply(all_cred_cipher)]
899        #[wasm_bindgen_test]
900        async fn should_prevent_out_of_order_commits(case: TestContext) {
901            let [alice_central, bob_central] = case.sessions().await;
902            Box::pin(async move {
903                let id = conversation_id();
904                alice_central
905                    .transaction
906                    .new_conversation(&id, case.credential_type, case.cfg.clone())
907                    .await
908                    .unwrap();
909                alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
910
911                alice_central
912                    .transaction
913                    .conversation(&id)
914                    .await
915                    .unwrap()
916                    .update_key_material()
917                    .await
918                    .unwrap();
919                let commit1 = alice_central.mls_transport().await.latest_commit().await;
920                let commit1 = commit1.to_bytes().unwrap();
921                alice_central
922                    .transaction
923                    .conversation(&id)
924                    .await
925                    .unwrap()
926                    .update_key_material()
927                    .await
928                    .unwrap();
929                let commit2 = alice_central.mls_transport().await.latest_commit().await;
930                let commit2 = commit2.to_bytes().unwrap();
931
932                // fails when a commit is skipped
933                let out_of_order = bob_central
934                    .transaction
935                    .conversation(&id)
936                    .await
937                    .unwrap()
938                    .decrypt_message(&commit2)
939                    .await;
940                assert!(matches!(out_of_order.unwrap_err(), Error::BufferedFutureMessage { .. }));
941
942                // works in the right order though
943                // NB: here 'commit2' has been buffered so it is also applied when we decrypt commit1
944                bob_central
945                    .transaction
946                    .conversation(&id)
947                    .await
948                    .unwrap()
949                    .decrypt_message(&commit1)
950                    .await
951                    .unwrap();
952
953                // and then fails again when trying to decrypt a commit with an epoch in the past
954                let past_commit = bob_central
955                    .transaction
956                    .conversation(&id)
957                    .await
958                    .unwrap()
959                    .decrypt_message(&commit1)
960                    .await;
961                assert!(matches!(past_commit.unwrap_err(), Error::StaleCommit));
962            })
963            .await;
964        }
965
966        #[apply(all_cred_cipher)]
967        #[wasm_bindgen_test]
968        async fn should_prevent_replayed_encrypted_handshake_messages(case: TestContext) {
969            if !case.is_pure_ciphertext() {
970                return;
971            }
972
973            let [alice_central, bob_central] = case.sessions().await;
974            Box::pin(async move {
975                let id = conversation_id();
976                alice_central
977                    .transaction
978                    .new_conversation(&id, case.credential_type, case.cfg.clone())
979                    .await
980                    .unwrap();
981                alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
982
983                let proposal1 = alice_central
984                    .transaction
985                    .new_update_proposal(&id)
986                    .await
987                    .unwrap()
988                    .proposal;
989                let proposal2 = proposal1.clone();
990                alice_central
991                    .get_conversation_unchecked(&id)
992                    .await
993                    .group
994                    .clear_pending_proposals();
995
996                alice_central
997                    .transaction
998                    .conversation(&id)
999                    .await
1000                    .unwrap()
1001                    .update_key_material()
1002                    .await
1003                    .unwrap();
1004                let commit1 = alice_central.mls_transport().await.latest_commit().await;
1005                let commit2 = commit1.clone();
1006
1007                // replayed encrypted proposal should fail
1008                bob_central
1009                    .transaction
1010                    .conversation(&id)
1011                    .await
1012                    .unwrap()
1013                    .decrypt_message(proposal1.to_bytes().unwrap())
1014                    .await
1015                    .unwrap();
1016                assert!(matches!(
1017                    bob_central
1018                        .transaction
1019                        .conversation(&id)
1020                        .await
1021                        .unwrap()
1022                        .decrypt_message(proposal2.to_bytes().unwrap())
1023                        .await
1024                        .unwrap_err(),
1025                    Error::DuplicateMessage
1026                ));
1027                bob_central
1028                    .get_conversation_unchecked(&id)
1029                    .await
1030                    .group
1031                    .clear_pending_proposals();
1032
1033                // replayed encrypted commit should fail
1034                bob_central
1035                    .transaction
1036                    .conversation(&id)
1037                    .await
1038                    .unwrap()
1039                    .decrypt_message(commit1.to_bytes().unwrap())
1040                    .await
1041                    .unwrap();
1042                assert!(matches!(
1043                    bob_central
1044                        .transaction
1045                        .conversation(&id)
1046                        .await
1047                        .unwrap()
1048                        .decrypt_message(commit2.to_bytes().unwrap())
1049                        .await
1050                        .unwrap_err(),
1051                    Error::StaleCommit
1052                ));
1053            })
1054            .await;
1055        }
1056    }
1057}