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