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