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::test_utils::*;
56 use crate::transaction_context::Error as TransactionError;
57
58 use super::{Error, *};
59
60 mod transport {
61 use super::*;
62 use std::sync::Arc;
63
64 #[apply(all_cred_cipher)]
65 async fn retry_should_work(case: TestContext) {
66 use crate::mls::conversation::Conversation as _;
67
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!(alice.get_conversation_unchecked(&id).await.members().len(), 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!(alice.get_conversation_unchecked(&id).await.id, id);
142 assert_eq!(
143 alice.get_conversation_unchecked(&id).await.group.group_id().as_slice(),
144 id
145 );
146 assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
147 assert_eq!(
148 alice.get_conversation_unchecked(&id).await.id(),
149 bob.get_conversation_unchecked(&id).await.id()
150 );
151 assert_eq!(bob.get_conversation_unchecked(&id).await.members().len(), 2);
152 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
153 })
154 .await
155 }
156
157 #[apply(all_cred_cipher)]
158 async fn should_return_valid_welcome(case: TestContext) {
159 let [alice, bob] = case.sessions().await;
160 Box::pin(async move {
161 let conversation = case.create_conversation([&alice, &bob]).await;
162 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
163 })
164 .await
165 }
166
167 #[apply(all_cred_cipher)]
168 async fn should_return_valid_group_info(case: TestContext) {
169 let [alice, bob, guest] = case.sessions().await;
170 Box::pin(async move {
171 let conversation = case.create_conversation([&alice, &bob]).await;
172 let commit_bundle = alice.mls_transport().await.latest_commit_bundle().await;
173 let group_info = commit_bundle.group_info.get_group_info();
174 let conversation = conversation
175 .external_join_via_group_info_notify(&guest, group_info)
176 .await;
177 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
178 })
179 .await
180 }
181 }
182
183 mod remove_members {
184 use super::*;
185
186 #[apply(all_cred_cipher)]
187 async fn alice_can_remove_bob_from_conversation(case: TestContext) {
188 let [alice, bob] = case.sessions().await;
189 Box::pin(async move {
190 let conversation = case.create_conversation([&alice, &bob]).await.remove_notify(&bob).await;
191 let id = conversation.id().clone();
192
193 let MlsCommitBundle { welcome, .. } = alice.mls_transport().await.latest_commit_bundle().await;
194 assert!(welcome.is_none());
195
196 assert_eq!(conversation.member_count().await, 1);
197
198 assert!(matches!(
200 bob.transaction.conversation(&id).await.unwrap_err(),
201 TransactionError::Leaf(crate::LeafError::ConversationNotFound(ref i))
202 if i == &id
203 ));
204 assert!(!conversation.can_talk(&alice, &bob).await);
205 })
206 .await;
207 }
208
209 #[apply(all_cred_cipher)]
210 async fn should_return_valid_welcome(case: TestContext) {
211 let [alice, bob, guest] = case.sessions().await;
212 Box::pin(async move {
213 let conversation = case
214 .create_conversation([&alice, &bob])
215 .await
216 .invite_proposal_notify(&guest)
217 .await
218 .remove_notify(&bob)
219 .await;
220
221 assert!(conversation.is_functional_and_contains([&alice, &guest]).await);
222 assert!(!conversation.can_talk(&alice, &bob).await);
224 })
225 .await;
226 }
227
228 #[apply(all_cred_cipher)]
229 async fn should_return_valid_group_info(case: TestContext) {
230 let [alice, bob, guest] = case.sessions().await;
231 Box::pin(async move {
232 let conversation = case.create_conversation([&alice, &bob]).await.remove_notify(&bob).await;
233 let commit_bundle = alice.mls_transport().await.latest_commit_bundle().await;
234 let group_info = commit_bundle.group_info.get_group_info();
235 let conversation = conversation
236 .external_join_via_group_info_notify(&guest, group_info)
237 .await;
238
239 assert!(conversation.is_functional_and_contains([&alice, &guest]).await);
240 assert!(!conversation.can_talk(&alice, &bob).await);
242 })
243 .await;
244 }
245 }
246
247 mod update_keying_material {
248 use super::*;
249
250 #[apply(all_cred_cipher)]
251 async fn should_succeed(case: TestContext) {
252 let [alice, bob] = case.sessions().await;
253 Box::pin(async move {
254 let conversation = case.create_conversation([&alice, &bob]).await;
255 let id = conversation.id().clone();
256
257 let init_count = alice.transaction.count_entities().await;
258
259 let bob_keys = bob
260 .get_conversation_unchecked(&id)
261 .await
262 .encryption_keys()
263 .collect::<Vec<Vec<u8>>>();
264 let alice_keys = alice
265 .get_conversation_unchecked(&id)
266 .await
267 .encryption_keys()
268 .collect::<Vec<Vec<u8>>>();
269 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
270
271 let alice_key = alice.encryption_key_of(&id, alice.get_client_id().await).await;
272
273 let conversation = conversation.update_notify().await;
275 let MlsCommitBundle { welcome, .. } = alice.mls_transport().await.latest_commit_bundle().await;
276 assert!(welcome.is_none());
277
278 assert!(
279 !alice
280 .get_conversation_unchecked(&id)
281 .await
282 .encryption_keys()
283 .contains(&alice_key)
284 );
285
286 let alice_new_keys = alice
287 .get_conversation_unchecked(&id)
288 .await
289 .encryption_keys()
290 .collect::<Vec<_>>();
291 assert!(!alice_new_keys.contains(&alice_key));
292
293 let bob_new_keys = bob
294 .get_conversation_unchecked(&id)
295 .await
296 .encryption_keys()
297 .collect::<Vec<_>>();
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 let id = conversation.id().clone();
317
318 let bob_keys = bob
319 .get_conversation_unchecked(&id)
320 .await
321 .signature_keys()
322 .collect::<Vec<SignaturePublicKey>>();
323 let alice_keys = alice
324 .get_conversation_unchecked(&id)
325 .await
326 .signature_keys()
327 .collect::<Vec<SignaturePublicKey>>();
328
329 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
331
332 let alice_key = alice.encryption_key_of(&id, alice.get_client_id().await).await;
333
334 let conversation = conversation.invite_proposal_notify(&charlie).await;
336
337 assert!(
338 alice
339 .get_conversation_unchecked(&id)
340 .await
341 .encryption_keys()
342 .contains(&alice_key)
343 );
344
345 assert_eq!(conversation.member_count().await, 2);
347
348 let conversation = conversation.update_notify().await;
350 let MlsCommitBundle { welcome, .. } = alice.mls_transport().await.latest_commit_bundle().await;
351 assert!(welcome.is_some());
352 assert!(
353 !alice
354 .get_conversation_unchecked(&id)
355 .await
356 .encryption_keys()
357 .contains(&alice_key)
358 );
359
360 assert_eq!(conversation.member_count().await, 3);
361
362 let alice_new_keys = alice
363 .get_conversation_unchecked(&id)
364 .await
365 .encryption_keys()
366 .collect::<Vec<Vec<u8>>>();
367 assert!(!alice_new_keys.contains(&alice_key));
368
369 let bob_new_keys = bob
370 .get_conversation_unchecked(&id)
371 .await
372 .encryption_keys()
373 .collect::<Vec<Vec<u8>>>();
374 assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
375
376 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
378 })
379 .await;
380 }
381
382 #[apply(all_cred_cipher)]
383 async fn should_return_valid_welcome(case: TestContext) {
384 let [alice, bob, guest] = case.sessions().await;
385 Box::pin(async move {
386 let conversation = case
387 .create_conversation([&alice, &bob])
388 .await
389 .invite_proposal_notify(&guest)
390 .await
391 .update_notify()
392 .await;
393
394 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
395 })
396 .await;
397 }
398
399 #[apply(all_cred_cipher)]
400 async fn should_return_valid_group_info(case: TestContext) {
401 let [alice, bob, guest] = case.sessions().await;
402 Box::pin(async move {
403 let conversation = case.create_conversation([&alice, &bob]).await.update_notify().await;
404
405 let group_info = alice.mls_transport().await.latest_group_info().await;
406 let group_info = group_info.get_group_info();
407
408 let conversation = conversation
409 .external_join_via_group_info_notify(&guest, group_info)
410 .await;
411 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
412 })
413 .await;
414 }
415 }
416
417 mod commit_pending_proposals {
418 use super::*;
419
420 #[apply(all_cred_cipher)]
421 async fn should_create_a_commit_out_of_self_pending_proposals(case: TestContext) {
422 let [alice, bob] = case.sessions().await;
423 Box::pin(async move {
424 let conversation = case
425 .create_conversation([&alice])
426 .await
427 .advance_epoch()
428 .await
429 .invite_proposal_notify(&bob)
430 .await;
431 let id = conversation.id.clone();
432
433 assert!(!alice.pending_proposals(&id).await.is_empty());
434 assert_eq!(conversation.member_count().await, 1);
435
436 let conversation = conversation.commit_pending_proposals_notify().await;
437 assert_eq!(conversation.member_count().await, 2);
438
439 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
440 })
441 .await;
442 }
443
444 #[apply(all_cred_cipher)]
445 async fn should_create_a_commit_out_of_pending_proposals_by_ref(case: TestContext) {
446 let [alice, bob, charlie] = case.sessions().await;
447 Box::pin(async move {
448 let conversation = case
450 .create_conversation([&alice, &bob])
451 .await
452 .acting_as(&bob)
453 .await
454 .invite_proposal_notify(&charlie)
455 .await;
456
457 assert!(!bob.pending_proposals(conversation.id()).await.is_empty());
458 assert_eq!(conversation.member_count().await, 2);
459
460 let conversation = conversation.commit_pending_proposals_notify().await;
462 assert_eq!(conversation.member_count().await, 3);
463
464 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
465 })
466 .await;
467 }
468
469 #[apply(all_cred_cipher)]
470 async fn should_return_valid_welcome(case: TestContext) {
471 let [alice, bob] = case.sessions().await;
472 Box::pin(async move {
473 let conversation = case
474 .create_conversation([&alice])
475 .await
476 .invite_proposal_notify(&bob)
477 .await
478 .commit_pending_proposals_notify()
479 .await;
480
481 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
482 })
483 .await;
484 }
485
486 #[apply(all_cred_cipher)]
487 async fn should_return_valid_group_info(case: TestContext) {
488 let [alice, bob, guest] = case.sessions().await;
489 Box::pin(async move {
490 let conversation = case
491 .create_conversation([&alice])
492 .await
493 .invite_proposal_notify(&bob)
494 .await
495 .commit_pending_proposals_notify()
496 .await;
497 let commit_bundle = alice.mls_transport().await.latest_commit_bundle().await;
498 let group_info = commit_bundle.group_info.get_group_info();
499 let conversation = conversation
500 .external_join_via_group_info_notify(&guest, group_info)
501 .await;
502
503 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
504 })
505 .await;
506 }
507 }
508
509 mod delivery_semantics {
510 use super::*;
511
512 #[apply(all_cred_cipher)]
513 async fn should_prevent_out_of_order_commits(case: TestContext) {
514 let [alice, bob] = case.sessions().await;
515 Box::pin(async move {
516 let conversation = case.create_conversation([&alice, &bob]).await;
517 let id = conversation.id().clone();
518
519 let commit_guard = conversation.update().await;
520 let commit1 = commit_guard.message();
521 let commit1 = commit1.to_bytes().unwrap();
522
523 let commit_guard = commit_guard.finish().update().await;
524 let commit2 = commit_guard.message();
525 let commit2 = commit2.to_bytes().unwrap();
526
527 let out_of_order = bob
529 .transaction
530 .conversation(&id)
531 .await
532 .unwrap()
533 .decrypt_message(&commit2)
534 .await;
535 assert!(matches!(out_of_order.unwrap_err(), Error::BufferedFutureMessage { .. }));
536
537 bob.transaction
540 .conversation(&id)
541 .await
542 .unwrap()
543 .decrypt_message(&commit1)
544 .await
545 .unwrap();
546
547 let past_commit = bob
549 .transaction
550 .conversation(&id)
551 .await
552 .unwrap()
553 .decrypt_message(&commit1)
554 .await;
555 assert!(matches!(past_commit.unwrap_err(), Error::StaleCommit));
556 })
557 .await;
558 }
559
560 #[apply(all_cred_cipher)]
561 async fn should_prevent_replayed_encrypted_handshake_messages(case: TestContext) {
562 if !case.is_pure_ciphertext() {
563 return;
564 }
565
566 let [alice, bob] = case.sessions().await;
567 Box::pin(async move {
568 let conversation = case.create_conversation([&alice, &bob]).await;
569
570 let proposal_guard = conversation.update_proposal().await;
571 let proposal_replay = proposal_guard.message();
572
573 let conversation = proposal_guard.notify_members().await;
575 assert!(matches!(
576 conversation
577 .guard_of(&bob)
578 .await
579 .decrypt_message(proposal_replay.to_bytes().unwrap())
580 .await
581 .unwrap_err(),
582 Error::DuplicateMessage
583 ));
584
585 let commit_guard = conversation.update().await;
586 let commit_replay = commit_guard.message();
587
588 let conversation = commit_guard.notify_members().await;
590 assert!(matches!(
591 conversation
592 .guard_of(&bob)
593 .await
594 .decrypt_message(commit_replay.to_bytes().unwrap())
595 .await
596 .unwrap_err(),
597 Error::StaleCommit
598 ));
599 })
600 .await;
601 }
602 }
603}