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 use crate::Credential;
114
115 #[apply(all_cred_cipher)]
116 async fn can_add_members_to_conversation(case: TestContext) {
117 let [alice, bob] = case.sessions().await;
118 Box::pin(async move {
119 let conversation = case.create_conversation([&alice]).await;
120 let id = conversation.id.clone();
121 let bob_keypackage = bob.new_keypackage(&case).await;
122 alice
124 .replace_transport(Arc::<CoreCryptoTransportAbortProvider>::default())
125 .await;
126 alice
127 .transaction
128 .conversation(&id)
129 .await
130 .unwrap()
131 .add_members(vec![bob_keypackage.clone().into()])
132 .await
133 .unwrap_err();
134
135 assert_eq!(conversation.member_count().await, 1);
137
138 alice
139 .replace_transport(Arc::<CoreCryptoTransportSuccessProvider>::default())
140 .await;
141
142 let conversation = conversation.invite_notify([&bob]).await;
143
144 assert_eq!(*conversation.id(), id);
145 assert_eq!(
146 conversation
147 .guard()
148 .await
149 .conversation()
150 .await
151 .group
152 .group_id()
153 .as_slice(),
154 id.as_ref()
155 );
156 assert_eq!(conversation.member_count().await, 2);
157 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
158 })
159 .await
160 }
161
162 #[apply(all_cred_cipher)]
163 async fn should_fail_on_duplicate_signatures(case: TestContext) {
164 let [alice, bob, carol] = case.sessions().await;
165 Box::pin(async move {
166 let conversation = case.create_conversation([&alice]).await;
167 let id = conversation.id.clone();
168 let bob_keypackage = bob.new_keypackage(&case).await;
169 let signature_key_pair = bob
170 .find_any_credential(case.ciphersuite(), case.credential_type)
171 .await
172 .signature_key_pair
173 .clone();
174 let credential = Credential {
175 ciphersuite: case.ciphersuite(),
176 credential_type: CredentialType::Basic,
177 mls_credential: openmls::credentials::Credential::new_basic(
178 carol.get_client_id().await.into_inner(),
179 ),
180 signature_key_pair,
181 earliest_validity: 0,
182 };
183 let cred_ref = carol.add_credential(credential).await.unwrap();
184 let carol_key_package = carol.new_keypackage_from_ref(cred_ref, None).await;
185 let _affected_clients = [(carol.get_client_id().await, bob.get_client_id().await)];
186
187 let error = alice
188 .transaction
189 .conversation(&id)
190 .await
191 .unwrap()
192 .add_members(vec![bob_keypackage.clone().into(), carol_key_package.clone().into()])
193 .await
194 .unwrap_err();
195
196 assert!(matches!(
197 error,
198 Error::DuplicateSignature {
199 affected_clients: _affected_clients
200 }
201 ));
202 })
203 .await
204 }
205
206 #[apply(all_cred_cipher)]
207 async fn should_return_valid_welcome(case: TestContext) {
208 let [alice, bob] = case.sessions().await;
209 Box::pin(async move {
210 let conversation = case.create_conversation([&alice, &bob]).await;
211 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
212 })
213 .await
214 }
215
216 #[apply(all_cred_cipher)]
217 async fn should_return_valid_group_info(case: TestContext) {
218 let [alice, bob, guest] = case.sessions().await;
219 Box::pin(async move {
220 let conversation = case.create_conversation([&alice, &bob]).await;
221 let commit_bundle = alice.mls_transport().await.latest_commit_bundle().await;
222 let group_info = commit_bundle.group_info.get_group_info();
223 let conversation = conversation
224 .external_join_via_group_info_notify(&guest, group_info)
225 .await;
226 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
227 })
228 .await
229 }
230 }
231
232 mod remove_members {
233 use super::*;
234
235 #[apply(all_cred_cipher)]
236 async fn alice_can_remove_bob_from_conversation(case: TestContext) {
237 let [alice, bob] = case.sessions().await;
238 Box::pin(async move {
239 let conversation = case.create_conversation([&alice, &bob]).await.remove_notify(&bob).await;
240 let id = conversation.id().clone();
241
242 let MlsCommitBundle { welcome, .. } = alice.mls_transport().await.latest_commit_bundle().await;
243 assert!(welcome.is_none());
244
245 assert_eq!(conversation.member_count().await, 1);
246
247 assert!(matches!(
249 bob.transaction.conversation(&id).await.unwrap_err(),
250 TransactionError::Leaf(crate::LeafError::ConversationNotFound(ref i))
251 if i == &id
252 ));
253 assert!(!conversation.can_talk(&alice, &bob).await);
254 })
255 .await;
256 }
257
258 #[apply(all_cred_cipher)]
259 async fn should_return_valid_welcome(case: TestContext) {
260 let [alice, bob, guest] = case.sessions().await;
261 Box::pin(async move {
262 let conversation = case
263 .create_conversation([&alice, &bob])
264 .await
265 .invite_proposal_notify(&guest)
266 .await
267 .remove_notify(&bob)
268 .await;
269
270 assert!(conversation.is_functional_and_contains([&alice, &guest]).await);
271 assert!(!conversation.can_talk(&alice, &bob).await);
273 })
274 .await;
275 }
276
277 #[apply(all_cred_cipher)]
278 async fn should_return_valid_group_info(case: TestContext) {
279 let [alice, bob, guest] = case.sessions().await;
280 Box::pin(async move {
281 let conversation = case.create_conversation([&alice, &bob]).await.remove_notify(&bob).await;
282 let commit_bundle = alice.mls_transport().await.latest_commit_bundle().await;
283 let group_info = commit_bundle.group_info.get_group_info();
284 let conversation = conversation
285 .external_join_via_group_info_notify(&guest, group_info)
286 .await;
287
288 assert!(conversation.is_functional_and_contains([&alice, &guest]).await);
289 assert!(!conversation.can_talk(&alice, &bob).await);
291 })
292 .await;
293 }
294 }
295
296 mod update_keying_material {
297 use super::*;
298
299 #[apply(all_cred_cipher)]
300 async fn should_succeed(case: TestContext) {
301 let [alice, bob] = case.sessions().await;
302 Box::pin(async move {
303 let conversation = case.create_conversation([&alice, &bob]).await;
304 let init_count = alice.transaction.count_entities().await;
305
306 let bob_keys = conversation
307 .guard_of(&bob)
308 .await
309 .conversation()
310 .await
311 .encryption_keys()
312 .collect::<Vec<Vec<u8>>>();
313 let alice_keys = conversation
314 .guard()
315 .await
316 .conversation()
317 .await
318 .encryption_keys()
319 .collect::<Vec<Vec<u8>>>();
320 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
321
322 let alice_key = conversation.encryption_public_key().await;
323
324 let conversation = conversation.update_notify().await;
326 let MlsCommitBundle { welcome, .. } = alice.mls_transport().await.latest_commit_bundle().await;
327 assert!(welcome.is_none());
328
329 let alice_new_keys = conversation
330 .guard()
331 .await
332 .conversation()
333 .await
334 .encryption_keys()
335 .collect::<Vec<Vec<u8>>>();
336 assert!(!alice_new_keys.contains(&alice_key));
337
338 let bob_new_keys = conversation
339 .guard_of(&bob)
340 .await
341 .conversation()
342 .await
343 .encryption_keys()
344 .collect::<Vec<Vec<u8>>>();
345 assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
346
347 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
349
350 let final_count = alice.transaction.count_entities().await;
353 assert_eq!(init_count, final_count);
354 })
355 .await;
356 }
357
358 #[apply(all_cred_cipher)]
359 async fn should_create_welcome_for_pending_add_proposals(case: TestContext) {
360 let [alice, bob, charlie] = case.sessions().await;
361 Box::pin(async move {
362 let conversation = case.create_conversation([&alice, &bob]).await;
363
364 let bob_keys = conversation
365 .guard_of(&bob)
366 .await
367 .conversation()
368 .await
369 .signature_keys()
370 .collect::<Vec<SignaturePublicKey>>();
371 let alice_keys = conversation
372 .guard()
373 .await
374 .conversation()
375 .await
376 .signature_keys()
377 .collect::<Vec<SignaturePublicKey>>();
378
379 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
381
382 let alice_key = conversation.encryption_public_key().await;
383
384 let conversation = conversation.invite_proposal_notify(&charlie).await;
386
387 assert!(
388 conversation
389 .guard()
390 .await
391 .conversation()
392 .await
393 .encryption_keys()
394 .contains(&alice_key)
395 );
396
397 assert_eq!(conversation.member_count().await, 2);
399
400 let conversation = conversation.update_notify().await;
402 let MlsCommitBundle { welcome, .. } = alice.mls_transport().await.latest_commit_bundle().await;
403 assert!(welcome.is_some());
404 assert!(
405 !conversation
406 .guard()
407 .await
408 .conversation()
409 .await
410 .encryption_keys()
411 .contains(&alice_key)
412 );
413
414 assert_eq!(conversation.member_count().await, 3);
415
416 let alice_new_keys = conversation
417 .guard()
418 .await
419 .conversation()
420 .await
421 .encryption_keys()
422 .collect::<Vec<Vec<u8>>>();
423 assert!(!alice_new_keys.contains(&alice_key));
424
425 let bob_new_keys = conversation
426 .guard_of(&bob)
427 .await
428 .conversation()
429 .await
430 .encryption_keys()
431 .collect::<Vec<Vec<u8>>>();
432 assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
433
434 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
436 })
437 .await;
438 }
439
440 #[apply(all_cred_cipher)]
441 async fn should_return_valid_welcome(case: TestContext) {
442 let [alice, bob, guest] = case.sessions().await;
443 Box::pin(async move {
444 let conversation = case
445 .create_conversation([&alice, &bob])
446 .await
447 .invite_proposal_notify(&guest)
448 .await
449 .update_notify()
450 .await;
451
452 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
453 })
454 .await;
455 }
456
457 #[apply(all_cred_cipher)]
458 async fn should_return_valid_group_info(case: TestContext) {
459 let [alice, bob, guest] = case.sessions().await;
460 Box::pin(async move {
461 let conversation = case.create_conversation([&alice, &bob]).await.update_notify().await;
462
463 let group_info = alice.mls_transport().await.latest_group_info().await;
464 let group_info = group_info.get_group_info();
465
466 let conversation = conversation
467 .external_join_via_group_info_notify(&guest, group_info)
468 .await;
469 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
470 })
471 .await;
472 }
473 }
474
475 mod commit_pending_proposals {
476 use super::*;
477
478 #[apply(all_cred_cipher)]
479 async fn should_create_a_commit_out_of_self_pending_proposals(case: TestContext) {
480 let [alice, bob] = case.sessions().await;
481 Box::pin(async move {
482 let conversation = case
483 .create_conversation([&alice])
484 .await
485 .advance_epoch()
486 .await
487 .invite_proposal_notify(&bob)
488 .await;
489
490 assert!(conversation.has_pending_proposals().await);
491 assert_eq!(conversation.member_count().await, 1);
492
493 let conversation = conversation.commit_pending_proposals_notify().await;
494 assert_eq!(conversation.member_count().await, 2);
495
496 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
497 })
498 .await;
499 }
500
501 #[apply(all_cred_cipher)]
502 async fn should_create_a_commit_out_of_pending_proposals_by_ref(case: TestContext) {
503 let [alice, bob, charlie] = case.sessions().await;
504 Box::pin(async move {
505 let conversation = case
507 .create_conversation([&alice, &bob])
508 .await
509 .acting_as(&bob)
510 .await
511 .invite_proposal_notify(&charlie)
512 .await
513 .acting_as(&bob)
514 .await;
515
516 assert!(conversation.has_pending_proposals().await);
517 assert_eq!(conversation.member_count().await, 2);
518
519 let conversation = conversation.commit_pending_proposals_notify().await;
521 assert_eq!(conversation.member_count().await, 3);
522
523 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
524 })
525 .await;
526 }
527
528 #[apply(all_cred_cipher)]
529 async fn should_return_valid_welcome(case: TestContext) {
530 let [alice, bob] = case.sessions().await;
531 Box::pin(async move {
532 let conversation = case
533 .create_conversation([&alice])
534 .await
535 .invite_proposal_notify(&bob)
536 .await
537 .commit_pending_proposals_notify()
538 .await;
539
540 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
541 })
542 .await;
543 }
544
545 #[apply(all_cred_cipher)]
546 async fn should_return_valid_group_info(case: TestContext) {
547 let [alice, bob, guest] = case.sessions().await;
548 Box::pin(async move {
549 let conversation = case
550 .create_conversation([&alice])
551 .await
552 .invite_proposal_notify(&bob)
553 .await
554 .commit_pending_proposals_notify()
555 .await;
556 let commit_bundle = alice.mls_transport().await.latest_commit_bundle().await;
557 let group_info = commit_bundle.group_info.get_group_info();
558 let conversation = conversation
559 .external_join_via_group_info_notify(&guest, group_info)
560 .await;
561
562 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
563 })
564 .await;
565 }
566 }
567
568 mod delivery_semantics {
569 use super::*;
570
571 #[apply(all_cred_cipher)]
572 async fn should_prevent_out_of_order_commits(case: TestContext) {
573 let [alice, bob] = case.sessions().await;
574 Box::pin(async move {
575 let conversation = case.create_conversation([&alice, &bob]).await;
576 let id = conversation.id().clone();
577
578 let commit_guard = conversation.update().await;
579 let commit1 = commit_guard.message();
580 let commit1 = commit1.to_bytes().unwrap();
581
582 let commit_guard = commit_guard.finish().update().await;
583 let commit2 = commit_guard.message();
584 let commit2 = commit2.to_bytes().unwrap();
585
586 let out_of_order = bob
588 .transaction
589 .conversation(&id)
590 .await
591 .unwrap()
592 .decrypt_message(&commit2)
593 .await;
594 assert!(matches!(out_of_order.unwrap_err(), Error::BufferedFutureMessage { .. }));
595
596 bob.transaction
599 .conversation(&id)
600 .await
601 .unwrap()
602 .decrypt_message(&commit1)
603 .await
604 .unwrap();
605
606 let past_commit = bob
608 .transaction
609 .conversation(&id)
610 .await
611 .unwrap()
612 .decrypt_message(&commit1)
613 .await;
614 assert!(matches!(past_commit.unwrap_err(), Error::StaleCommit));
615 })
616 .await;
617 }
618
619 #[apply(all_cred_cipher)]
620 async fn should_prevent_replayed_encrypted_handshake_messages(case: TestContext) {
621 if !case.is_pure_ciphertext() {
622 return;
623 }
624
625 let [alice, bob] = case.sessions().await;
626 Box::pin(async move {
627 let conversation = case.create_conversation([&alice, &bob]).await;
628
629 let proposal_guard = conversation.update_proposal().await;
630 let proposal_replay = proposal_guard.message();
631
632 let conversation = proposal_guard.notify_members().await;
634 assert!(matches!(
635 conversation
636 .guard_of(&bob)
637 .await
638 .decrypt_message(proposal_replay.to_bytes().unwrap())
639 .await
640 .unwrap_err(),
641 Error::DuplicateMessage
642 ));
643
644 let commit_guard = conversation.update().await;
645 let commit_replay = commit_guard.message();
646
647 let conversation = commit_guard.notify_members().await;
649 assert!(matches!(
650 conversation
651 .guard_of(&bob)
652 .await
653 .decrypt_message(commit_replay.to_bytes().unwrap())
654 .await
655 .unwrap_err(),
656 Error::StaleCommit
657 ));
658 })
659 .await;
660 }
661 }
662}