core_crypto/mls/conversation/
own_commit.rs1use super::{Error, Result};
2use crate::{
3 RecursiveError,
4 mls::credential::{
5 crl::{extract_crl_uris_from_group, get_new_crl_distribution_points},
6 ext::CredentialExt,
7 },
8 prelude::{MlsConversation, MlsConversationDecryptMessage, Session},
9};
10use mls_crypto_provider::MlsCryptoProvider;
11use openmls::prelude::{
12 ConfirmationTag, ContentType, CredentialWithKey, FramedContentBodyIn, MlsMessageIn, MlsMessageInBody, Sender,
13};
14
15impl MlsConversation {
16 pub(crate) fn extract_confirmation_tag_from_own_commit<'a>(
19 &self,
20 own_commit: &'a MlsMessageIn,
21 ) -> Result<&'a ConfirmationTag> {
22 if let MlsMessageInBody::PublicMessage(msg) = own_commit.body_as_ref() {
23 let is_commit = matches!(msg.content_type(), ContentType::Commit);
24 let own_index = self.group.own_leaf_index();
25 let is_self_sent = matches!(msg.sender(), Sender::Member(i) if i == &own_index);
26 let is_own_commit = is_commit && is_self_sent;
27
28 assert!(
29 is_own_commit,
30 "extract_confirmation_tag_from_own_commit() must always be called with an own commit."
31 );
32 assert!(
33 matches!(msg.body(), FramedContentBodyIn::Commit(_)),
34 "extract_confirmation_tag_from_own_commit() must always be called with an own commit."
35 );
36
37 msg.auth
38 .confirmation_tag
39 .as_ref()
40 .ok_or(Error::MlsMessageInvalidState("Message confirmation tag not present"))
41 } else {
42 panic!(
43 "extract_confirmation_tag_from_own_commit() must always be called \
44 with an MlsMessageIn containing an MlsMessageInBody::PublicMessage"
45 );
46 }
47 }
48
49 pub(crate) async fn handle_own_commit(
50 &mut self,
51 client: &Session,
52 backend: &MlsCryptoProvider,
53 ct: &ConfirmationTag,
54 ) -> Result<MlsConversationDecryptMessage> {
55 if self.group.pending_commit().is_none() {
56 return Err(Error::SelfCommitIgnored);
60 }
61
62 if !self.eq_pending_commit(ct) {
63 return Err(Error::ClearingPendingCommitError);
67 }
68
69 self.merge_pending_commit(client, backend).await
72 }
73
74 pub(crate) fn eq_pending_commit(&self, commit_ct: &ConfirmationTag) -> bool {
76 if let Some(pending_commit) = self.group.pending_commit() {
77 return pending_commit.get_confirmation_tag() == commit_ct;
78 }
79 false
80 }
81
82 pub(crate) async fn merge_pending_commit(
85 &mut self,
86 client: &Session,
87 backend: &MlsCryptoProvider,
88 ) -> Result<MlsConversationDecryptMessage> {
89 self.commit_accepted(client, backend).await?;
90
91 let own_leaf = self
92 .group
93 .own_leaf()
94 .ok_or(Error::MlsGroupInvalidState("own_leaf is None"))?;
95
96 let own_leaf_credential_with_key = CredentialWithKey {
98 credential: own_leaf.credential().clone(),
99 signature_key: own_leaf.signature_key().clone(),
100 };
101 let identity = own_leaf_credential_with_key
102 .extract_identity(self.ciphersuite(), None)
103 .map_err(RecursiveError::mls_credential("extracting identity"))?;
104
105 let crl_new_distribution_points = get_new_crl_distribution_points(
106 backend,
107 extract_crl_uris_from_group(&self.group)
108 .map_err(RecursiveError::mls_credential("extracting crl uris from group"))?,
109 )
110 .await
111 .map_err(RecursiveError::mls_credential("getting new crl distribution points"))?;
112
113 #[expect(deprecated)]
115 Ok(MlsConversationDecryptMessage {
116 app_msg: None,
117 proposals: vec![],
118 is_active: self.group.is_active(),
119 delay: self.compute_next_commit_delay(),
120 sender_client_id: None,
121 has_epoch_changed: true,
122 identity,
123 buffered_messages: None,
124 crl_new_distribution_points,
125 })
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use crate::test_utils::*;
132 use openmls::prelude::{ProcessMessageError, ValidationError};
133
134 use super::super::error::Error;
135 use crate::prelude::MlsError;
136
137 use crate::mls::conversation::Conversation as _;
138 use wasm_bindgen_test::*;
139
140 wasm_bindgen_test_configure!(run_in_browser);
141 #[apply(all_cred_cipher)]
143 #[wasm_bindgen_test]
144 pub async fn should_succeed_when_incoming_commit_same_as_pending(case: TestContext) {
145 if case.is_pure_ciphertext() || case.is_basic() {
146 return;
147 }
148 let [alice] = case.sessions().await;
149 Box::pin(async move {
150 let x509_test_chain = alice.x509_chain_unchecked();
151
152 let conversation = case.create_conversation([&alice]).await;
153
154 assert!(!conversation.has_pending_commit().await);
155 let epoch = conversation.guard().await.epoch().await;
156
157 let intermediate_ca = x509_test_chain.find_local_intermediate_ca();
158
159 let (new_handle, new_display_name) = ("new_alice_wire", "New Alice Smith");
164 let cb = alice
165 .save_new_credential(&case, new_handle, new_display_name, intermediate_ca)
166 .await;
167
168 let commit_guard = conversation.e2ei_rotate_unmerged(&cb).await;
170 assert!(commit_guard.conversation().has_pending_commit().await);
171
172 let conversation = commit_guard.notify_members_and_verify_sender().await;
174
175 let epoch_after_decrypt = conversation.guard().await.epoch().await;
176 assert_eq!(epoch + 1, epoch_after_decrypt);
177
178 assert!(!conversation.has_pending_proposals().await);
180
181 alice
183 .verify_local_credential_rotated(conversation.id(), new_handle, new_display_name)
184 .await;
185 })
186 .await
187 }
188
189 #[apply(all_cred_cipher)]
191 #[wasm_bindgen_test]
192 pub async fn should_succeed_when_incoming_commit_mismatches_pending_commit(case: TestContext) {
193 if case.is_pure_ciphertext() {
194 return;
195 }
196 let [alice] = case.sessions().await;
197 Box::pin(async move {
198 let conversation = case.create_conversation([&alice]).await;
199
200 assert!(!conversation.has_pending_commit().await);
201
202 let commit_guard = conversation.update_unmerged().await;
204 let unmerged_commit = commit_guard.message();
205 assert!(commit_guard.conversation().has_pending_commit().await);
206 let conversation = commit_guard.finish();
207 conversation.guard().await.clear_pending_commit().await.unwrap();
208 assert!(!conversation.has_pending_commit().await);
209
210 let commit_guard = conversation.update_unmerged().await;
212 let unmerged_commit2 = commit_guard.message();
213 assert_ne!(unmerged_commit, unmerged_commit2);
214 let conversation = commit_guard.finish();
215
216 let decrypt = conversation
217 .guard()
218 .await
219 .decrypt_message(&unmerged_commit.to_bytes().unwrap())
220 .await;
221 assert!(matches!(decrypt.unwrap_err(), Error::ClearingPendingCommitError));
222 })
223 .await
224 }
225
226 #[apply(all_cred_cipher)]
228 #[wasm_bindgen_test]
229 pub async fn should_ignore_self_incoming_commit_when_no_pending_commit(case: TestContext) {
230 if case.is_pure_ciphertext() {
231 return;
232 }
233 let [alice] = case.sessions().await;
234 Box::pin(async move {
235 let conversation = case.create_conversation([&alice]).await;
236
237 assert!(!conversation.has_pending_commit().await);
238
239 let commit_guard = conversation.update_unmerged().await;
241 let conversation = commit_guard.conversation();
242 assert!(conversation.has_pending_commit().await);
243
244 conversation.guard().await.clear_pending_commit().await.unwrap();
246 assert!(!conversation.has_pending_commit().await);
247
248 let (_, decrypt_self) = commit_guard.notify_member_fallible(&alice).await;
249 assert!(matches!(decrypt_self.unwrap_err(), Error::SelfCommitIgnored));
251 })
252 .await
253 }
254
255 #[apply(all_cred_cipher)]
256 #[wasm_bindgen_test]
257 pub async fn should_fail_when_tampering_with_incoming_own_commit_same_as_pending(case: TestContext) {
258 use crate::MlsErrorKind;
259
260 if case.is_pure_ciphertext() {
261 return;
264 }
265
266 let [alice] = case.sessions().await;
267 let conversation = case.create_conversation([&alice]).await;
268 Box::pin(async move {
269 assert!(!conversation.has_pending_commit().await);
271
272 let commit_guard = conversation.update_unmerged().await;
274 let add_bob_message = commit_guard.message();
275 let conversation = commit_guard.conversation();
276
277 assert!(conversation.has_pending_commit().await);
279
280 let commit_serialized = &mut add_bob_message.to_bytes().unwrap();
281
282 commit_serialized[300] = commit_serialized[300].wrapping_add(1);
286
287 let decryption_result = conversation.guard().await.decrypt_message(commit_serialized).await;
288 let error = decryption_result.unwrap_err();
289 assert!(matches!(
290 error,
291 Error::Mls(MlsError {
292 source: MlsErrorKind::MlsMessageError(ProcessMessageError::ValidationError(
293 ValidationError::InvalidMembershipTag
294 )),
295 ..
296 })
297 ));
298
299 assert!(conversation.has_pending_commit().await);
301
302 assert!(
304 conversation
305 .guard()
306 .await
307 .decrypt_message(&add_bob_message.to_bytes().unwrap())
308 .await
309 .is_ok()
310 );
311
312 assert!(!conversation.has_pending_commit().await);
314 })
315 .await
316 }
317
318 #[apply(all_cred_cipher)]
319 #[wasm_bindgen_test]
320 async fn should_succeed_when_incoming_commit_is_self_commit_but_was_lost(case: TestContext) {
321 if case.is_pure_ciphertext() {
322 return;
325 }
326
327 let [mut alice, bob] = case.sessions().await;
328 let conversation = case.create_conversation([&alice, &bob]).await;
329 let conversation_id = conversation.id().to_owned();
330
331 drop(conversation);
332 alice.commit_transaction().await;
334 let conversation = TestConversation::new_from_existing(&case, conversation_id.clone(), [&alice, &bob]).await;
335
336 let commit_guard = conversation.update_unmerged().await.notify_member(&bob).await;
339 let unmerged_commit = commit_guard.message().to_bytes().unwrap();
340 let _conversation = commit_guard.finish();
341
342 alice.pretend_crash().await;
345
346 alice
348 .transaction
349 .conversation(&conversation_id)
350 .await
351 .unwrap()
352 .decrypt_message(&unmerged_commit)
353 .await
354 .unwrap_err();
355 return;
360
361 #[expect(unreachable_code)]
362 {
363 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
365 }
366 }
367}