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