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