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