1use openmls::prelude::{KeyPackageIn, LeafNode, LeafNodeIndex, MlsMessageOut};
9
10use mls_crypto_provider::MlsCryptoProvider;
11
12use super::MlsConversation;
13use crate::context::CentralContext;
14use crate::{
15 e2e_identity::init_certificates::NewCrlDistributionPoint,
16 mls::credential::{
17 crl::{extract_crl_uris_from_credentials, get_new_crl_distribution_points},
18 CredentialBundle,
19 },
20 prelude::{Client, ClientId, ConversationId, CryptoError, CryptoResult, MlsError, MlsGroupInfoBundle},
21};
22
23impl CentralContext {
24 #[cfg_attr(test, crate::idempotent)]
39 pub async fn add_members_to_conversation(
40 &self,
41 id: &ConversationId,
42 key_packages: Vec<KeyPackageIn>,
43 ) -> CryptoResult<MlsConversationCreationMessage> {
44 let client = self.mls_client().await?;
45 if let Some(callbacks) = self.callbacks().await?.as_ref() {
46 let client_id = client.id().await?;
47 if !callbacks.authorize(id.clone(), client_id).await {
48 return Err(CryptoError::Unauthorized);
49 }
50 }
51 self.get_conversation(id)
52 .await?
53 .write()
54 .await
55 .add_members(&client, key_packages, &self.mls_provider().await?)
56 .await
57 }
58
59 #[cfg_attr(test, crate::idempotent)]
73 pub async fn remove_members_from_conversation(
74 &self,
75 id: &ConversationId,
76 clients: &[ClientId],
77 ) -> CryptoResult<MlsCommitBundle> {
78 let client = self.mls_client().await?;
79 if let Some(callbacks) = self.callbacks().await?.as_ref() {
80 let client_id = client.id().await?;
81 if !callbacks.authorize(id.clone(), client_id).await {
82 return Err(CryptoError::Unauthorized);
83 }
84 }
85 self.get_conversation(id)
86 .await?
87 .write()
88 .await
89 .remove_members(&client, clients, &self.mls_provider().await?)
90 .await
91 }
92
93 #[cfg_attr(test, crate::idempotent)]
107 pub async fn update_keying_material(&self, id: &ConversationId) -> CryptoResult<MlsCommitBundle> {
108 let client = self.mls_client().await?;
109 self.get_conversation(id)
110 .await?
111 .write()
112 .await
113 .update_keying_material(&client, &self.mls_provider().await?, None, None)
114 .await
115 }
116
117 #[cfg_attr(test, crate::idempotent)]
128 pub async fn commit_pending_proposals(&self, id: &ConversationId) -> CryptoResult<Option<MlsCommitBundle>> {
129 let client = self.mls_client().await?;
130 self.get_conversation(id)
131 .await?
132 .write()
133 .await
134 .commit_pending_proposals(&client, &self.mls_provider().await?)
135 .await
136 }
137}
138
139impl MlsConversation {
141 #[cfg_attr(test, crate::durable)]
144 pub(crate) async fn add_members(
145 &mut self,
146 client: &Client,
147 key_packages: Vec<KeyPackageIn>,
148 backend: &MlsCryptoProvider,
149 ) -> CryptoResult<MlsConversationCreationMessage> {
150 let signer = &self.find_most_recent_credential_bundle(client).await?.signature_key;
151
152 let crl_new_distribution_points = get_new_crl_distribution_points(
154 backend,
155 extract_crl_uris_from_credentials(key_packages.iter().filter_map(|kp| {
156 let mls_credential = kp.credential().mls_credential();
157 if matches!(mls_credential, openmls::prelude::MlsCredentialType::X509(_)) {
158 Some(mls_credential)
159 } else {
160 None
161 }
162 }))?,
163 )
164 .await?;
165
166 let (commit, welcome, gi) = self
167 .group
168 .add_members(backend, signer, key_packages)
169 .await
170 .map_err(MlsError::from)?;
171
172 let gi = gi.ok_or(CryptoError::ImplementationError)?;
174 let group_info = MlsGroupInfoBundle::try_new_full_plaintext(gi)?;
175
176 self.persist_group_when_changed(&backend.keystore(), false).await?;
177
178 Ok(MlsConversationCreationMessage {
179 welcome,
180 commit,
181 group_info,
182 crl_new_distribution_points,
183 })
184 }
185
186 #[cfg_attr(test, crate::durable)]
189 pub(crate) async fn remove_members(
190 &mut self,
191 client: &Client,
192 clients: &[ClientId],
193 backend: &MlsCryptoProvider,
194 ) -> CryptoResult<MlsCommitBundle> {
195 let member_kps = self
196 .group
197 .members()
198 .filter(|kp| {
199 clients
200 .iter()
201 .any(move |client_id| client_id.as_slice() == kp.credential.identity())
202 })
203 .try_fold(vec![], |mut acc, kp| -> CryptoResult<Vec<LeafNodeIndex>> {
204 acc.push(kp.index);
205 Ok(acc)
206 })?;
207
208 let signer = &self.find_most_recent_credential_bundle(client).await?.signature_key;
209
210 let (commit, welcome, gi) = self
211 .group
212 .remove_members(backend, signer, &member_kps)
213 .await
214 .map_err(MlsError::from)?;
215
216 let gi = gi.ok_or(CryptoError::ImplementationError)?;
218 let group_info = MlsGroupInfoBundle::try_new_full_plaintext(gi)?;
219
220 self.persist_group_when_changed(&backend.keystore(), false).await?;
221
222 Ok(MlsCommitBundle {
223 commit,
224 welcome,
225 group_info,
226 })
227 }
228
229 #[cfg_attr(test, crate::durable)]
231 pub(crate) async fn update_keying_material(
232 &mut self,
233 client: &Client,
234 backend: &MlsCryptoProvider,
235 cb: Option<&CredentialBundle>,
236 leaf_node: Option<LeafNode>,
237 ) -> CryptoResult<MlsCommitBundle> {
238 let cb = match cb {
239 None => &self.find_most_recent_credential_bundle(client).await?,
240 Some(cb) => cb,
241 };
242 let (commit, welcome, group_info) = self
243 .group
244 .explicit_self_update(backend, &cb.signature_key, leaf_node)
245 .await
246 .map_err(MlsError::from)?;
247
248 let group_info = group_info.ok_or(CryptoError::ImplementationError)?;
250 let group_info = MlsGroupInfoBundle::try_new_full_plaintext(group_info)?;
251
252 self.persist_group_when_changed(&backend.keystore(), false).await?;
253
254 Ok(MlsCommitBundle {
255 welcome,
256 commit,
257 group_info,
258 })
259 }
260
261 #[cfg_attr(test, crate::durable)]
263 pub(crate) async fn commit_pending_proposals(
264 &mut self,
265 client: &Client,
266 backend: &MlsCryptoProvider,
267 ) -> CryptoResult<Option<MlsCommitBundle>> {
268 if self.group.pending_proposals().count() > 0 {
269 let signer = &self.find_most_recent_credential_bundle(client).await?.signature_key;
270
271 let (commit, welcome, gi) = self
272 .group
273 .commit_to_pending_proposals(backend, signer)
274 .await
275 .map_err(MlsError::from)?;
276 let group_info = MlsGroupInfoBundle::try_new_full_plaintext(gi.unwrap())?;
277
278 self.persist_group_when_changed(&backend.keystore(), false).await?;
279
280 Ok(Some(MlsCommitBundle {
281 welcome,
282 commit,
283 group_info,
284 }))
285 } else {
286 Ok(None)
287 }
288 }
289}
290
291#[derive(Debug)]
294pub struct MlsConversationCreationMessage {
295 pub welcome: MlsMessageOut,
297 pub commit: MlsMessageOut,
299 pub group_info: MlsGroupInfoBundle,
301 pub crl_new_distribution_points: NewCrlDistributionPoint,
303}
304
305impl MlsConversationCreationMessage {
306 #[allow(clippy::type_complexity)]
311 pub fn to_bytes(self) -> CryptoResult<(Vec<u8>, Vec<u8>, MlsGroupInfoBundle, NewCrlDistributionPoint)> {
312 use openmls::prelude::TlsSerializeTrait as _;
313 let welcome = self.welcome.tls_serialize_detached().map_err(MlsError::from)?;
314 let msg = self.commit.tls_serialize_detached().map_err(MlsError::from)?;
315 Ok((welcome, msg, self.group_info, self.crl_new_distribution_points))
316 }
317}
318
319#[derive(Debug, Clone)]
321pub struct MlsCommitBundle {
322 pub welcome: Option<MlsMessageOut>,
324 pub commit: MlsMessageOut,
326 pub group_info: MlsGroupInfoBundle,
328}
329
330impl MlsCommitBundle {
331 #[allow(clippy::type_complexity)]
336 pub fn to_bytes_triple(self) -> CryptoResult<(Option<Vec<u8>>, Vec<u8>, MlsGroupInfoBundle)> {
337 use openmls::prelude::TlsSerializeTrait as _;
338 let welcome = self
339 .welcome
340 .as_ref()
341 .map(|w| w.tls_serialize_detached().map_err(MlsError::from))
342 .transpose()?;
343 let commit = self.commit.tls_serialize_detached().map_err(MlsError::from)?;
344 Ok((welcome, commit, self.group_info))
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use itertools::Itertools;
351 use openmls::prelude::SignaturePublicKey;
352 use wasm_bindgen_test::*;
353
354 use crate::test_utils::*;
355
356 use super::*;
357
358 wasm_bindgen_test_configure!(run_in_browser);
359
360 mod add_members {
361 use super::*;
362
363 #[apply(all_cred_cipher)]
364 #[wasm_bindgen_test]
365 async fn can_add_members_to_conversation(case: TestCase) {
366 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
367 Box::pin(async move {
368 let id = conversation_id();
369
370 alice_central
371 .context
372 .new_conversation(&id, case.credential_type, case.cfg.clone())
373 .await
374 .unwrap();
375 let bob = bob_central.rand_key_package(&case).await;
376 let MlsConversationCreationMessage { welcome, .. } = alice_central
377 .context
378 .add_members_to_conversation(&id, vec![bob])
379 .await
380 .unwrap();
381
382 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
384 alice_central.context.commit_accepted(&id).await.unwrap();
385
386 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
387 assert_eq!(
388 alice_central
389 .get_conversation_unchecked(&id)
390 .await
391 .group
392 .group_id()
393 .as_slice(),
394 id
395 );
396 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
397
398 bob_central
399 .context
400 .process_welcome_message(welcome.into(), case.custom_cfg())
401 .await
402 .unwrap();
403 assert_eq!(
404 alice_central.get_conversation_unchecked(&id).await.id(),
405 bob_central.get_conversation_unchecked(&id).await.id()
406 );
407 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
408 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
409 })
410 })
411 .await
412 }
413
414 #[apply(all_cred_cipher)]
415 #[wasm_bindgen_test]
416 async fn should_return_valid_welcome(case: TestCase) {
417 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
418 Box::pin(async move {
419 let id = conversation_id();
420 alice_central
421 .context
422 .new_conversation(&id, case.credential_type, case.cfg.clone())
423 .await
424 .unwrap();
425
426 let bob = bob_central.rand_key_package(&case).await;
427 let welcome = alice_central
428 .context
429 .add_members_to_conversation(&id, vec![bob])
430 .await
431 .unwrap()
432 .welcome;
433 alice_central.context.commit_accepted(&id).await.unwrap();
434
435 bob_central
436 .context
437 .process_welcome_message(welcome.into(), case.custom_cfg())
438 .await
439 .unwrap();
440 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
441 })
442 })
443 .await
444 }
445
446 #[apply(all_cred_cipher)]
447 #[wasm_bindgen_test]
448 async fn should_return_valid_group_info(case: TestCase) {
449 run_test_with_client_ids(
450 case.clone(),
451 ["alice", "bob", "guest"],
452 move |[alice_central, bob_central, mut guest_central]| {
453 Box::pin(async move {
454 let id = conversation_id();
455 alice_central
456 .context
457 .new_conversation(&id, case.credential_type, case.cfg.clone())
458 .await
459 .unwrap();
460
461 let bob = bob_central.rand_key_package(&case).await;
462 let commit_bundle = alice_central
463 .context
464 .add_members_to_conversation(&id, vec![bob])
465 .await
466 .unwrap();
467 let group_info = commit_bundle.group_info.get_group_info();
468 alice_central.context.commit_accepted(&id).await.unwrap();
469
470 assert!(guest_central
471 .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
472 .await
473 .is_ok());
474 })
475 },
476 )
477 .await
478 }
479 }
480
481 mod remove_members {
482 use super::*;
483
484 #[apply(all_cred_cipher)]
485 #[wasm_bindgen_test]
486 async fn alice_can_remove_bob_from_conversation(case: TestCase) {
487 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
488 Box::pin(async move {
489 let id = conversation_id();
490
491 alice_central
492 .context
493 .new_conversation(&id, case.credential_type, case.cfg.clone())
494 .await
495 .unwrap();
496 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
497
498 let MlsCommitBundle { commit, welcome, .. } = alice_central
499 .context
500 .remove_members_from_conversation(&id, &[bob_central.get_client_id().await])
501 .await
502 .unwrap();
503 assert!(welcome.is_none());
504
505 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
507 alice_central.context.commit_accepted(&id).await.unwrap();
508 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
509
510 bob_central
511 .context
512 .decrypt_message(&id, commit.to_bytes().unwrap())
513 .await
514 .unwrap();
515
516 assert!(matches!(
518 bob_central.context.get_conversation(&id).await.unwrap_err(),
519 CryptoError::ConversationNotFound(conv_id) if conv_id == id
520 ));
521 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_err());
522 })
523 })
524 .await;
525 }
526
527 #[apply(all_cred_cipher)]
528 #[wasm_bindgen_test]
529 async fn should_return_valid_welcome(case: TestCase) {
530 run_test_with_client_ids(
531 case.clone(),
532 ["alice", "bob", "guest"],
533 move |[alice_central, bob_central, mut guest_central]| {
534 Box::pin(async move {
535 let id = conversation_id();
536
537 alice_central
538 .context
539 .new_conversation(&id, case.credential_type, case.cfg.clone())
540 .await
541 .unwrap();
542 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
543
544 let proposal = alice_central
545 .context
546 .new_add_proposal(&id, guest_central.get_one_key_package(&case).await)
547 .await
548 .unwrap();
549 bob_central
550 .context
551 .decrypt_message(&id, proposal.proposal.to_bytes().unwrap())
552 .await
553 .unwrap();
554
555 let welcome = alice_central
556 .context
557 .remove_members_from_conversation(&id, &[bob_central.get_client_id().await])
558 .await
559 .unwrap()
560 .welcome;
561 alice_central.context.commit_accepted(&id).await.unwrap();
562
563 assert!(guest_central
564 .try_join_from_welcome(
565 &id,
566 welcome.unwrap().into(),
567 case.custom_cfg(),
568 vec![&alice_central]
569 )
570 .await
571 .is_ok());
572 assert!(guest_central.try_talk_to(&id, &bob_central).await.is_err());
574 })
575 },
576 )
577 .await;
578 }
579
580 #[apply(all_cred_cipher)]
581 #[wasm_bindgen_test]
582 async fn should_return_valid_group_info(case: TestCase) {
583 run_test_with_client_ids(
584 case.clone(),
585 ["alice", "bob", "guest"],
586 move |[alice_central, bob_central, mut guest_central]| {
587 Box::pin(async move {
588 let id = conversation_id();
589
590 alice_central
591 .context
592 .new_conversation(&id, case.credential_type, case.cfg.clone())
593 .await
594 .unwrap();
595 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
596
597 let commit_bundle = alice_central
598 .context
599 .remove_members_from_conversation(&id, &[bob_central.get_client_id().await])
600 .await
601 .unwrap();
602
603 alice_central.context.commit_accepted(&id).await.unwrap();
604 let group_info = commit_bundle.group_info.get_group_info();
605
606 assert!(guest_central
607 .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
608 .await
609 .is_ok());
610 assert!(guest_central.try_talk_to(&id, &bob_central).await.is_err());
612 })
613 },
614 )
615 .await;
616 }
617 }
618
619 mod update_keying_material {
620 use super::*;
621
622 #[apply(all_cred_cipher)]
623 #[wasm_bindgen_test]
624 async fn should_succeed(case: TestCase) {
625 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
626 Box::pin(async move {
627 let id = conversation_id();
628 alice_central
629 .context
630 .new_conversation(&id, case.credential_type, case.cfg.clone())
631 .await
632 .unwrap();
633 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
634
635 let init_count = alice_central.context.count_entities().await;
636
637 let bob_keys = bob_central
638 .get_conversation_unchecked(&id)
639 .await
640 .encryption_keys()
641 .collect::<Vec<Vec<u8>>>();
642 let alice_keys = alice_central
643 .get_conversation_unchecked(&id)
644 .await
645 .encryption_keys()
646 .collect::<Vec<Vec<u8>>>();
647 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
648
649 let alice_key = alice_central
650 .encryption_key_of(&id, alice_central.get_client_id().await)
651 .await;
652
653 let MlsCommitBundle { commit, welcome, .. } =
655 alice_central.context.update_keying_material(&id).await.unwrap();
656 assert!(welcome.is_none());
657
658 assert!(alice_central
660 .get_conversation_unchecked(&id)
661 .await
662 .encryption_keys()
663 .contains(&alice_key));
664
665 alice_central.context.commit_accepted(&id).await.unwrap();
666
667 assert!(!alice_central
668 .get_conversation_unchecked(&id)
669 .await
670 .encryption_keys()
671 .contains(&alice_key));
672
673 let alice_new_keys = alice_central
674 .get_conversation_unchecked(&id)
675 .await
676 .encryption_keys()
677 .collect::<Vec<_>>();
678 assert!(!alice_new_keys.contains(&alice_key));
679
680 bob_central
682 .context
683 .decrypt_message(&id, &commit.to_bytes().unwrap())
684 .await
685 .unwrap();
686
687 let bob_new_keys = bob_central
688 .get_conversation_unchecked(&id)
689 .await
690 .encryption_keys()
691 .collect::<Vec<_>>();
692 assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
693
694 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
696
697 let final_count = alice_central.context.count_entities().await;
700 assert_eq!(init_count, final_count);
701 })
702 })
703 .await;
704 }
705
706 #[apply(all_cred_cipher)]
707 #[wasm_bindgen_test]
708 async fn should_create_welcome_for_pending_add_proposals(case: TestCase) {
709 run_test_with_client_ids(
710 case.clone(),
711 ["alice", "bob", "charlie"],
712 move |[alice_central, bob_central, charlie_central]| {
713 Box::pin(async move {
714 let id = conversation_id();
715 alice_central
716 .context
717 .new_conversation(&id, case.credential_type, case.cfg.clone())
718 .await
719 .unwrap();
720 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
721
722 let bob_keys = bob_central
723 .get_conversation_unchecked(&id)
724 .await
725 .signature_keys()
726 .collect::<Vec<SignaturePublicKey>>();
727 let alice_keys = alice_central
728 .get_conversation_unchecked(&id)
729 .await
730 .signature_keys()
731 .collect::<Vec<SignaturePublicKey>>();
732
733 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
735
736 let alice_key = alice_central
737 .encryption_key_of(&id, alice_central.get_client_id().await)
738 .await;
739
740 let charlie_kp = charlie_central.get_one_key_package(&case).await;
742 let add_charlie_proposal =
743 alice_central.context.new_add_proposal(&id, charlie_kp).await.unwrap();
744
745 bob_central
747 .context
748 .decrypt_message(&id, add_charlie_proposal.proposal.to_bytes().unwrap())
749 .await
750 .unwrap();
751
752 let MlsCommitBundle { commit, welcome, .. } =
754 alice_central.context.update_keying_material(&id).await.unwrap();
755 assert!(welcome.is_some());
756 assert!(alice_central
757 .get_conversation_unchecked(&id)
758 .await
759 .encryption_keys()
760 .contains(&alice_key));
761 alice_central.context.commit_accepted(&id).await.unwrap();
762 assert!(!alice_central
763 .get_conversation_unchecked(&id)
764 .await
765 .encryption_keys()
766 .contains(&alice_key));
767
768 charlie_central
770 .context
771 .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
772 .await
773 .unwrap();
774
775 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 3);
776 assert_eq!(charlie_central.get_conversation_unchecked(&id).await.members().len(), 3);
777 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
779
780 let alice_new_keys = alice_central
781 .get_conversation_unchecked(&id)
782 .await
783 .encryption_keys()
784 .collect::<Vec<Vec<u8>>>();
785 assert!(!alice_new_keys.contains(&alice_key));
786
787 bob_central
789 .context
790 .decrypt_message(&id, &commit.to_bytes().unwrap())
791 .await
792 .unwrap();
793 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 3);
794
795 let bob_new_keys = bob_central
796 .get_conversation_unchecked(&id)
797 .await
798 .encryption_keys()
799 .collect::<Vec<Vec<u8>>>();
800 assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
801
802 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
804 assert!(bob_central.try_talk_to(&id, &charlie_central).await.is_ok());
805 assert!(charlie_central.try_talk_to(&id, &alice_central).await.is_ok());
806 })
807 },
808 )
809 .await;
810 }
811
812 #[apply(all_cred_cipher)]
813 #[wasm_bindgen_test]
814 async fn should_return_valid_welcome(case: TestCase) {
815 run_test_with_client_ids(
816 case.clone(),
817 ["alice", "bob", "guest"],
818 move |[alice_central, bob_central, mut guest_central]| {
819 Box::pin(async move {
820 let id = conversation_id();
821 alice_central
822 .context
823 .new_conversation(&id, case.credential_type, case.cfg.clone())
824 .await
825 .unwrap();
826 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
827
828 let proposal = alice_central
829 .context
830 .new_add_proposal(&id, guest_central.get_one_key_package(&case).await)
831 .await
832 .unwrap()
833 .proposal;
834 bob_central
835 .context
836 .decrypt_message(&id, proposal.to_bytes().unwrap())
837 .await
838 .unwrap();
839
840 let MlsCommitBundle { commit, welcome, .. } =
841 alice_central.context.update_keying_material(&id).await.unwrap();
842 alice_central.context.commit_accepted(&id).await.unwrap();
843
844 bob_central
845 .context
846 .decrypt_message(&id, commit.to_bytes().unwrap())
847 .await
848 .unwrap();
849
850 assert!(guest_central
851 .try_join_from_welcome(
852 &id,
853 welcome.unwrap().into(),
854 case.custom_cfg(),
855 vec![&alice_central, &bob_central]
856 )
857 .await
858 .is_ok());
859 })
860 },
861 )
862 .await;
863 }
864
865 #[apply(all_cred_cipher)]
866 #[wasm_bindgen_test]
867 async fn should_return_valid_group_info(case: TestCase) {
868 run_test_with_client_ids(
869 case.clone(),
870 ["alice", "bob", "guest"],
871 move |[alice_central, bob_central, mut guest_central]| {
872 Box::pin(async move {
873 let id = conversation_id();
874 alice_central
875 .context
876 .new_conversation(&id, case.credential_type, case.cfg.clone())
877 .await
878 .unwrap();
879 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
880
881 let commit_bundle = alice_central.context.update_keying_material(&id).await.unwrap();
882 let group_info = commit_bundle.group_info.get_group_info();
883 alice_central.context.commit_accepted(&id).await.unwrap();
884
885 assert!(guest_central
886 .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
887 .await
888 .is_ok());
889 })
890 },
891 )
892 .await;
893 }
894 }
895
896 mod commit_pending_proposals {
897 use super::*;
898
899 #[apply(all_cred_cipher)]
900 #[wasm_bindgen_test]
901 async fn should_create_a_commit_out_of_self_pending_proposals(case: TestCase) {
902 run_test_with_client_ids(
903 case.clone(),
904 ["alice", "bob"],
905 move |[mut alice_central, bob_central]| {
906 Box::pin(async move {
907 let id = conversation_id();
908 alice_central
909 .context
910 .new_conversation(&id, case.credential_type, case.cfg.clone())
911 .await
912 .unwrap();
913 alice_central
914 .context
915 .new_add_proposal(&id, bob_central.get_one_key_package(&case).await)
916 .await
917 .unwrap();
918 assert!(!alice_central.pending_proposals(&id).await.is_empty());
919 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
920 let MlsCommitBundle { welcome, .. } = alice_central
921 .context
922 .commit_pending_proposals(&id)
923 .await
924 .unwrap()
925 .unwrap();
926 alice_central.context.commit_accepted(&id).await.unwrap();
927 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
928
929 bob_central
930 .context
931 .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
932 .await
933 .unwrap();
934 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
935 })
936 },
937 )
938 .await;
939 }
940
941 #[apply(all_cred_cipher)]
942 #[wasm_bindgen_test]
943 async fn should_return_none_when_there_are_no_pending_proposals(case: TestCase) {
944 run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
945 Box::pin(async move {
946 let id = conversation_id();
947 alice_central
948 .context
949 .new_conversation(&id, case.credential_type, case.cfg.clone())
950 .await
951 .unwrap();
952 assert!(alice_central.pending_proposals(&id).await.is_empty());
953 assert!(alice_central
954 .context
955 .commit_pending_proposals(&id)
956 .await
957 .unwrap()
958 .is_none());
959 })
960 })
961 .await;
962 }
963
964 #[apply(all_cred_cipher)]
965 #[wasm_bindgen_test]
966 async fn should_create_a_commit_out_of_pending_proposals_by_ref(case: TestCase) {
967 run_test_with_client_ids(
968 case.clone(),
969 ["alice", "bob", "charlie"],
970 move |[alice_central, mut bob_central, charlie_central]| {
971 Box::pin(async move {
972 let id = conversation_id();
973 alice_central
974 .context
975 .new_conversation(&id, case.credential_type, case.cfg.clone())
976 .await
977 .unwrap();
978 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
979 let proposal = bob_central
980 .context
981 .new_add_proposal(&id, charlie_central.get_one_key_package(&case).await)
982 .await
983 .unwrap();
984 assert!(!bob_central.pending_proposals(&id).await.is_empty());
985 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
986 alice_central
987 .context
988 .decrypt_message(&id, proposal.proposal.to_bytes().unwrap())
989 .await
990 .unwrap();
991
992 let MlsCommitBundle { commit, .. } = alice_central
993 .context
994 .commit_pending_proposals(&id)
995 .await
996 .unwrap()
997 .unwrap();
998 alice_central.context.commit_accepted(&id).await.unwrap();
999 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 3);
1000
1001 bob_central
1002 .context
1003 .decrypt_message(&id, commit.to_bytes().unwrap())
1004 .await
1005 .unwrap();
1006 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 3);
1007 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
1008 })
1009 },
1010 )
1011 .await;
1012 }
1013
1014 #[apply(all_cred_cipher)]
1015 #[wasm_bindgen_test]
1016 async fn should_return_valid_welcome(case: TestCase) {
1017 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1018 Box::pin(async move {
1019 let id = conversation_id();
1020 alice_central
1021 .context
1022 .new_conversation(&id, case.credential_type, case.cfg.clone())
1023 .await
1024 .unwrap();
1025 alice_central
1026 .context
1027 .new_add_proposal(&id, bob_central.get_one_key_package(&case).await)
1028 .await
1029 .unwrap();
1030 let MlsCommitBundle { welcome, .. } = alice_central
1031 .context
1032 .commit_pending_proposals(&id)
1033 .await
1034 .unwrap()
1035 .unwrap();
1036 alice_central.context.commit_accepted(&id).await.unwrap();
1037
1038 bob_central
1039 .context
1040 .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
1041 .await
1042 .unwrap();
1043 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
1044 })
1045 })
1046 .await;
1047 }
1048
1049 #[apply(all_cred_cipher)]
1050 #[wasm_bindgen_test]
1051 async fn should_return_valid_group_info(case: TestCase) {
1052 run_test_with_client_ids(
1053 case.clone(),
1054 ["alice", "bob", "guest"],
1055 move |[alice_central, bob_central, mut guest_central]| {
1056 Box::pin(async move {
1057 let id = conversation_id();
1058 alice_central
1059 .context
1060 .new_conversation(&id, case.credential_type, case.cfg.clone())
1061 .await
1062 .unwrap();
1063 alice_central
1064 .context
1065 .new_add_proposal(&id, bob_central.get_one_key_package(&case).await)
1066 .await
1067 .unwrap();
1068 let commit_bundle = alice_central
1069 .context
1070 .commit_pending_proposals(&id)
1071 .await
1072 .unwrap()
1073 .unwrap();
1074 let group_info = commit_bundle.group_info.get_group_info();
1075 alice_central.context.commit_accepted(&id).await.unwrap();
1076
1077 assert!(guest_central
1078 .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
1079 .await
1080 .is_ok());
1081 })
1082 },
1083 )
1084 .await;
1085 }
1086 }
1087
1088 mod delivery_semantics {
1089 use crate::prelude::MlsWirePolicy;
1090
1091 use super::*;
1092
1093 #[apply(all_cred_cipher)]
1094 #[wasm_bindgen_test]
1095 async fn should_prevent_out_of_order_commits(case: TestCase) {
1096 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1097 Box::pin(async move {
1098 let id = conversation_id();
1099 alice_central
1100 .context
1101 .new_conversation(&id, case.credential_type, case.cfg.clone())
1102 .await
1103 .unwrap();
1104 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1105
1106 let commit1 = alice_central.context.update_keying_material(&id).await.unwrap().commit;
1107 let commit1 = commit1.to_bytes().unwrap();
1108 alice_central.context.commit_accepted(&id).await.unwrap();
1109 let commit2 = alice_central.context.update_keying_material(&id).await.unwrap().commit;
1110 let commit2 = commit2.to_bytes().unwrap();
1111 alice_central.context.commit_accepted(&id).await.unwrap();
1112
1113 let out_of_order = bob_central.context.decrypt_message(&id, &commit2).await;
1115 assert!(matches!(
1116 out_of_order.unwrap_err(),
1117 CryptoError::BufferedFutureMessage { .. }
1118 ));
1119
1120 bob_central.context.decrypt_message(&id, &commit1).await.unwrap();
1123
1124 let past_commit = bob_central.context.decrypt_message(&id, &commit1).await;
1126 assert!(matches!(past_commit.unwrap_err(), CryptoError::StaleCommit));
1127 })
1128 })
1129 .await;
1130 }
1131
1132 #[apply(all_cred_cipher)]
1133 #[wasm_bindgen_test]
1134 async fn should_allow_dropped_commits(case: TestCase) {
1135 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1136 Box::pin(async move {
1137 let id = conversation_id();
1138 alice_central
1139 .context
1140 .new_conversation(&id, case.credential_type, case.cfg.clone())
1141 .await
1142 .unwrap();
1143 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1144
1145 let _alice_commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
1146 let bob_commit = bob_central.context.update_keying_material(&id).await.unwrap().commit;
1147 alice_central
1149 .context
1150 .decrypt_message(&id, bob_commit.to_bytes().unwrap())
1151 .await
1152 .unwrap();
1153 bob_central.context.commit_accepted(&id).await.unwrap();
1154 })
1155 })
1156 .await;
1157 }
1158
1159 #[apply(all_cred_cipher)]
1160 #[wasm_bindgen_test]
1161 async fn should_prevent_replayed_encrypted_handshake_messages(case: TestCase) {
1162 if case.custom_cfg().wire_policy == MlsWirePolicy::Ciphertext {
1163 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1164 Box::pin(async move {
1165 let id = conversation_id();
1166 alice_central
1167 .context
1168 .new_conversation(&id, case.credential_type, case.cfg.clone())
1169 .await
1170 .unwrap();
1171 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1172
1173 let proposal1 = alice_central.context.new_update_proposal(&id).await.unwrap().proposal;
1174 let proposal2 = proposal1.clone();
1175 alice_central
1176 .get_conversation_unchecked(&id)
1177 .await
1178 .group
1179 .clear_pending_proposals();
1180
1181 let commit1 = alice_central.context.update_keying_material(&id).await.unwrap().commit;
1182 let commit2 = commit1.clone();
1183
1184 bob_central
1186 .context
1187 .decrypt_message(&id, proposal1.to_bytes().unwrap())
1188 .await
1189 .unwrap();
1190 assert!(matches!(
1191 bob_central
1192 .context
1193 .decrypt_message(&id, proposal2.to_bytes().unwrap())
1194 .await
1195 .unwrap_err(),
1196 CryptoError::DuplicateMessage
1197 ));
1198 bob_central
1199 .get_conversation_unchecked(&id)
1200 .await
1201 .group
1202 .clear_pending_proposals();
1203
1204 bob_central
1206 .context
1207 .decrypt_message(&id, commit1.to_bytes().unwrap())
1208 .await
1209 .unwrap();
1210 assert!(matches!(
1211 bob_central
1212 .context
1213 .decrypt_message(&id, commit2.to_bytes().unwrap())
1214 .await
1215 .unwrap_err(),
1216 CryptoError::StaleCommit
1217 ));
1218 })
1219 })
1220 .await;
1221 }
1222 }
1223 }
1224}