core_crypto/mls/conversation/
commit.rs1use openmls::prelude::MlsMessageOut;
9
10use super::{Error, Result};
11use crate::{MlsGroupInfoBundle, mls::conversation::WelcomeMessage};
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)]
33 pub fn to_bytes_triple(self) -> Result<(Option<WelcomeMessage>, Vec<u8>, MlsGroupInfoBundle)> {
34 use openmls::prelude::TlsSerializeTrait as _;
35 let welcome = self.welcome.map(Into::into);
36 let commit = self
37 .commit
38 .tls_serialize_detached()
39 .map_err(Error::tls_serialize("serialize commit"))?;
40 Ok((welcome, commit, self.group_info))
41 }
42}
43
44#[cfg(test)]
45mod tests {
46 use super::{Error, *};
47 use crate::{
48 mls::conversation::ConversationWithMls as _, test_utils::*, transaction_context::Error as TransactionError,
49 };
50
51 mod add_members {
52 use std::sync::Arc;
53
54 use super::*;
55 use crate::Credential;
56
57 #[apply(all_cred_cipher)]
58 async fn can_add_members_to_conversation(case: TestContext) {
59 let [alice, bob] = case.sessions().await;
60 Box::pin(async move {
61 let conversation = case.create_conversation([&alice]).await;
62 let id = conversation.id.clone();
63 let bob_keypackage = bob.new_keypackage(&case).await;
64 alice
66 .replace_transport(Arc::<CoreCryptoTransportAbortProvider>::default())
67 .await;
68 alice
69 .transaction
70 .conversation(&id)
71 .await
72 .unwrap()
73 .add_members(vec![bob_keypackage.clone().into()])
74 .await
75 .unwrap_err();
76
77 assert_eq!(conversation.member_count().await, 1);
79
80 alice
81 .replace_transport(Arc::<CoreCryptoTransportSuccessProvider>::default())
82 .await;
83
84 let conversation = conversation.invite_notify([&bob]).await;
85
86 assert_eq!(*conversation.id(), id);
87 assert_eq!(
88 conversation
89 .guard()
90 .await
91 .conversation()
92 .await
93 .group
94 .group_id()
95 .as_slice(),
96 id.as_ref()
97 );
98 assert_eq!(conversation.member_count().await, 2);
99 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
100 })
101 .await
102 }
103
104 #[apply(all_cred_cipher)]
105 async fn should_fail_on_duplicate_signatures(case: TestContext) {
106 let [alice, bob, carol] = case.sessions().await;
107 Box::pin(async move {
108 let conversation = case.create_conversation([&alice]).await;
109 let id = conversation.id.clone();
110 let bob_keypackage = bob.new_keypackage(&case).await;
111 let signature_key_pair = bob
112 .find_any_credential(case.ciphersuite(), case.credential_type)
113 .await
114 .signature_key_pair
115 .clone();
116 let credential = Credential {
117 ciphersuite: case.ciphersuite(),
118 credential_type: CredentialType::Basic,
119 mls_credential: openmls::credentials::Credential::new_basic(
120 carol.get_client_id().await.into_inner(),
121 ),
122 signature_key_pair,
123 earliest_validity: 0,
124 };
125 let cred_ref = carol.add_credential(credential).await.unwrap();
126 let carol_key_package = carol.new_keypackage_from_ref(cred_ref, None).await;
127 let _affected_clients = [(carol.get_client_id().await, bob.get_client_id().await)];
128
129 let error = alice
130 .transaction
131 .conversation(&id)
132 .await
133 .unwrap()
134 .add_members(vec![bob_keypackage.clone().into(), carol_key_package.clone().into()])
135 .await
136 .unwrap_err();
137
138 assert!(matches!(
139 error,
140 Error::DuplicateSignature {
141 affected_clients: _affected_clients
142 }
143 ));
144 })
145 .await
146 }
147
148 #[apply(all_cred_cipher)]
149 async fn should_return_valid_welcome(case: TestContext) {
150 let [alice, bob] = case.sessions().await;
151 Box::pin(async move {
152 let conversation = case.create_conversation([&alice, &bob]).await;
153 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
154 })
155 .await
156 }
157
158 #[apply(all_cred_cipher)]
159 async fn should_return_valid_group_info(case: TestContext) {
160 let [alice, bob, guest] = case.sessions().await;
161 Box::pin(async move {
162 let conversation = case.create_conversation([&alice, &bob]).await;
163 let commit_bundle = alice.mls_transport().await.latest_commit_bundle().await;
164 let group_info = commit_bundle.group_info.get_group_info();
165 let conversation = conversation
166 .external_join_via_group_info_notify(&guest, group_info)
167 .await;
168 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
169 })
170 .await
171 }
172 }
173
174 mod remove_members {
175 use super::*;
176
177 #[apply(all_cred_cipher)]
178 async fn alice_can_remove_bob_from_conversation(case: TestContext) {
179 let [alice, bob] = case.sessions().await;
180 Box::pin(async move {
181 let conversation = case.create_conversation([&alice, &bob]).await.remove_notify(&bob).await;
182 let id = conversation.id().clone();
183
184 let MlsCommitBundle { welcome, .. } = alice.mls_transport().await.latest_commit_bundle().await;
185 assert!(welcome.is_none());
186
187 assert_eq!(conversation.member_count().await, 1);
188
189 assert!(matches!(
191 bob.transaction.conversation(&id).await.unwrap_err(),
192 TransactionError::Leaf(crate::LeafError::ConversationNotFound(ref i))
193 if i == &id
194 ));
195 assert!(!conversation.can_talk(&alice, &bob).await);
196 })
197 .await;
198 }
199
200 #[apply(all_cred_cipher)]
201 async fn should_return_valid_group_info(case: TestContext) {
202 let [alice, bob, guest] = case.sessions().await;
203 Box::pin(async move {
204 let conversation = case.create_conversation([&alice, &bob]).await.remove_notify(&bob).await;
205 let commit_bundle = alice.mls_transport().await.latest_commit_bundle().await;
206 let group_info = commit_bundle.group_info.get_group_info();
207 let conversation = conversation
208 .external_join_via_group_info_notify(&guest, group_info)
209 .await;
210
211 assert!(conversation.is_functional_and_contains([&alice, &guest]).await);
212 assert!(!conversation.can_talk(&alice, &bob).await);
214 })
215 .await;
216 }
217 }
218
219 mod update_keying_material {
220 use super::*;
221
222 #[apply(all_cred_cipher)]
223 async fn should_succeed(case: TestContext) {
224 let [alice, bob] = case.sessions().await;
225 Box::pin(async move {
226 let conversation = case.create_conversation([&alice, &bob]).await;
227 let init_count = alice.transaction.count_entities().await;
228
229 let bob_keys = conversation
230 .guard_of(&bob)
231 .await
232 .conversation()
233 .await
234 .encryption_keys()
235 .collect::<Vec<Vec<u8>>>();
236 let alice_keys = conversation
237 .guard()
238 .await
239 .conversation()
240 .await
241 .encryption_keys()
242 .collect::<Vec<Vec<u8>>>();
243 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
244
245 let alice_key = conversation.encryption_public_key().await;
246
247 let conversation = conversation.update_notify().await;
249 let MlsCommitBundle { welcome, .. } = alice.mls_transport().await.latest_commit_bundle().await;
250 assert!(welcome.is_none());
251
252 let alice_new_keys = conversation
253 .guard()
254 .await
255 .conversation()
256 .await
257 .encryption_keys()
258 .collect::<Vec<Vec<u8>>>();
259 assert!(!alice_new_keys.contains(&alice_key));
260
261 let bob_new_keys = conversation
262 .guard_of(&bob)
263 .await
264 .conversation()
265 .await
266 .encryption_keys()
267 .collect::<Vec<Vec<u8>>>();
268 assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
269
270 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
272
273 let final_count = alice.transaction.count_entities().await;
276 assert_eq!(init_count, final_count);
277 })
278 .await;
279 }
280
281 #[apply(all_cred_cipher)]
282 async fn should_return_valid_group_info(case: TestContext) {
283 let [alice, bob, guest] = case.sessions().await;
284 Box::pin(async move {
285 let conversation = case.create_conversation([&alice, &bob]).await.update_notify().await;
286
287 let group_info = alice.mls_transport().await.latest_group_info().await;
288 let group_info = group_info.get_group_info();
289
290 let conversation = conversation
291 .external_join_via_group_info_notify(&guest, group_info)
292 .await;
293 assert!(conversation.is_functional_and_contains([&alice, &bob, &guest]).await);
294 })
295 .await;
296 }
297 }
298
299 mod commit_pending_proposals {
300 use super::*;
301
302 #[apply(all_cred_cipher)]
303 async fn should_create_a_commit_out_of_pending_proposals_by_ref(case: TestContext) {
304 let [alice, bob, charlie] = case.sessions().await;
305 Box::pin(async move {
306 let conversation = case
308 .create_conversation([&alice, &bob, &charlie])
309 .await
310 .acting_as(&bob)
311 .await
312 .remove_proposal_notify(&charlie)
313 .await
314 .acting_as(&bob)
315 .await;
316
317 assert!(conversation.has_pending_proposals().await);
318 assert_eq!(conversation.member_count().await, 3);
319
320 let conversation = conversation.commit_pending_proposals_notify().await;
322 assert_eq!(conversation.member_count().await, 2);
323
324 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
325 })
326 .await;
327 }
328 }
329
330 mod delivery_semantics {
331 use super::*;
332
333 #[apply(all_cred_cipher)]
334 async fn should_prevent_out_of_order_commits(case: TestContext) {
335 let [alice, bob] = case.sessions().await;
336 Box::pin(async move {
337 let conversation = case.create_conversation([&alice, &bob]).await;
338 let id = conversation.id().clone();
339
340 let commit_guard = conversation.update().await;
341 let commit1 = commit_guard.message();
342 let commit1 = commit1.to_bytes().unwrap();
343
344 let commit_guard = commit_guard.finish().update().await;
345 let commit2 = commit_guard.message();
346 let commit2 = commit2.to_bytes().unwrap();
347
348 let out_of_order = bob
350 .transaction
351 .conversation(&id)
352 .await
353 .unwrap()
354 .decrypt_message(&commit2)
355 .await;
356 assert!(matches!(out_of_order.unwrap_err(), Error::BufferedFutureMessage { .. }));
357
358 bob.transaction
361 .conversation(&id)
362 .await
363 .unwrap()
364 .decrypt_message(&commit1)
365 .await
366 .unwrap();
367
368 let past_commit = bob
370 .transaction
371 .conversation(&id)
372 .await
373 .unwrap()
374 .decrypt_message(&commit1)
375 .await;
376 assert!(matches!(past_commit.unwrap_err(), Error::StaleCommit));
377 })
378 .await;
379 }
380
381 #[apply(all_cred_cipher)]
382 async fn should_prevent_replayed_encrypted_handshake_messages(case: TestContext) {
383 if !case.is_pure_ciphertext() {
384 return;
385 }
386
387 let [alice, bob] = case.sessions().await;
388 Box::pin(async move {
389 let conversation = case.create_conversation([&alice, &bob]).await;
390
391 let commit_guard = conversation.update().await;
392 let commit_replay = commit_guard.message();
393
394 let conversation = commit_guard.notify_members().await;
396 assert!(matches!(
397 conversation
398 .guard_of(&bob)
399 .await
400 .decrypt_message(commit_replay.to_bytes().unwrap())
401 .await
402 .unwrap_err(),
403 Error::StaleCommit
404 ));
405 })
406 .await;
407 }
408 }
409}