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 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
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(
172 &case,
173 new_handle,
174 new_display_name,
175 x509_test_chain.find_certificate_for_actor("alice").unwrap(),
176 intermediate_ca,
177 )
178 .await;
179
180 let commit = alice_central.create_unmerged_e2ei_rotate_commit(&id, &cb).await;
182 assert!(alice_central.pending_commit(&id).await.is_some());
183
184 let epoch = alice_central.transaction.conversation(&id).await.unwrap().epoch().await;
185
186 let decrypt_self = alice_central
188 .transaction
189 .conversation(&id)
190 .await
191 .unwrap()
192 .decrypt_message(&commit.commit.to_bytes().unwrap())
193 .await;
194 assert!(decrypt_self.is_ok());
195 let decrypt_self = decrypt_self.unwrap();
196
197 let epoch_after_decrypt = alice_central.transaction.conversation(&id).await.unwrap().epoch().await;
198 assert_eq!(epoch + 1, epoch_after_decrypt);
199
200 assert!(decrypt_self.proposals.is_empty());
202
203 alice_central.verify_sender_identity(&case, &decrypt_self).await;
205 alice_central
206 .verify_local_credential_rotated(&id, new_handle, new_display_name)
207 .await;
208 })
209 })
210 .await
211 }
212 }
213
214 #[apply(all_cred_cipher)]
216 #[wasm_bindgen_test]
217 pub async fn should_succeed_when_incoming_commit_mismatches_pending_commit(case: TestContext) {
218 if !case.is_pure_ciphertext() {
219 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
220 Box::pin(async move {
221 let id = conversation_id();
222 alice_central
223 .transaction
224 .new_conversation(&id, case.credential_type, case.cfg.clone())
225 .await
226 .unwrap();
227
228 assert!(alice_central.pending_commit(&id).await.is_none());
229
230 let unmerged_commit = alice_central.create_unmerged_commit(&id).await.commit;
232 assert!(alice_central.pending_commit(&id).await.is_some());
233 alice_central
234 .transaction
235 .conversation(&id)
236 .await
237 .unwrap()
238 .clear_pending_commit()
239 .await
240 .unwrap();
241 assert!(alice_central.pending_commit(&id).await.is_none());
242
243 let unmerged_commit2 = alice_central.create_unmerged_commit(&id).await.commit;
245 assert_ne!(unmerged_commit, unmerged_commit2);
246
247 let decrypt = alice_central
248 .transaction
249 .conversation(&id)
250 .await
251 .unwrap()
252 .decrypt_message(&unmerged_commit.to_bytes().unwrap())
253 .await;
254 assert!(matches!(decrypt.unwrap_err(), Error::ClearingPendingCommitError));
255 })
256 })
257 .await
258 }
259 }
260
261 #[apply(all_cred_cipher)]
263 #[wasm_bindgen_test]
264 pub async fn should_ignore_self_incoming_commit_when_no_pending_commit(case: TestContext) {
265 if !case.is_pure_ciphertext() {
266 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
267 Box::pin(async move {
268 let id = conversation_id();
269 alice_central
270 .transaction
271 .new_conversation(&id, case.credential_type, case.cfg.clone())
272 .await
273 .unwrap();
274
275 assert!(alice_central.pending_commit(&id).await.is_none());
276
277 let commit = alice_central.create_unmerged_commit(&id).await.commit;
279 assert!(alice_central.pending_commit(&id).await.is_some());
280
281 alice_central
283 .transaction
284 .conversation(&id)
285 .await
286 .unwrap()
287 .clear_pending_commit()
288 .await
289 .unwrap();
290 assert!(alice_central.pending_commit(&id).await.is_none());
291
292 let decrypt_self = alice_central
293 .transaction
294 .conversation(&id)
295 .await
296 .unwrap()
297 .decrypt_message(&commit.to_bytes().unwrap())
298 .await;
299 assert!(matches!(decrypt_self.unwrap_err(), Error::SelfCommitIgnored));
301 })
302 })
303 .await
304 }
305 }
306
307 #[apply(all_cred_cipher)]
308 #[wasm_bindgen_test]
309 pub async fn should_fail_when_tampering_with_incoming_own_commit_same_as_pending(case: TestContext) {
310 use crate::MlsErrorKind;
311
312 if case.is_pure_ciphertext() {
313 return;
316 }
317 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
318 Box::pin(async move {
319 let conversation_id = conversation_id();
320 alice_central
321 .transaction
322 .new_conversation(&conversation_id, case.credential_type, case.cfg.clone())
323 .await
324 .unwrap();
325
326 assert!(alice_central.pending_commit(&conversation_id).await.is_none());
328
329 let add_bob_message = alice_central.create_unmerged_commit(&conversation_id).await.commit;
331
332 assert!(alice_central.pending_commit(&conversation_id).await.is_some());
334
335 let commit_serialized = &mut add_bob_message.to_bytes().unwrap();
336
337 commit_serialized[355] = commit_serialized[355].wrapping_add(1);
341
342 let decryption_result = alice_central
343 .transaction
344 .conversation(&conversation_id)
345 .await
346 .unwrap()
347 .decrypt_message(commit_serialized)
348 .await;
349 let error = decryption_result.unwrap_err();
350 assert!(matches!(
351 error,
352 Error::Mls(MlsError {
353 source: MlsErrorKind::MlsMessageError(ProcessMessageError::ValidationError(
354 ValidationError::InvalidMembershipTag
355 )),
356 ..
357 })
358 ));
359
360 assert!(alice_central.pending_commit(&conversation_id).await.is_some());
362
363 assert!(
365 alice_central
366 .transaction
367 .conversation(&conversation_id)
368 .await
369 .unwrap()
370 .decrypt_message(&add_bob_message.to_bytes().unwrap())
371 .await
372 .is_ok()
373 );
374
375 assert!(alice_central.pending_commit(&conversation_id).await.is_none());
377 })
378 })
379 .await
380 }
381}