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