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