1use log::trace;
2use openmls::prelude::{GroupEpoch, GroupId, JoinProposal, LeafNodeIndex, MlsMessageOut, Proposal};
3use std::collections::HashSet;
4
5use super::Result;
6use crate::{
7 LeafError, MlsError, RecursiveError,
8 context::CentralContext,
9 mls::{self, ClientId, ConversationId, credential::typ::MlsCredentialType},
10 prelude::{MlsCiphersuite, MlsConversation},
11};
12
13impl MlsConversation {
14 pub fn members_in_next_epoch(&self) -> Vec<ClientId> {
16 let pending_removals = self.pending_removals();
17 let existing_clients = self
18 .group
19 .members()
20 .filter_map(|kp| {
21 if !pending_removals.contains(&kp.index) {
22 Some(kp.credential.identity().into())
23 } else {
24 trace!(client_index:% = kp.index; "Client is pending removal");
25 None
26 }
27 })
28 .collect::<HashSet<_>>();
29 existing_clients.into_iter().collect()
30 }
31
32 fn pending_removals(&self) -> Vec<LeafNodeIndex> {
34 self.group
35 .pending_proposals()
36 .filter_map(|proposal| match proposal.proposal() {
37 Proposal::Remove(remove) => Some(remove.removed()),
38 _ => None,
39 })
40 .collect::<Vec<_>>()
41 }
42}
43
44impl CentralContext {
45 #[cfg_attr(test, crate::dispotent)]
62 pub async fn new_external_add_proposal(
63 &self,
64 conversation_id: ConversationId,
65 epoch: GroupEpoch,
66 ciphersuite: MlsCiphersuite,
67 credential_type: MlsCredentialType,
68 ) -> Result<MlsMessageOut> {
69 let group_id = GroupId::from_slice(conversation_id.as_slice());
70 let mls_provider = self
71 .mls_provider()
72 .await
73 .map_err(RecursiveError::root("getting mls provider"))?;
74
75 let client = self
76 .mls_client()
77 .await
78 .map_err(RecursiveError::root("getting mls client"))?;
79 let cb = client
80 .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), credential_type)
81 .await;
82 let cb = match (cb, credential_type) {
83 (Ok(cb), _) => cb,
84 (Err(mls::client::Error::CredentialNotFound(_)), MlsCredentialType::Basic) => {
85 client
87 .init_basic_credential_bundle_if_missing(&mls_provider, ciphersuite.signature_algorithm())
88 .await
89 .map_err(RecursiveError::mls_client(
90 "initializing basic credential bundle if missing",
91 ))?;
92
93 client
94 .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), credential_type)
95 .await
96 .map_err(RecursiveError::mls_client(
97 "finding most recent credential bundle (which we just created)",
98 ))?
99 }
100 (Err(mls::client::Error::CredentialNotFound(_)), MlsCredentialType::X509) => {
101 return Err(LeafError::E2eiEnrollmentNotDone.into());
102 }
103 (Err(e), _) => return Err(RecursiveError::mls_client("finding most recent credential bundle")(e).into()),
104 };
105 let kp = client
106 .generate_one_keypackage_from_credential_bundle(&mls_provider, ciphersuite, &cb)
107 .await
108 .map_err(RecursiveError::mls_client(
109 "generating one keypackage from credential bundle",
110 ))?;
111
112 JoinProposal::new(kp, group_id, epoch, &cb.signature_key)
113 .map_err(MlsError::wrap("creating join proposal"))
114 .map_err(Into::into)
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use wasm_bindgen_test::*;
121
122 use crate::test_utils::*;
123
124 wasm_bindgen_test_configure!(run_in_browser);
125
126 mod add {
127 use super::*;
128
129 #[apply(all_cred_cipher)]
130 #[wasm_bindgen_test]
131 async fn guest_should_externally_propose_adding_itself_to_owner_group(case: TestCase) {
132 run_test_with_client_ids(
133 case.clone(),
134 ["owner", "guest"],
135 move |[owner_central, guest_central]| {
136 Box::pin(async move {
137 let id = conversation_id();
138 owner_central
139 .context
140 .new_conversation(&id, case.credential_type, case.cfg.clone())
141 .await
142 .unwrap();
143 let epoch = owner_central.get_conversation_unchecked(&id).await.group.epoch();
144
145 let external_add = guest_central
147 .context
148 .new_external_add_proposal(id.clone(), epoch, case.ciphersuite(), case.credential_type)
149 .await
150 .unwrap();
151
152 let decrypted = owner_central
154 .context
155 .conversation(&id)
156 .await
157 .unwrap()
158 .decrypt_message(external_add.to_bytes().unwrap())
159 .await
160 .unwrap();
161 assert_eq!(owner_central.get_conversation_unchecked(&id).await.members().len(), 1);
163
164 guest_central.verify_sender_identity(&case, &decrypted).await;
166
167 owner_central
169 .context
170 .conversation(&id)
171 .await
172 .unwrap()
173 .commit_pending_proposals()
174 .await
175 .unwrap();
176 assert_eq!(owner_central.get_conversation_unchecked(&id).await.members().len(), 2);
178
179 let welcome = guest_central.mls_transport.latest_welcome_message().await;
180 guest_central
181 .context
182 .process_welcome_message(welcome.into(), case.custom_cfg())
183 .await
184 .unwrap();
185 assert_eq!(guest_central.get_conversation_unchecked(&id).await.members().len(), 2);
186 assert!(guest_central.try_talk_to(&id, &owner_central).await.is_ok());
188 })
189 },
190 )
191 .await
192 }
193 }
194
195 mod remove {
196 use super::*;
197 use crate::{MlsErrorKind, prelude::MlsError};
198 use openmls::prelude::{
199 ExternalProposal, GroupId, MlsMessageIn, ProcessMessageError, SenderExtensionIndex, ValidationError,
200 };
201
202 #[apply(all_cred_cipher)]
203 #[wasm_bindgen_test]
204 async fn ds_should_remove_guest_from_conversation(case: TestCase) {
205 run_test_with_client_ids(case.clone(), ["owner", "guest", "ds"], move |[owner, guest, ds]| {
206 Box::pin(async move {
207 let owner_central = &owner.context;
208 let guest_central = &guest.context;
209 let id = conversation_id();
210
211 let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
212 let mut cfg = case.cfg.clone();
213 owner_central
214 .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
215 .await
216 .unwrap();
217 owner_central
218 .new_conversation(&id, case.credential_type, cfg)
219 .await
220 .unwrap();
221
222 owner.invite_all(&case, &id, [&guest]).await.unwrap();
223 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
224
225 let to_remove = owner.index_of(&id, guest.get_client_id().await).await;
228 let sender_index = SenderExtensionIndex::new(0);
229
230 let (sc, ct) = (case.signature_scheme(), case.credential_type);
231 let cb = ds.find_most_recent_credential_bundle(sc, ct).await.unwrap();
232
233 let group_id = GroupId::from_slice(&id[..]);
234 let epoch = owner.get_conversation_unchecked(&id).await.group.epoch();
235 let proposal =
236 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
237 .unwrap();
238
239 owner_central
240 .conversation(&id)
241 .await
242 .unwrap()
243 .decrypt_message(proposal.to_bytes().unwrap())
244 .await
245 .unwrap();
246 guest_central
247 .conversation(&id)
248 .await
249 .unwrap()
250 .decrypt_message(proposal.to_bytes().unwrap())
251 .await
252 .unwrap();
253 owner_central
254 .conversation(&id)
255 .await
256 .unwrap()
257 .commit_pending_proposals()
258 .await
259 .unwrap();
260 let commit = owner.mls_transport.latest_commit().await;
261
262 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 1);
263
264 guest_central
266 .conversation(&id)
267 .await
268 .unwrap()
269 .decrypt_message(commit.to_bytes().unwrap())
270 .await
271 .unwrap();
272 assert!(guest_central.conversation(&id).await.is_err());
273 assert!(guest.try_talk_to(&id, &owner).await.is_err());
274 })
275 })
276 .await
277 }
278
279 #[apply(all_cred_cipher)]
280 #[wasm_bindgen_test]
281 async fn should_fail_when_invalid_external_sender(case: TestCase) {
282 use crate::mls;
283
284 run_test_with_client_ids(
285 case.clone(),
286 ["owner", "guest", "ds", "attacker"],
287 move |[owner, guest, ds, attacker]| {
288 Box::pin(async move {
289 let id = conversation_id();
290 let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
292 let mut cfg = case.cfg.clone();
293 owner
294 .context
295 .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
296 .await
297 .unwrap();
298 owner
299 .context
300 .new_conversation(&id, case.credential_type, cfg)
301 .await
302 .unwrap();
303
304 owner.invite_all(&case, &id, [&guest]).await.unwrap();
305 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
306
307 let to_remove = owner.index_of(&id, guest.get_client_id().await).await;
309 let sender_index = SenderExtensionIndex::new(1);
310
311 let (sc, ct) = (case.signature_scheme(), case.credential_type);
312 let cb = attacker.find_most_recent_credential_bundle(sc, ct).await.unwrap();
313 let group_id = GroupId::from_slice(&id[..]);
314 let epoch = owner.get_conversation_unchecked(&id).await.group.epoch();
315 let proposal =
316 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
317 .unwrap();
318
319 let owner_decrypt = owner
320 .context
321 .conversation(&id)
322 .await
323 .unwrap()
324 .decrypt_message(proposal.to_bytes().unwrap())
325 .await;
326
327 assert!(matches!(
328 owner_decrypt.unwrap_err(),
329 mls::conversation::Error::Mls(MlsError {
330 source: MlsErrorKind::MlsMessageError(ProcessMessageError::ValidationError(
331 ValidationError::UnauthorizedExternalSender
332 )),
333 ..
334 })
335 ));
336
337 let guest_decrypt = owner
338 .context
339 .conversation(&id)
340 .await
341 .unwrap()
342 .decrypt_message(proposal.to_bytes().unwrap())
343 .await;
344 assert!(matches!(
345 guest_decrypt.unwrap_err(),
346 mls::conversation::Error::Mls(MlsError {
347 source: MlsErrorKind::MlsMessageError(ProcessMessageError::ValidationError(
348 ValidationError::UnauthorizedExternalSender
349 )),
350 ..
351 })
352 ));
353 })
354 },
355 )
356 .await
357 }
358
359 #[apply(all_cred_cipher)]
360 #[wasm_bindgen_test]
361 async fn should_fail_when_wrong_signature_key(case: TestCase) {
362 use crate::mls;
363
364 run_test_with_client_ids(case.clone(), ["owner", "guest", "ds"], move |[owner, guest, ds]| {
365 Box::pin(async move {
366 let id = conversation_id();
367
368 let key = ds.client_signature_key(&case).await.as_slice().to_vec();
372 let mut cfg = case.cfg.clone();
373 owner
374 .context
375 .set_raw_external_senders(&mut cfg, vec![key.as_slice().to_vec()])
376 .await
377 .unwrap();
378 owner
379 .context
380 .new_conversation(&id, case.credential_type, cfg)
381 .await
382 .unwrap();
383
384 owner.invite_all(&case, &id, [&guest]).await.unwrap();
385 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
386
387 let to_remove = owner.index_of(&id, guest.get_client_id().await).await;
388 let sender_index = SenderExtensionIndex::new(0);
389
390 let (sc, ct) = (case.signature_scheme(), case.credential_type);
391 let cb = guest.find_most_recent_credential_bundle(sc, ct).await.unwrap();
394 let group_id = GroupId::from_slice(&id[..]);
395 let epoch = owner.get_conversation_unchecked(&id).await.group.epoch();
396 let proposal =
397 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
398 .unwrap();
399
400 let owner_decrypt = owner
401 .context
402 .conversation(&id)
403 .await
404 .unwrap()
405 .decrypt_message(proposal.to_bytes().unwrap())
406 .await;
407 assert!(matches!(
408 owner_decrypt.unwrap_err(),
409 mls::conversation::Error::Mls(MlsError {
410 source: MlsErrorKind::MlsMessageError(ProcessMessageError::InvalidSignature),
411 ..
412 })
413 ));
414
415 let guest_decrypt = owner
416 .context
417 .conversation(&id)
418 .await
419 .unwrap()
420 .decrypt_message(proposal.to_bytes().unwrap())
421 .await;
422 assert!(matches!(
423 guest_decrypt.unwrap_err(),
424 mls::conversation::Error::Mls(MlsError {
425 source: MlsErrorKind::MlsMessageError(ProcessMessageError::InvalidSignature),
426 ..
427 })
428 ));
429 })
430 })
431 .await
432 }
433
434 #[apply(all_cred_cipher)]
435 #[wasm_bindgen_test]
436 async fn joiners_from_welcome_can_accept_external_remove_proposals(case: TestCase) {
437 run_test_with_client_ids(
438 case.clone(),
439 ["alice", "bob", "charlie", "ds"],
440 move |[alice, bob, charlie, ds]| {
441 Box::pin(async move {
442 let alice_central = &alice.context;
443 let bob_central = &bob.context;
444 let charlie_central = &charlie.context;
445 let id = conversation_id();
446
447 let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
448 let mut cfg = case.cfg.clone();
449 alice_central
450 .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
451 .await
452 .unwrap();
453
454 alice_central
455 .new_conversation(&id, case.credential_type, cfg)
456 .await
457 .unwrap();
458
459 alice.invite_all(&case, &id, [&bob]).await.unwrap();
460 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
461
462 let charlie_kp = charlie.rand_key_package(&case).await;
465 alice_central
466 .conversation(&id)
467 .await
468 .unwrap()
469 .add_members(vec![charlie_kp])
470 .await
471 .unwrap();
472 let welcome = alice.mls_transport.latest_welcome_message().await;
473 let commit = alice.mls_transport.latest_commit().await;
474 bob_central
475 .conversation(&id)
476 .await
477 .unwrap()
478 .decrypt_message(commit.to_bytes().unwrap())
479 .await
480 .unwrap();
481 charlie_central
483 .process_welcome_message(MlsMessageIn::from(welcome), case.custom_cfg())
484 .await
485 .unwrap();
486 assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 3);
487 assert!(charlie.try_talk_to(&id, &alice).await.is_ok());
488 assert!(charlie.try_talk_to(&id, &bob).await.is_ok());
489
490 let to_remove = alice.index_of(&id, bob.get_client_id().await).await;
493 let sender_index = SenderExtensionIndex::new(0);
494 let (sc, ct) = (case.signature_scheme(), case.credential_type);
495 let cb = ds.find_most_recent_credential_bundle(sc, ct).await.unwrap();
496 let group_id = GroupId::from_slice(&id[..]);
497 let epoch = alice.get_conversation_unchecked(&id).await.group.epoch();
498 let proposal =
499 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
500 .unwrap();
501
502 let charlie_can_verify_ext_proposal = charlie_central
505 .conversation(&id)
506 .await
507 .unwrap()
508 .decrypt_message(proposal.to_bytes().unwrap())
509 .await;
510 assert!(charlie_can_verify_ext_proposal.is_ok());
511
512 alice_central
513 .conversation(&id)
514 .await
515 .unwrap()
516 .decrypt_message(proposal.to_bytes().unwrap())
517 .await
518 .unwrap();
519 bob_central
520 .conversation(&id)
521 .await
522 .unwrap()
523 .decrypt_message(proposal.to_bytes().unwrap())
524 .await
525 .unwrap();
526
527 charlie_central
528 .conversation(&id)
529 .await
530 .unwrap()
531 .commit_pending_proposals()
532 .await
533 .unwrap();
534 let commit = charlie.mls_transport.latest_commit().await;
535 assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 2);
536
537 alice_central
538 .conversation(&id)
539 .await
540 .unwrap()
541 .decrypt_message(commit.to_bytes().unwrap())
542 .await
543 .unwrap();
544 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
545 bob_central
546 .conversation(&id)
547 .await
548 .unwrap()
549 .decrypt_message(commit.to_bytes().unwrap())
550 .await
551 .unwrap();
552 assert!(alice.try_talk_to(&id, &charlie).await.is_ok());
553 assert!(alice.try_talk_to(&id, &bob).await.is_err());
554 })
555 },
556 )
557 .await
558 }
559
560 #[apply(all_cred_cipher)]
561 #[wasm_bindgen_test]
562 async fn joiners_from_external_commit_can_accept_external_remove_proposals(case: TestCase) {
563 run_test_with_client_ids(
564 case.clone(),
565 ["alice", "bob", "charlie", "ds"],
566 move |[alice, bob, charlie, ds]| {
567 Box::pin(async move {
568 let alice_central = &alice.context;
569 let bob_central = &bob.context;
570 let charlie_central = &charlie.context;
571 let id = conversation_id();
572
573 let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
574 let mut cfg = case.cfg.clone();
575 alice_central
576 .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
577 .await
578 .unwrap();
579
580 alice_central
581 .new_conversation(&id, case.credential_type, cfg)
582 .await
583 .unwrap();
584
585 alice.invite_all(&case, &id, [&bob]).await.unwrap();
586 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
587
588 let public_group_state = alice.get_group_info(&id).await;
591 charlie_central
592 .join_by_external_commit(public_group_state, case.custom_cfg(), case.credential_type)
593 .await
594 .unwrap();
595 let commit = charlie.mls_transport.latest_commit().await;
596
597 alice_central
599 .conversation(&id)
600 .await
601 .unwrap()
602 .decrypt_message(commit.to_bytes().unwrap())
603 .await
604 .unwrap();
605 bob_central
606 .conversation(&id)
607 .await
608 .unwrap()
609 .decrypt_message(commit.to_bytes().unwrap())
610 .await
611 .unwrap();
612
613 assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 3);
614 assert!(charlie.try_talk_to(&id, &alice).await.is_ok());
615 assert!(charlie.try_talk_to(&id, &bob).await.is_ok());
616
617 let to_remove = alice.index_of(&id, bob.get_client_id().await).await;
620 let sender_index = SenderExtensionIndex::new(0);
621 let (sc, ct) = (case.signature_scheme(), case.credential_type);
622 let cb = ds.find_most_recent_credential_bundle(sc, ct).await.unwrap();
623 let group_id = GroupId::from_slice(&id[..]);
624 let epoch = alice.get_conversation_unchecked(&id).await.group.epoch();
625 let proposal =
626 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
627 .unwrap();
628
629 let charlie_can_verify_ext_proposal = charlie_central
632 .conversation(&id)
633 .await
634 .unwrap()
635 .decrypt_message(proposal.to_bytes().unwrap())
636 .await;
637 assert!(charlie_can_verify_ext_proposal.is_ok());
638
639 alice_central
640 .conversation(&id)
641 .await
642 .unwrap()
643 .decrypt_message(proposal.to_bytes().unwrap())
644 .await
645 .unwrap();
646 bob_central
647 .conversation(&id)
648 .await
649 .unwrap()
650 .decrypt_message(proposal.to_bytes().unwrap())
651 .await
652 .unwrap();
653
654 charlie_central
655 .conversation(&id)
656 .await
657 .unwrap()
658 .commit_pending_proposals()
659 .await
660 .unwrap();
661 assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 2);
662
663 let commit = charlie.mls_transport.latest_commit().await;
664 alice_central
665 .conversation(&id)
666 .await
667 .unwrap()
668 .decrypt_message(commit.to_bytes().unwrap())
669 .await
670 .unwrap();
671 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
672 bob_central
673 .conversation(&id)
674 .await
675 .unwrap()
676 .decrypt_message(commit.to_bytes().unwrap())
677 .await
678 .unwrap();
679 assert!(alice.try_talk_to(&id, &charlie).await.is_ok());
680 assert!(alice.try_talk_to(&id, &bob).await.is_err());
681 })
682 },
683 )
684 .await
685 }
686 }
687}