core_crypto/mls/conversation/
commit.rs1use openmls::prelude::MlsMessageOut;
9
10use super::{Error, Result};
11use crate::MlsGroupInfoBundle;
12
13#[derive(Debug, Clone)]
15pub struct MlsCommitBundle {
16 pub welcome: Option<MlsMessageOut>,
18 pub commit: MlsMessageOut,
20 pub group_info: MlsGroupInfoBundle,
22 pub encrypted_message: Option<Vec<u8>>,
24}
25
26impl MlsCommitBundle {
27 #[allow(clippy::type_complexity)]
32 pub fn to_bytes_triple(self) -> Result<(Option<Vec<u8>>, Vec<u8>, MlsGroupInfoBundle)> {
33 use openmls::prelude::TlsSerializeTrait as _;
34 let welcome = self
35 .welcome
36 .as_ref()
37 .map(|w| {
38 w.tls_serialize_detached()
39 .map_err(Error::tls_serialize("serialize welcome"))
40 })
41 .transpose()?;
42 let commit = self
43 .commit
44 .tls_serialize_detached()
45 .map_err(Error::tls_serialize("serialize commit"))?;
46 Ok((welcome, commit, self.group_info))
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use itertools::Itertools;
53 use openmls::prelude::SignaturePublicKey;
54
55 use super::{Error, *};
56 use crate::{
57 mls::conversation::{Conversation as _, ConversationWithMls as _},
58 test_utils::*,
59 transaction_context::Error as TransactionError,
60 };
61
62 mod transport {
63 use std::sync::Arc;
64
65 use super::*;
66
67 #[apply(all_cred_cipher)]
68 async fn retry_should_work(case: TestContext) {
69 let [alice, bob, charlie] = case.sessions().await;
70 Box::pin(async move {
71 let conversation = case.create_conversation([&alice, &bob]).await;
73
74 let commit = conversation.acting_as(&bob).await.update().await;
76 let bob_epoch = commit.conversation().guard_of(&bob).await.epoch().await;
77 assert_eq!(2, bob_epoch);
78 let alice_epoch = commit.conversation().guard_of(&alice).await.epoch().await;
79 assert_eq!(1, alice_epoch);
80 let intermediate_commit = commit.message();
81 let retry_provider = Arc::new(
83 CoreCryptoTransportRetrySuccessProvider::default().with_intermediate_commits(
84 alice.clone(),
85 &[intermediate_commit],
86 commit.conversation().id(),
87 ),
88 );
89
90 alice.replace_transport(retry_provider.clone()).await;
91
92 let conversation = commit.finish().advance_epoch().await.invite_notify([&charlie]).await;
96
97 assert_eq!(retry_provider.retry_count().await, 2);
99 assert_eq!(retry_provider.success_count().await, 2);
101
102 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
104 })
105 .await;
106 }
107 }
108
109 mod add_members {
110 use std::sync::Arc;
111
112 use super::*;
113
114 #[apply(all_cred_cipher)]
115 async fn can_add_members_to_conversation(case: TestContext) {
116 let [alice, bob] = case.sessions().await;
117 Box::pin(async move {
118 let conversation = case.create_conversation([&alice]).await;
119 let id = conversation.id.clone();
120 let bob_keypackage = bob.rand_key_package(&case).await;
121 alice
123 .replace_transport(Arc::<CoreCryptoTransportAbortProvider>::default())
124 .await;
125 alice
126 .transaction
127 .conversation(&id)
128 .await
129 .unwrap()
130 .add_members(vec![bob_keypackage.clone()])
131 .await
132 .unwrap_err();
133
134 assert_eq!(conversation.member_count().await, 1);
136
137 alice
138 .replace_transport(Arc::<CoreCryptoTransportSuccessProvider>::default())
139 .await;
140
141 let conversation = conversation.invite_notify([&bob]).await;
142
143 assert_eq!(*conversation.id(), id);
144 assert_eq!(
145 conversation
146 .guard()
147 .await
148 .conversation()
149 .await
150 .group
151 .group_id()
152 .as_slice(),
153 id.as_ref()
154 );
155 assert_eq!(conversation.member_count().await, 2);
156 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
157 })
158 .await
159 }
160
161 #[apply(all_cred_cipher)]
162 async fn should_return_valid_welcome(case: TestContext) {
163 let [alice, bob] = case.sessions().await;
164 Box::pin(async move {
165 let conversation = case.create_conversation([&alice, &bob]).await;
166 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
167 })
168 .await
169 }
170
171 #[apply(all_cred_cipher)]
172 async fn should_return_valid_group_info(case: TestContext) {
173 let [alice, bob, guest] = case.sessions().await;
174 Box::pin(async move {
175 let conversation = case.create_conversation([&alice, &bob]).await;
176 let commit_bundle = alice.mls_transport().await.latest_commit_bundle().await;
177 let group_info = commit_bundle.group_info.get_group_info();
178 let conversation = conversation
179 .external_join_via_group_info_notify(&guest, group_info)
180 .await;
181 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
182 })
183 .await
184 }
185 }
186
187 mod remove_members {
188 use super::*;
189
190 #[apply(all_cred_cipher)]
191 async fn alice_can_remove_bob_from_conversation(case: TestContext) {
192 let [alice, bob] = case.sessions().await;
193 Box::pin(async move {
194 let conversation = case.create_conversation([&alice, &bob]).await.remove_notify(&bob).await;
195 let id = conversation.id().clone();
196
197 let MlsCommitBundle { welcome, .. } = alice.mls_transport().await.latest_commit_bundle().await;
198 assert!(welcome.is_none());
199
200 assert_eq!(conversation.member_count().await, 1);
201
202 assert!(matches!(
204 bob.transaction.conversation(&id).await.unwrap_err(),
205 TransactionError::Leaf(crate::LeafError::ConversationNotFound(ref i))
206 if i == &id
207 ));
208 assert!(!conversation.can_talk(&alice, &bob).await);
209 })
210 .await;
211 }
212
213 #[apply(all_cred_cipher)]
214 async fn should_return_valid_welcome(case: TestContext) {
215 let [alice, bob, guest] = case.sessions().await;
216 Box::pin(async move {
217 let conversation = case
218 .create_conversation([&alice, &bob])
219 .await
220 .invite_proposal_notify(&guest)
221 .await
222 .remove_notify(&bob)
223 .await;
224
225 assert!(conversation.is_functional_and_contains([&alice, &guest]).await);
226 assert!(!conversation.can_talk(&alice, &bob).await);
228 })
229 .await;
230 }
231
232 #[apply(all_cred_cipher)]
233 async fn should_return_valid_group_info(case: TestContext) {
234 let [alice, bob, guest] = case.sessions().await;
235 Box::pin(async move {
236 let conversation = case.create_conversation([&alice, &bob]).await.remove_notify(&bob).await;
237 let commit_bundle = alice.mls_transport().await.latest_commit_bundle().await;
238 let group_info = commit_bundle.group_info.get_group_info();
239 let conversation = conversation
240 .external_join_via_group_info_notify(&guest, group_info)
241 .await;
242
243 assert!(conversation.is_functional_and_contains([&alice, &guest]).await);
244 assert!(!conversation.can_talk(&alice, &bob).await);
246 })
247 .await;
248 }
249 }
250
251 mod update_keying_material {
252 use super::*;
253
254 #[apply(all_cred_cipher)]
255 async fn should_succeed(case: TestContext) {
256 let [alice, bob] = case.sessions().await;
257 Box::pin(async move {
258 let conversation = case.create_conversation([&alice, &bob]).await;
259 let init_count = alice.transaction.count_entities().await;
260
261 let bob_keys = conversation
262 .guard_of(&bob)
263 .await
264 .conversation()
265 .await
266 .encryption_keys()
267 .collect::<Vec<Vec<u8>>>();
268 let alice_keys = conversation
269 .guard()
270 .await
271 .conversation()
272 .await
273 .encryption_keys()
274 .collect::<Vec<Vec<u8>>>();
275 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
276
277 let alice_key = conversation.encryption_public_key().await;
278
279 let conversation = conversation.update_notify().await;
281 let MlsCommitBundle { welcome, .. } = alice.mls_transport().await.latest_commit_bundle().await;
282 assert!(welcome.is_none());
283
284 let alice_new_keys = conversation
285 .guard()
286 .await
287 .conversation()
288 .await
289 .encryption_keys()
290 .collect::<Vec<Vec<u8>>>();
291 assert!(!alice_new_keys.contains(&alice_key));
292
293 let bob_new_keys = conversation
294 .guard_of(&bob)
295 .await
296 .conversation()
297 .await
298 .encryption_keys()
299 .collect::<Vec<Vec<u8>>>();
300 assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
301
302 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
304
305 let final_count = alice.transaction.count_entities().await;
308 assert_eq!(init_count, final_count);
309 })
310 .await;
311 }
312
313 #[apply(all_cred_cipher)]
314 async fn should_create_welcome_for_pending_add_proposals(case: TestContext) {
315 let [alice, bob, charlie] = case.sessions().await;
316 Box::pin(async move {
317 let conversation = case.create_conversation([&alice, &bob]).await;
318
319 let bob_keys = conversation
320 .guard_of(&bob)
321 .await
322 .conversation()
323 .await
324 .signature_keys()
325 .collect::<Vec<SignaturePublicKey>>();
326 let alice_keys = conversation
327 .guard()
328 .await
329 .conversation()
330 .await
331 .signature_keys()
332 .collect::<Vec<SignaturePublicKey>>();
333
334 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
336
337 let alice_key = conversation.encryption_public_key().await;
338
339 let conversation = conversation.invite_proposal_notify(&charlie).await;
341
342 assert!(
343 conversation
344 .guard()
345 .await
346 .conversation()
347 .await
348 .encryption_keys()
349 .contains(&alice_key)
350 );
351
352 assert_eq!(conversation.member_count().await, 2);
354
355 let conversation = conversation.update_notify().await;
357 let MlsCommitBundle { welcome, .. } = alice.mls_transport().await.latest_commit_bundle().await;
358 assert!(welcome.is_some());
359 assert!(
360 !conversation
361 .guard()
362 .await
363 .conversation()
364 .await
365 .encryption_keys()
366 .contains(&alice_key)
367 );
368
369 assert_eq!(conversation.member_count().await, 3);
370
371 let alice_new_keys = conversation
372 .guard()
373 .await
374 .conversation()
375 .await
376 .encryption_keys()
377 .collect::<Vec<Vec<u8>>>();
378 assert!(!alice_new_keys.contains(&alice_key));
379
380 let bob_new_keys = conversation
381 .guard_of(&bob)
382 .await
383 .conversation()
384 .await
385 .encryption_keys()
386 .collect::<Vec<Vec<u8>>>();
387 assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
388
389 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
391 })
392 .await;
393 }
394
395 #[apply(all_cred_cipher)]
396 async fn should_return_valid_welcome(case: TestContext) {
397 let [alice, bob, guest] = case.sessions().await;
398 Box::pin(async move {
399 let conversation = case
400 .create_conversation([&alice, &bob])
401 .await
402 .invite_proposal_notify(&guest)
403 .await
404 .update_notify()
405 .await;
406
407 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
408 })
409 .await;
410 }
411
412 #[apply(all_cred_cipher)]
413 async fn should_return_valid_group_info(case: TestContext) {
414 let [alice, bob, guest] = case.sessions().await;
415 Box::pin(async move {
416 let conversation = case.create_conversation([&alice, &bob]).await.update_notify().await;
417
418 let group_info = alice.mls_transport().await.latest_group_info().await;
419 let group_info = group_info.get_group_info();
420
421 let conversation = conversation
422 .external_join_via_group_info_notify(&guest, group_info)
423 .await;
424 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
425 })
426 .await;
427 }
428 }
429
430 mod commit_pending_proposals {
431 use super::*;
432
433 #[apply(all_cred_cipher)]
434 async fn should_create_a_commit_out_of_self_pending_proposals(case: TestContext) {
435 let [alice, bob] = case.sessions().await;
436 Box::pin(async move {
437 let conversation = case
438 .create_conversation([&alice])
439 .await
440 .advance_epoch()
441 .await
442 .invite_proposal_notify(&bob)
443 .await;
444
445 assert!(conversation.has_pending_proposals().await);
446 assert_eq!(conversation.member_count().await, 1);
447
448 let conversation = conversation.commit_pending_proposals_notify().await;
449 assert_eq!(conversation.member_count().await, 2);
450
451 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
452 })
453 .await;
454 }
455
456 #[apply(all_cred_cipher)]
457 async fn should_create_a_commit_out_of_pending_proposals_by_ref(case: TestContext) {
458 let [alice, bob, charlie] = case.sessions().await;
459 Box::pin(async move {
460 let conversation = case
462 .create_conversation([&alice, &bob])
463 .await
464 .acting_as(&bob)
465 .await
466 .invite_proposal_notify(&charlie)
467 .await
468 .acting_as(&bob)
469 .await;
470
471 assert!(conversation.has_pending_proposals().await);
472 assert_eq!(conversation.member_count().await, 2);
473
474 let conversation = conversation.commit_pending_proposals_notify().await;
476 assert_eq!(conversation.member_count().await, 3);
477
478 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
479 })
480 .await;
481 }
482
483 #[apply(all_cred_cipher)]
484 async fn should_return_valid_welcome(case: TestContext) {
485 let [alice, bob] = case.sessions().await;
486 Box::pin(async move {
487 let conversation = case
488 .create_conversation([&alice])
489 .await
490 .invite_proposal_notify(&bob)
491 .await
492 .commit_pending_proposals_notify()
493 .await;
494
495 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
496 })
497 .await;
498 }
499
500 #[apply(all_cred_cipher)]
501 async fn should_return_valid_group_info(case: TestContext) {
502 let [alice, bob, guest] = case.sessions().await;
503 Box::pin(async move {
504 let conversation = case
505 .create_conversation([&alice])
506 .await
507 .invite_proposal_notify(&bob)
508 .await
509 .commit_pending_proposals_notify()
510 .await;
511 let commit_bundle = alice.mls_transport().await.latest_commit_bundle().await;
512 let group_info = commit_bundle.group_info.get_group_info();
513 let conversation = conversation
514 .external_join_via_group_info_notify(&guest, group_info)
515 .await;
516
517 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
518 })
519 .await;
520 }
521 }
522
523 mod delivery_semantics {
524 use super::*;
525
526 #[apply(all_cred_cipher)]
527 async fn should_prevent_out_of_order_commits(case: TestContext) {
528 let [alice, bob] = case.sessions().await;
529 Box::pin(async move {
530 let conversation = case.create_conversation([&alice, &bob]).await;
531 let id = conversation.id().clone();
532
533 let commit_guard = conversation.update().await;
534 let commit1 = commit_guard.message();
535 let commit1 = commit1.to_bytes().unwrap();
536
537 let commit_guard = commit_guard.finish().update().await;
538 let commit2 = commit_guard.message();
539 let commit2 = commit2.to_bytes().unwrap();
540
541 let out_of_order = bob
543 .transaction
544 .conversation(&id)
545 .await
546 .unwrap()
547 .decrypt_message(&commit2)
548 .await;
549 assert!(matches!(out_of_order.unwrap_err(), Error::BufferedFutureMessage { .. }));
550
551 bob.transaction
554 .conversation(&id)
555 .await
556 .unwrap()
557 .decrypt_message(&commit1)
558 .await
559 .unwrap();
560
561 let past_commit = bob
563 .transaction
564 .conversation(&id)
565 .await
566 .unwrap()
567 .decrypt_message(&commit1)
568 .await;
569 assert!(matches!(past_commit.unwrap_err(), Error::StaleCommit));
570 })
571 .await;
572 }
573
574 #[apply(all_cred_cipher)]
575 async fn should_prevent_replayed_encrypted_handshake_messages(case: TestContext) {
576 if !case.is_pure_ciphertext() {
577 return;
578 }
579
580 let [alice, bob] = case.sessions().await;
581 Box::pin(async move {
582 let conversation = case.create_conversation([&alice, &bob]).await;
583
584 let proposal_guard = conversation.update_proposal().await;
585 let proposal_replay = proposal_guard.message();
586
587 let conversation = proposal_guard.notify_members().await;
589 assert!(matches!(
590 conversation
591 .guard_of(&bob)
592 .await
593 .decrypt_message(proposal_replay.to_bytes().unwrap())
594 .await
595 .unwrap_err(),
596 Error::DuplicateMessage
597 ));
598
599 let commit_guard = conversation.update().await;
600 let commit_replay = commit_guard.message();
601
602 let conversation = commit_guard.notify_members().await;
604 assert!(matches!(
605 conversation
606 .guard_of(&bob)
607 .await
608 .decrypt_message(commit_replay.to_bytes().unwrap())
609 .await
610 .unwrap_err(),
611 Error::StaleCommit
612 ));
613 })
614 .await;
615 }
616 }
617}