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_x509() {
146 let [alice_central] = case.sessions().await;
147 Box::pin(async move {
148 let x509_test_chain = alice_central
149 .x509_test_chain
150 .as_ref()
151 .as_ref()
152 .expect("No x509 test chain");
153
154 let id = conversation_id();
155 alice_central
156 .transaction
157 .new_conversation(&id, case.credential_type, case.cfg.clone())
158 .await
159 .unwrap();
160
161 assert!(alice_central.pending_commit(&id).await.is_none());
162
163 let intermediate_ca = x509_test_chain.find_local_intermediate_ca();
164
165 let (new_handle, new_display_name) = ("new_alice_wire", "New Alice Smith");
170 let cb = alice_central
171 .save_new_credential(&case, new_handle, new_display_name, intermediate_ca)
172 .await;
173
174 let commit = alice_central.create_unmerged_e2ei_rotate_commit(&id, &cb).await;
176 assert!(alice_central.pending_commit(&id).await.is_some());
177
178 let epoch = alice_central.transaction.conversation(&id).await.unwrap().epoch().await;
179
180 let decrypt_self = alice_central
182 .transaction
183 .conversation(&id)
184 .await
185 .unwrap()
186 .decrypt_message(&commit.commit.to_bytes().unwrap())
187 .await;
188 assert!(decrypt_self.is_ok());
189 let decrypt_self = decrypt_self.unwrap();
190
191 let epoch_after_decrypt = alice_central.transaction.conversation(&id).await.unwrap().epoch().await;
192 assert_eq!(epoch + 1, epoch_after_decrypt);
193
194 assert!(decrypt_self.proposals.is_empty());
196
197 alice_central.verify_sender_identity(&case, &decrypt_self).await;
199 alice_central
200 .verify_local_credential_rotated(&id, new_handle, new_display_name)
201 .await;
202 })
203 .await
204 }
205 }
206
207 #[apply(all_cred_cipher)]
209 #[wasm_bindgen_test]
210 pub async fn should_succeed_when_incoming_commit_mismatches_pending_commit(case: TestContext) {
211 if !case.is_pure_ciphertext() {
212 let [alice_central] = case.sessions().await;
213 Box::pin(async move {
214 let id = conversation_id();
215 alice_central
216 .transaction
217 .new_conversation(&id, case.credential_type, case.cfg.clone())
218 .await
219 .unwrap();
220
221 assert!(alice_central.pending_commit(&id).await.is_none());
222
223 let unmerged_commit = alice_central.create_unmerged_commit(&id).await.commit;
225 assert!(alice_central.pending_commit(&id).await.is_some());
226 alice_central
227 .transaction
228 .conversation(&id)
229 .await
230 .unwrap()
231 .clear_pending_commit()
232 .await
233 .unwrap();
234 assert!(alice_central.pending_commit(&id).await.is_none());
235
236 let unmerged_commit2 = alice_central.create_unmerged_commit(&id).await.commit;
238 assert_ne!(unmerged_commit, unmerged_commit2);
239
240 let decrypt = alice_central
241 .transaction
242 .conversation(&id)
243 .await
244 .unwrap()
245 .decrypt_message(&unmerged_commit.to_bytes().unwrap())
246 .await;
247 assert!(matches!(decrypt.unwrap_err(), Error::ClearingPendingCommitError));
248 })
249 .await
250 }
251 }
252
253 #[apply(all_cred_cipher)]
255 #[wasm_bindgen_test]
256 pub async fn should_ignore_self_incoming_commit_when_no_pending_commit(case: TestContext) {
257 if !case.is_pure_ciphertext() {
258 let [alice_central] = case.sessions().await;
259 Box::pin(async move {
260 let id = conversation_id();
261 alice_central
262 .transaction
263 .new_conversation(&id, case.credential_type, case.cfg.clone())
264 .await
265 .unwrap();
266
267 assert!(alice_central.pending_commit(&id).await.is_none());
268
269 let commit = alice_central.create_unmerged_commit(&id).await.commit;
271 assert!(alice_central.pending_commit(&id).await.is_some());
272
273 alice_central
275 .transaction
276 .conversation(&id)
277 .await
278 .unwrap()
279 .clear_pending_commit()
280 .await
281 .unwrap();
282 assert!(alice_central.pending_commit(&id).await.is_none());
283
284 let decrypt_self = alice_central
285 .transaction
286 .conversation(&id)
287 .await
288 .unwrap()
289 .decrypt_message(&commit.to_bytes().unwrap())
290 .await;
291 assert!(matches!(decrypt_self.unwrap_err(), Error::SelfCommitIgnored));
293 })
294 .await
295 }
296 }
297
298 #[apply(all_cred_cipher)]
299 #[wasm_bindgen_test]
300 pub async fn should_fail_when_tampering_with_incoming_own_commit_same_as_pending(case: TestContext) {
301 use crate::MlsErrorKind;
302
303 if case.is_pure_ciphertext() {
304 return;
307 }
308
309 let [alice_central] = case.sessions().await;
310 Box::pin(async move {
311 let conversation_id = conversation_id();
312 alice_central
313 .transaction
314 .new_conversation(&conversation_id, case.credential_type, case.cfg.clone())
315 .await
316 .unwrap();
317
318 assert!(alice_central.pending_commit(&conversation_id).await.is_none());
320
321 let add_bob_message = alice_central.create_unmerged_commit(&conversation_id).await.commit;
323
324 assert!(alice_central.pending_commit(&conversation_id).await.is_some());
326
327 let commit_serialized = &mut add_bob_message.to_bytes().unwrap();
328
329 commit_serialized[300] = commit_serialized[300].wrapping_add(1);
333
334 let decryption_result = alice_central
335 .transaction
336 .conversation(&conversation_id)
337 .await
338 .unwrap()
339 .decrypt_message(commit_serialized)
340 .await;
341 let error = decryption_result.unwrap_err();
342 assert!(matches!(
343 error,
344 Error::Mls(MlsError {
345 source: MlsErrorKind::MlsMessageError(ProcessMessageError::ValidationError(
346 ValidationError::InvalidMembershipTag
347 )),
348 ..
349 })
350 ));
351
352 assert!(alice_central.pending_commit(&conversation_id).await.is_some());
354
355 assert!(
357 alice_central
358 .transaction
359 .conversation(&conversation_id)
360 .await
361 .unwrap()
362 .decrypt_message(&add_bob_message.to_bytes().unwrap())
363 .await
364 .is_ok()
365 );
366
367 assert!(alice_central.pending_commit(&conversation_id).await.is_none());
369 })
370 .await
371 }
372}