1use log::{trace, warn};
2use openmls::{
3 group::QueuedProposal,
4 prelude::{GroupEpoch, GroupId, JoinProposal, LeafNodeIndex, MlsMessageOut, Proposal, Sender},
5};
6use std::collections::HashSet;
7
8use crate::{
9 group_store::GroupStoreValue,
10 mls::{credential::typ::MlsCredentialType, ClientId, ConversationId},
11 prelude::{CoreCryptoCallbacks, CryptoError, CryptoResult, MlsCiphersuite, MlsConversation, MlsError},
12};
13
14use crate::context::CentralContext;
15
16impl MlsConversation {
17 pub(crate) async fn validate_external_proposal(
20 &self,
21 proposal: &QueuedProposal,
22 parent_conversation: Option<&GroupStoreValue<MlsConversation>>,
23 callbacks: Option<&dyn CoreCryptoCallbacks>,
24 ) -> CryptoResult<()> {
25 let is_external_proposal = matches!(proposal.sender(), Sender::External(_) | Sender::NewMemberProposal);
26 if is_external_proposal {
27 if let Proposal::Add(add_proposal) = proposal.proposal() {
28 let callbacks = callbacks.ok_or(CryptoError::CallbacksNotSet)?;
29 let existing_clients = self.members_in_next_epoch();
30 let self_identity = add_proposal.key_package().leaf_node().credential().identity();
31 let parent_clients = if let Some(parent_conv) = parent_conversation {
32 Some(
33 parent_conv
34 .read()
35 .await
36 .group
37 .members()
38 .map(|kp| kp.credential.identity().to_vec().into())
39 .collect(),
40 )
41 } else {
42 None
43 };
44 let is_self_user_in_group = callbacks
45 .client_is_existing_group_user(
46 self.id.clone(),
47 self_identity.into(),
48 existing_clients,
49 parent_clients,
50 )
51 .await;
52 if !is_self_user_in_group {
53 return Err(CryptoError::UnauthorizedExternalAddProposal);
54 }
55 }
56 } else {
57 warn!("Not external proposal.");
58 }
59 Ok(())
60 }
61
62 pub fn members_in_next_epoch(&self) -> Vec<ClientId> {
64 let pending_removals = self.pending_removals();
65 let existing_clients = self
66 .group
67 .members()
68 .filter_map(|kp| {
69 if !pending_removals.contains(&kp.index) {
70 Some(kp.credential.identity().into())
71 } else {
72 trace!(client_index:% = kp.index; "Client is pending removal");
73 None
74 }
75 })
76 .collect::<HashSet<_>>();
77 existing_clients.into_iter().collect()
78 }
79
80 fn pending_removals(&self) -> Vec<LeafNodeIndex> {
82 self.group
83 .pending_proposals()
84 .filter_map(|proposal| match proposal.proposal() {
85 Proposal::Remove(ref remove) => Some(remove.removed()),
86 _ => None,
87 })
88 .collect::<Vec<_>>()
89 }
90}
91
92impl CentralContext {
93 #[cfg_attr(test, crate::dispotent)]
110 pub async fn new_external_add_proposal(
111 &self,
112 conversation_id: ConversationId,
113 epoch: GroupEpoch,
114 ciphersuite: MlsCiphersuite,
115 credential_type: MlsCredentialType,
116 ) -> CryptoResult<MlsMessageOut> {
117 let group_id = GroupId::from_slice(&conversation_id[..]);
118 let mls_provider = self.mls_provider().await?;
119
120 let client = self.mls_client().await?;
121 let cb = client
122 .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), credential_type)
123 .await;
124 let cb = match (cb, credential_type) {
125 (Ok(cb), _) => cb,
126 (Err(CryptoError::CredentialNotFound(_)), MlsCredentialType::Basic) => {
127 client
129 .init_basic_credential_bundle_if_missing(&mls_provider, ciphersuite.signature_algorithm())
130 .await?;
131
132 client
133 .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), credential_type)
134 .await?
135 }
136 (Err(CryptoError::CredentialNotFound(_)), MlsCredentialType::X509) => {
137 return Err(CryptoError::E2eiEnrollmentNotDone)
138 }
139 (Err(e), _) => return Err(e),
140 };
141 let kp = client
142 .generate_one_keypackage_from_credential_bundle(&mls_provider, ciphersuite, &cb)
143 .await?;
144
145 Ok(JoinProposal::new(kp, group_id, epoch, &cb.signature_key).map_err(MlsError::from)?)
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use wasm_bindgen_test::*;
152
153 use crate::{prelude::MlsCommitBundle, test_utils::*};
154
155 wasm_bindgen_test_configure!(run_in_browser);
156
157 mod add {
158 use super::*;
159
160 #[apply(all_cred_cipher)]
161 #[wasm_bindgen_test]
162 async fn guest_should_externally_propose_adding_itself_to_owner_group(case: TestCase) {
163 run_test_with_client_ids(
164 case.clone(),
165 ["owner", "guest"],
166 move |[owner_central, guest_central]| {
167 Box::pin(async move {
168 let id = conversation_id();
169 owner_central
170 .context
171 .new_conversation(&id, case.credential_type, case.cfg.clone())
172 .await
173 .unwrap();
174 let epoch = owner_central.get_conversation_unchecked(&id).await.group.epoch();
175
176 let external_add = guest_central
178 .context
179 .new_external_add_proposal(id.clone(), epoch, case.ciphersuite(), case.credential_type)
180 .await
181 .unwrap();
182
183 let decrypted = owner_central
185 .context
186 .decrypt_message(&id, external_add.to_bytes().unwrap())
187 .await
188 .unwrap();
189 assert_eq!(owner_central.get_conversation_unchecked(&id).await.members().len(), 1);
191
192 guest_central.verify_sender_identity(&case, &decrypted).await;
194
195 let MlsCommitBundle { welcome, .. } = owner_central
197 .context
198 .commit_pending_proposals(&id)
199 .await
200 .unwrap()
201 .unwrap();
202 owner_central.context.commit_accepted(&id).await.unwrap();
203 assert_eq!(owner_central.get_conversation_unchecked(&id).await.members().len(), 2);
205
206 guest_central
207 .context
208 .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
209 .await
210 .unwrap();
211 assert_eq!(guest_central.get_conversation_unchecked(&id).await.members().len(), 2);
212 assert!(guest_central.try_talk_to(&id, &owner_central).await.is_ok());
214 })
215 },
216 )
217 .await
218 }
219 }
220
221 mod remove {
222 use super::*;
223 use crate::prelude::{CryptoError, MlsConversationCreationMessage, MlsConversationInitBundle, MlsError};
224 use openmls::prelude::{
225 ExternalProposal, GroupId, MlsMessageIn, ProcessMessageError, SenderExtensionIndex, ValidationError,
226 };
227
228 #[apply(all_cred_cipher)]
229 #[wasm_bindgen_test]
230 async fn ds_should_remove_guest_from_conversation(case: TestCase) {
231 run_test_with_client_ids(case.clone(), ["owner", "guest", "ds"], move |[owner, guest, ds]| {
232 Box::pin(async move {
233 let owner_central = &owner.context;
234 let guest_central = &guest.context;
235 let id = conversation_id();
236
237 let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
238 let mut cfg = case.cfg.clone();
239 owner_central
240 .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
241 .await
242 .unwrap();
243 owner_central
244 .new_conversation(&id, case.credential_type, cfg)
245 .await
246 .unwrap();
247
248 owner.invite_all(&case, &id, [&guest]).await.unwrap();
249 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
250
251 let to_remove = owner.index_of(&id, guest.get_client_id().await).await;
254 let sender_index = SenderExtensionIndex::new(0);
255
256 let (sc, ct) = (case.signature_scheme(), case.credential_type);
257 let cb = ds.find_most_recent_credential_bundle(sc, ct).await.unwrap();
258
259 let group_id = GroupId::from_slice(&id[..]);
260 let epoch = owner.get_conversation_unchecked(&id).await.group.epoch();
261 let proposal =
262 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
263 .unwrap();
264
265 owner_central
266 .decrypt_message(&id, proposal.to_bytes().unwrap())
267 .await
268 .unwrap();
269 guest_central
270 .decrypt_message(&id, proposal.to_bytes().unwrap())
271 .await
272 .unwrap();
273 let MlsCommitBundle { commit, .. } =
274 owner_central.commit_pending_proposals(&id).await.unwrap().unwrap();
275
276 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
278 owner_central.commit_accepted(&id).await.unwrap();
279 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 1);
280
281 guest_central
283 .decrypt_message(&id, commit.to_bytes().unwrap())
284 .await
285 .unwrap();
286 assert!(guest_central.get_conversation(&id).await.is_err());
287 assert!(guest.try_talk_to(&id, &owner).await.is_err());
288 })
289 })
290 .await
291 }
292
293 #[apply(all_cred_cipher)]
294 #[wasm_bindgen_test]
295 async fn should_fail_when_invalid_external_sender(case: TestCase) {
296 run_test_with_client_ids(
297 case.clone(),
298 ["owner", "guest", "ds", "attacker"],
299 move |[owner, guest, ds, attacker]| {
300 Box::pin(async move {
301 let id = conversation_id();
302 let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
304 let mut cfg = case.cfg.clone();
305 owner
306 .context
307 .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
308 .await
309 .unwrap();
310 owner
311 .context
312 .new_conversation(&id, case.credential_type, cfg)
313 .await
314 .unwrap();
315
316 owner.invite_all(&case, &id, [&guest]).await.unwrap();
317 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
318
319 let to_remove = owner.index_of(&id, guest.get_client_id().await).await;
321 let sender_index = SenderExtensionIndex::new(1);
322
323 let (sc, ct) = (case.signature_scheme(), case.credential_type);
324 let cb = attacker.find_most_recent_credential_bundle(sc, ct).await.unwrap();
325 let group_id = GroupId::from_slice(&id[..]);
326 let epoch = owner.get_conversation_unchecked(&id).await.group.epoch();
327 let proposal =
328 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
329 .unwrap();
330
331 let owner_decrypt = owner.context.decrypt_message(&id, proposal.to_bytes().unwrap()).await;
332
333 assert!(matches!(
334 owner_decrypt.unwrap_err(),
335 CryptoError::MlsError(MlsError::MlsMessageError(ProcessMessageError::ValidationError(
336 ValidationError::UnauthorizedExternalSender
337 )))
338 ));
339
340 let guest_decrypt = owner.context.decrypt_message(&id, proposal.to_bytes().unwrap()).await;
341 assert!(matches!(
342 guest_decrypt.unwrap_err(),
343 CryptoError::MlsError(MlsError::MlsMessageError(ProcessMessageError::ValidationError(
344 ValidationError::UnauthorizedExternalSender
345 )))
346 ));
347 })
348 },
349 )
350 .await
351 }
352
353 #[apply(all_cred_cipher)]
354 #[wasm_bindgen_test]
355 async fn should_fail_when_wrong_signature_key(case: TestCase) {
356 run_test_with_client_ids(case.clone(), ["owner", "guest", "ds"], move |[owner, guest, ds]| {
357 Box::pin(async move {
358 let id = conversation_id();
359
360 let key = ds.client_signature_key(&case).await.as_slice().to_vec();
364 let mut cfg = case.cfg.clone();
365 owner
366 .context
367 .set_raw_external_senders(&mut cfg, vec![key.as_slice().to_vec()])
368 .await
369 .unwrap();
370 owner
371 .context
372 .new_conversation(&id, case.credential_type, cfg)
373 .await
374 .unwrap();
375
376 owner.invite_all(&case, &id, [&guest]).await.unwrap();
377 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
378
379 let to_remove = owner.index_of(&id, guest.get_client_id().await).await;
380 let sender_index = SenderExtensionIndex::new(0);
381
382 let (sc, ct) = (case.signature_scheme(), case.credential_type);
383 let cb = guest.find_most_recent_credential_bundle(sc, ct).await.unwrap();
386 let group_id = GroupId::from_slice(&id[..]);
387 let epoch = owner.get_conversation_unchecked(&id).await.group.epoch();
388 let proposal =
389 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
390 .unwrap();
391
392 let owner_decrypt = owner.context.decrypt_message(&id, proposal.to_bytes().unwrap()).await;
393 assert!(matches!(
394 owner_decrypt.unwrap_err(),
395 CryptoError::MlsError(MlsError::MlsMessageError(ProcessMessageError::InvalidSignature))
396 ));
397
398 let guest_decrypt = owner.context.decrypt_message(&id, proposal.to_bytes().unwrap()).await;
399 assert!(matches!(
400 guest_decrypt.unwrap_err(),
401 CryptoError::MlsError(MlsError::MlsMessageError(ProcessMessageError::InvalidSignature))
402 ));
403 })
404 })
405 .await
406 }
407
408 #[apply(all_cred_cipher)]
409 #[wasm_bindgen_test]
410 async fn joiners_from_welcome_can_accept_external_remove_proposals(case: TestCase) {
411 run_test_with_client_ids(
412 case.clone(),
413 ["alice", "bob", "charlie", "ds"],
414 move |[alice, bob, charlie, ds]| {
415 Box::pin(async move {
416 let alice_central = &alice.context;
417 let bob_central = &bob.context;
418 let charlie_central = &charlie.context;
419 let id = conversation_id();
420
421 let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
422 let mut cfg = case.cfg.clone();
423 alice_central
424 .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
425 .await
426 .unwrap();
427
428 alice_central
429 .new_conversation(&id, case.credential_type, cfg)
430 .await
431 .unwrap();
432
433 alice.invite_all(&case, &id, [&bob]).await.unwrap();
434 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
435
436 let charlie_kp = charlie.rand_key_package(&case).await;
439 let MlsConversationCreationMessage { welcome, commit, .. } = alice_central
440 .add_members_to_conversation(&id, vec![charlie_kp])
441 .await
442 .unwrap();
443 alice_central.commit_accepted(&id).await.unwrap();
444 bob_central
445 .decrypt_message(&id, commit.to_bytes().unwrap())
446 .await
447 .unwrap();
448 charlie_central
450 .process_welcome_message(MlsMessageIn::from(welcome), case.custom_cfg())
451 .await
452 .unwrap();
453 assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 3);
454 assert!(charlie.try_talk_to(&id, &alice).await.is_ok());
455 assert!(charlie.try_talk_to(&id, &bob).await.is_ok());
456
457 let to_remove = alice.index_of(&id, bob.get_client_id().await).await;
460 let sender_index = SenderExtensionIndex::new(0);
461 let (sc, ct) = (case.signature_scheme(), case.credential_type);
462 let cb = ds.find_most_recent_credential_bundle(sc, ct).await.unwrap();
463 let group_id = GroupId::from_slice(&id[..]);
464 let epoch = alice.get_conversation_unchecked(&id).await.group.epoch();
465 let proposal =
466 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
467 .unwrap();
468
469 let charlie_can_verify_ext_proposal =
472 charlie_central.decrypt_message(&id, proposal.to_bytes().unwrap()).await;
473 assert!(charlie_can_verify_ext_proposal.is_ok());
474
475 alice_central
476 .decrypt_message(&id, proposal.to_bytes().unwrap())
477 .await
478 .unwrap();
479 bob_central
480 .decrypt_message(&id, proposal.to_bytes().unwrap())
481 .await
482 .unwrap();
483
484 let commit = charlie_central
485 .commit_pending_proposals(&id)
486 .await
487 .unwrap()
488 .unwrap()
489 .commit;
490 charlie_central.commit_accepted(&id).await.unwrap();
491 assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 2);
492
493 alice_central
494 .decrypt_message(&id, commit.to_bytes().unwrap())
495 .await
496 .unwrap();
497 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
498 bob_central
499 .decrypt_message(&id, commit.to_bytes().unwrap())
500 .await
501 .unwrap();
502 assert!(alice.try_talk_to(&id, &charlie).await.is_ok());
503 assert!(alice.try_talk_to(&id, &bob).await.is_err());
504 })
505 },
506 )
507 .await
508 }
509
510 #[apply(all_cred_cipher)]
511 #[wasm_bindgen_test]
512 async fn joiners_from_external_commit_can_accept_external_remove_proposals(case: TestCase) {
513 run_test_with_client_ids(
514 case.clone(),
515 ["alice", "bob", "charlie", "ds"],
516 move |[alice, bob, charlie, ds]| {
517 Box::pin(async move {
518 let alice_central = &alice.context;
519 let bob_central = &bob.context;
520 let charlie_central = &charlie.context;
521 let id = conversation_id();
522
523 let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
524 let mut cfg = case.cfg.clone();
525 alice_central
526 .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
527 .await
528 .unwrap();
529
530 alice_central
531 .new_conversation(&id, case.credential_type, cfg)
532 .await
533 .unwrap();
534
535 alice.invite_all(&case, &id, [&bob]).await.unwrap();
536 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
537
538 let public_group_state = alice.get_group_info(&id).await;
541 let MlsConversationInitBundle { commit, .. } = charlie_central
542 .join_by_external_commit(public_group_state, case.custom_cfg(), case.credential_type)
543 .await
544 .unwrap();
545
546 charlie_central
548 .merge_pending_group_from_external_commit(&id)
549 .await
550 .unwrap();
551 alice_central
552 .decrypt_message(&id, commit.to_bytes().unwrap())
553 .await
554 .unwrap();
555 bob_central
556 .decrypt_message(&id, commit.to_bytes().unwrap())
557 .await
558 .unwrap();
559
560 assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 3);
561 assert!(charlie.try_talk_to(&id, &alice).await.is_ok());
562 assert!(charlie.try_talk_to(&id, &bob).await.is_ok());
563
564 let to_remove = alice.index_of(&id, bob.get_client_id().await).await;
567 let sender_index = SenderExtensionIndex::new(0);
568 let (sc, ct) = (case.signature_scheme(), case.credential_type);
569 let cb = ds.find_most_recent_credential_bundle(sc, ct).await.unwrap();
570 let group_id = GroupId::from_slice(&id[..]);
571 let epoch = alice.get_conversation_unchecked(&id).await.group.epoch();
572 let proposal =
573 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
574 .unwrap();
575
576 let charlie_can_verify_ext_proposal =
579 charlie_central.decrypt_message(&id, proposal.to_bytes().unwrap()).await;
580 assert!(charlie_can_verify_ext_proposal.is_ok());
581
582 alice_central
583 .decrypt_message(&id, proposal.to_bytes().unwrap())
584 .await
585 .unwrap();
586 bob_central
587 .decrypt_message(&id, proposal.to_bytes().unwrap())
588 .await
589 .unwrap();
590
591 let commit = charlie_central
592 .commit_pending_proposals(&id)
593 .await
594 .unwrap()
595 .unwrap()
596 .commit;
597 charlie_central.commit_accepted(&id).await.unwrap();
598 assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 2);
599
600 alice_central
601 .decrypt_message(&id, commit.to_bytes().unwrap())
602 .await
603 .unwrap();
604 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
605 bob_central
606 .decrypt_message(&id, commit.to_bytes().unwrap())
607 .await
608 .unwrap();
609 assert!(alice.try_talk_to(&id, &charlie).await.is_ok());
610 assert!(alice.try_talk_to(&id, &bob).await.is_err());
611 })
612 },
613 )
614 .await
615 }
616 }
617}