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: TestContext) {
99 let [owner_central, guest_central] = case.sessions().await;
100 Box::pin(async move {
101 let id = conversation_id();
102 owner_central
103 .transaction
104 .new_conversation(&id, case.credential_type, case.cfg.clone())
105 .await
106 .unwrap();
107 let epoch = owner_central.get_conversation_unchecked(&id).await.group.epoch();
108
109 let external_add = guest_central
111 .transaction
112 .new_external_add_proposal(id.clone(), epoch, case.ciphersuite(), case.credential_type)
113 .await
114 .unwrap();
115
116 let decrypted = owner_central
118 .transaction
119 .conversation(&id)
120 .await
121 .unwrap()
122 .decrypt_message(external_add.to_bytes().unwrap())
123 .await
124 .unwrap();
125 assert_eq!(owner_central.get_conversation_unchecked(&id).await.members().len(), 1);
127
128 guest_central.verify_sender_identity(&case, &decrypted).await;
130
131 owner_central
133 .transaction
134 .conversation(&id)
135 .await
136 .unwrap()
137 .commit_pending_proposals()
138 .await
139 .unwrap();
140 assert_eq!(owner_central.get_conversation_unchecked(&id).await.members().len(), 2);
142
143 let welcome = guest_central.mls_transport().await.latest_welcome_message().await;
144 guest_central
145 .transaction
146 .process_welcome_message(welcome.into(), case.custom_cfg())
147 .await
148 .unwrap();
149 assert_eq!(guest_central.get_conversation_unchecked(&id).await.members().len(), 2);
150 assert!(guest_central.try_talk_to(&id, &owner_central).await.is_ok());
152 })
153 .await
154 }
155 }
156
157 mod remove {
158 use super::*;
159 use crate::{MlsErrorKind, prelude::MlsError};
160 use openmls::prelude::{
161 ExternalProposal, GroupId, MlsMessageIn, ProcessMessageError, SenderExtensionIndex, ValidationError,
162 };
163
164 #[apply(all_cred_cipher)]
165 #[wasm_bindgen_test]
166 async fn ds_should_remove_guest_from_conversation(case: TestContext) {
167 let [owner, guest, ds] = case.sessions().await;
168 Box::pin(async move {
169 let owner_central = &owner.transaction;
170 let guest_central = &guest.transaction;
171 let id = conversation_id();
172
173 let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
174 let mut cfg = case.cfg.clone();
175 owner_central
176 .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
177 .await
178 .unwrap();
179 owner_central
180 .new_conversation(&id, case.credential_type, cfg)
181 .await
182 .unwrap();
183
184 owner.invite_all(&case, &id, [&guest]).await.unwrap();
185 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
186
187 let to_remove = owner.index_of(&id, guest.get_client_id().await).await;
190 let sender_index = SenderExtensionIndex::new(0);
191
192 let (sc, ct) = (case.signature_scheme(), case.credential_type);
193 let cb = ds.find_most_recent_credential_bundle(sc, ct).await.unwrap();
194
195 let group_id = GroupId::from_slice(&id[..]);
196 let epoch = owner.get_conversation_unchecked(&id).await.group.epoch();
197 let proposal =
198 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index).unwrap();
199
200 owner_central
201 .conversation(&id)
202 .await
203 .unwrap()
204 .decrypt_message(proposal.to_bytes().unwrap())
205 .await
206 .unwrap();
207 guest_central
208 .conversation(&id)
209 .await
210 .unwrap()
211 .decrypt_message(proposal.to_bytes().unwrap())
212 .await
213 .unwrap();
214 owner_central
215 .conversation(&id)
216 .await
217 .unwrap()
218 .commit_pending_proposals()
219 .await
220 .unwrap();
221 let commit = owner.mls_transport().await.latest_commit().await;
222
223 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 1);
224
225 guest_central
227 .conversation(&id)
228 .await
229 .unwrap()
230 .decrypt_message(commit.to_bytes().unwrap())
231 .await
232 .unwrap();
233 assert!(guest_central.conversation(&id).await.is_err());
234 assert!(guest.try_talk_to(&id, &owner).await.is_err());
235 })
236 .await
237 }
238
239 #[apply(all_cred_cipher)]
240 #[wasm_bindgen_test]
241 async fn should_fail_when_invalid_external_sender(case: TestContext) {
242 use crate::mls;
243
244 let [owner, guest, ds, attacker] = case.sessions().await;
245 Box::pin(async move {
246 let id = conversation_id();
247 let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
249 let mut cfg = case.cfg.clone();
250 owner
251 .transaction
252 .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
253 .await
254 .unwrap();
255 owner
256 .transaction
257 .new_conversation(&id, case.credential_type, cfg)
258 .await
259 .unwrap();
260
261 owner.invite_all(&case, &id, [&guest]).await.unwrap();
262 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
263
264 let to_remove = owner.index_of(&id, guest.get_client_id().await).await;
266 let sender_index = SenderExtensionIndex::new(1);
267
268 let (sc, ct) = (case.signature_scheme(), case.credential_type);
269 let cb = attacker.find_most_recent_credential_bundle(sc, ct).await.unwrap();
270 let group_id = GroupId::from_slice(&id[..]);
271 let epoch = owner.get_conversation_unchecked(&id).await.group.epoch();
272 let proposal =
273 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index).unwrap();
274
275 let owner_decrypt = owner
276 .transaction
277 .conversation(&id)
278 .await
279 .unwrap()
280 .decrypt_message(proposal.to_bytes().unwrap())
281 .await;
282
283 assert!(matches!(
284 owner_decrypt.unwrap_err(),
285 mls::conversation::Error::Mls(MlsError {
286 source: MlsErrorKind::MlsMessageError(ProcessMessageError::ValidationError(
287 ValidationError::UnauthorizedExternalSender
288 )),
289 ..
290 })
291 ));
292
293 let guest_decrypt = owner
294 .transaction
295 .conversation(&id)
296 .await
297 .unwrap()
298 .decrypt_message(proposal.to_bytes().unwrap())
299 .await;
300 assert!(matches!(
301 guest_decrypt.unwrap_err(),
302 mls::conversation::Error::Mls(MlsError {
303 source: MlsErrorKind::MlsMessageError(ProcessMessageError::ValidationError(
304 ValidationError::UnauthorizedExternalSender
305 )),
306 ..
307 })
308 ));
309 })
310 .await
311 }
312
313 #[apply(all_cred_cipher)]
314 #[wasm_bindgen_test]
315 async fn should_fail_when_wrong_signature_key(case: TestContext) {
316 use crate::mls;
317
318 let [owner, guest, ds] = case.sessions().await;
319 Box::pin(async move {
320 let id = conversation_id();
321
322 let key = ds.client_signature_key(&case).await.as_slice().to_vec();
326 let mut cfg = case.cfg.clone();
327 owner
328 .transaction
329 .set_raw_external_senders(&mut cfg, vec![key.as_slice().to_vec()])
330 .await
331 .unwrap();
332 owner
333 .transaction
334 .new_conversation(&id, case.credential_type, cfg)
335 .await
336 .unwrap();
337
338 owner.invite_all(&case, &id, [&guest]).await.unwrap();
339 assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
340
341 let to_remove = owner.index_of(&id, guest.get_client_id().await).await;
342 let sender_index = SenderExtensionIndex::new(0);
343
344 let (sc, ct) = (case.signature_scheme(), case.credential_type);
345 let cb = guest.find_most_recent_credential_bundle(sc, ct).await.unwrap();
348 let group_id = GroupId::from_slice(&id[..]);
349 let epoch = owner.get_conversation_unchecked(&id).await.group.epoch();
350 let proposal =
351 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index).unwrap();
352
353 let owner_decrypt = owner
354 .transaction
355 .conversation(&id)
356 .await
357 .unwrap()
358 .decrypt_message(proposal.to_bytes().unwrap())
359 .await;
360 assert!(matches!(
361 owner_decrypt.unwrap_err(),
362 mls::conversation::Error::Mls(MlsError {
363 source: MlsErrorKind::MlsMessageError(ProcessMessageError::InvalidSignature),
364 ..
365 })
366 ));
367
368 let guest_decrypt = owner
369 .transaction
370 .conversation(&id)
371 .await
372 .unwrap()
373 .decrypt_message(proposal.to_bytes().unwrap())
374 .await;
375 assert!(matches!(
376 guest_decrypt.unwrap_err(),
377 mls::conversation::Error::Mls(MlsError {
378 source: MlsErrorKind::MlsMessageError(ProcessMessageError::InvalidSignature),
379 ..
380 })
381 ));
382 })
383 .await
384 }
385
386 #[apply(all_cred_cipher)]
387 #[wasm_bindgen_test]
388 async fn joiners_from_welcome_can_accept_external_remove_proposals(case: TestContext) {
389 let [alice, bob, charlie, ds] = case.sessions().await;
390 Box::pin(async move {
391 let alice_central = &alice.transaction;
392 let bob_central = &bob.transaction;
393 let charlie_central = &charlie.transaction;
394 let id = conversation_id();
395
396 let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
397 let mut cfg = case.cfg.clone();
398 alice_central
399 .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
400 .await
401 .unwrap();
402
403 alice_central
404 .new_conversation(&id, case.credential_type, cfg)
405 .await
406 .unwrap();
407
408 alice.invite_all(&case, &id, [&bob]).await.unwrap();
409 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
410
411 let charlie_kp = charlie.rand_key_package(&case).await;
414 alice_central
415 .conversation(&id)
416 .await
417 .unwrap()
418 .add_members(vec![charlie_kp])
419 .await
420 .unwrap();
421 let welcome = alice.mls_transport().await.latest_welcome_message().await;
422 let commit = alice.mls_transport().await.latest_commit().await;
423 bob_central
424 .conversation(&id)
425 .await
426 .unwrap()
427 .decrypt_message(commit.to_bytes().unwrap())
428 .await
429 .unwrap();
430 charlie_central
432 .process_welcome_message(MlsMessageIn::from(welcome), case.custom_cfg())
433 .await
434 .unwrap();
435 assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 3);
436 assert!(charlie.try_talk_to(&id, &alice).await.is_ok());
437 assert!(charlie.try_talk_to(&id, &bob).await.is_ok());
438
439 let to_remove = alice.index_of(&id, bob.get_client_id().await).await;
442 let sender_index = SenderExtensionIndex::new(0);
443 let (sc, ct) = (case.signature_scheme(), case.credential_type);
444 let cb = ds.find_most_recent_credential_bundle(sc, ct).await.unwrap();
445 let group_id = GroupId::from_slice(&id[..]);
446 let epoch = alice.get_conversation_unchecked(&id).await.group.epoch();
447 let proposal =
448 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index).unwrap();
449
450 let charlie_can_verify_ext_proposal = charlie_central
453 .conversation(&id)
454 .await
455 .unwrap()
456 .decrypt_message(proposal.to_bytes().unwrap())
457 .await;
458 assert!(charlie_can_verify_ext_proposal.is_ok());
459
460 alice_central
461 .conversation(&id)
462 .await
463 .unwrap()
464 .decrypt_message(proposal.to_bytes().unwrap())
465 .await
466 .unwrap();
467 bob_central
468 .conversation(&id)
469 .await
470 .unwrap()
471 .decrypt_message(proposal.to_bytes().unwrap())
472 .await
473 .unwrap();
474
475 charlie_central
476 .conversation(&id)
477 .await
478 .unwrap()
479 .commit_pending_proposals()
480 .await
481 .unwrap();
482 let commit = charlie.mls_transport().await.latest_commit().await;
483 assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 2);
484
485 alice_central
486 .conversation(&id)
487 .await
488 .unwrap()
489 .decrypt_message(commit.to_bytes().unwrap())
490 .await
491 .unwrap();
492 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
493 bob_central
494 .conversation(&id)
495 .await
496 .unwrap()
497 .decrypt_message(commit.to_bytes().unwrap())
498 .await
499 .unwrap();
500 assert!(alice.try_talk_to(&id, &charlie).await.is_ok());
501 assert!(alice.try_talk_to(&id, &bob).await.is_err());
502 })
503 .await
504 }
505
506 #[apply(all_cred_cipher)]
507 #[wasm_bindgen_test]
508 async fn joiners_from_external_commit_can_accept_external_remove_proposals(case: TestContext) {
509 let [alice, bob, charlie, ds] = case.sessions().await;
510 Box::pin(async move {
511 let alice_central = &alice.transaction;
512 let bob_central = &bob.transaction;
513 let charlie_central = &charlie.transaction;
514 let id = conversation_id();
515
516 let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
517 let mut cfg = case.cfg.clone();
518 alice_central
519 .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
520 .await
521 .unwrap();
522
523 alice_central
524 .new_conversation(&id, case.credential_type, cfg)
525 .await
526 .unwrap();
527
528 alice.invite_all(&case, &id, [&bob]).await.unwrap();
529 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
530
531 let public_group_state = alice.get_group_info(&id).await;
534 charlie_central
535 .join_by_external_commit(public_group_state, case.custom_cfg(), case.credential_type)
536 .await
537 .unwrap();
538 let commit = charlie.mls_transport().await.latest_commit().await;
539
540 alice_central
542 .conversation(&id)
543 .await
544 .unwrap()
545 .decrypt_message(commit.to_bytes().unwrap())
546 .await
547 .unwrap();
548 bob_central
549 .conversation(&id)
550 .await
551 .unwrap()
552 .decrypt_message(commit.to_bytes().unwrap())
553 .await
554 .unwrap();
555
556 assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 3);
557 assert!(charlie.try_talk_to(&id, &alice).await.is_ok());
558 assert!(charlie.try_talk_to(&id, &bob).await.is_ok());
559
560 let to_remove = alice.index_of(&id, bob.get_client_id().await).await;
563 let sender_index = SenderExtensionIndex::new(0);
564 let (sc, ct) = (case.signature_scheme(), case.credential_type);
565 let cb = ds.find_most_recent_credential_bundle(sc, ct).await.unwrap();
566 let group_id = GroupId::from_slice(&id[..]);
567 let epoch = alice.get_conversation_unchecked(&id).await.group.epoch();
568 let proposal =
569 ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index).unwrap();
570
571 let charlie_can_verify_ext_proposal = charlie_central
574 .conversation(&id)
575 .await
576 .unwrap()
577 .decrypt_message(proposal.to_bytes().unwrap())
578 .await;
579 assert!(charlie_can_verify_ext_proposal.is_ok());
580
581 alice_central
582 .conversation(&id)
583 .await
584 .unwrap()
585 .decrypt_message(proposal.to_bytes().unwrap())
586 .await
587 .unwrap();
588 bob_central
589 .conversation(&id)
590 .await
591 .unwrap()
592 .decrypt_message(proposal.to_bytes().unwrap())
593 .await
594 .unwrap();
595
596 charlie_central
597 .conversation(&id)
598 .await
599 .unwrap()
600 .commit_pending_proposals()
601 .await
602 .unwrap();
603 assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 2);
604
605 let commit = charlie.mls_transport().await.latest_commit().await;
606 alice_central
607 .conversation(&id)
608 .await
609 .unwrap()
610 .decrypt_message(commit.to_bytes().unwrap())
611 .await
612 .unwrap();
613 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
614 bob_central
615 .conversation(&id)
616 .await
617 .unwrap()
618 .decrypt_message(commit.to_bytes().unwrap())
619 .await
620 .unwrap();
621 assert!(alice.try_talk_to(&id, &charlie).await.is_ok());
622 assert!(alice.try_talk_to(&id, &bob).await.is_err());
623 })
624 .await
625 }
626 }
627}