core_crypto/mls/conversation/
own_commit.rs1use crate::{
2 mls::credential::{
3 crl::{extract_crl_uris_from_group, get_new_crl_distribution_points},
4 ext::CredentialExt,
5 },
6 prelude::{CryptoError, CryptoResult, MlsConversation, MlsConversationDecryptMessage},
7};
8use mls_crypto_provider::MlsCryptoProvider;
9use openmls::prelude::{
10 ConfirmationTag, ContentType, CredentialWithKey, FramedContentBodyIn, MlsMessageIn, MlsMessageInBody, Sender,
11};
12
13impl MlsConversation {
14 pub(crate) fn extract_confirmation_tag_from_own_commit<'a>(
17 &self,
18 own_commit: &'a MlsMessageIn,
19 ) -> CryptoResult<&'a ConfirmationTag> {
20 match own_commit.body_as_ref() {
21 MlsMessageInBody::PublicMessage(msg) => {
22 let is_commit = matches!(msg.content_type(), ContentType::Commit);
23 let own_index = self.group.own_leaf_index();
24 let is_self_sent = matches!(msg.sender(), Sender::Member(i) if i == &own_index);
25 let is_own_commit = is_commit && is_self_sent;
26
27 match is_own_commit.then_some(msg.body()) {
28 Some(FramedContentBodyIn::Commit(_)) => {
29 let confirmation_tag = msg
30 .auth
31 .confirmation_tag
32 .as_ref()
33 .ok_or(CryptoError::InternalMlsError)?;
34 Ok(confirmation_tag)
35 }
36 _ => unreachable!(
39 "extract_confirmation_tag_from_own_commit() must always be called \
40 with an own commit."
41 ),
42 }
43 }
44 _ => unreachable!(
46 "extract_confirmation_tag_from_own_commit() must always be called \
47 with an MlsMessageIn containing an MlsMessageInBody::PublicMessage"
48 ),
49 }
50 }
51
52 pub(crate) async fn handle_own_commit(
53 &mut self,
54 backend: &MlsCryptoProvider,
55 ct: &ConfirmationTag,
56 ) -> CryptoResult<MlsConversationDecryptMessage> {
57 if self.group.pending_commit().is_some() {
58 if self.eq_pending_commit(ct) {
59 self.merge_pending_commit(backend).await
62 } else {
63 Err(CryptoError::ClearingPendingCommitError)
67 }
68 } else {
69 Err(CryptoError::SelfCommitIgnored)
73 }
74 }
75
76 pub(crate) fn eq_pending_commit(&self, commit_ct: &ConfirmationTag) -> bool {
78 if let Some(pending_commit) = self.group.pending_commit() {
79 return pending_commit.get_confirmation_tag() == commit_ct;
80 }
81 false
82 }
83
84 pub(crate) async fn merge_pending_commit(
87 &mut self,
88 backend: &MlsCryptoProvider,
89 ) -> CryptoResult<MlsConversationDecryptMessage> {
90 self.commit_accepted(backend).await?;
91
92 let own_leaf = self.group.own_leaf().ok_or(CryptoError::InternalMlsError)?;
93
94 let own_leaf_credential_with_key = CredentialWithKey {
96 credential: own_leaf.credential().clone(),
97 signature_key: own_leaf.signature_key().clone(),
98 };
99 let identity = own_leaf_credential_with_key.extract_identity(self.ciphersuite(), None)?;
100
101 let crl_new_distribution_points =
102 get_new_crl_distribution_points(backend, extract_crl_uris_from_group(&self.group)?).await?;
103
104 Ok(MlsConversationDecryptMessage {
105 app_msg: None,
106 proposals: vec![],
107 is_active: self.group.is_active(),
108 delay: self.compute_next_commit_delay(),
109 sender_client_id: None,
110 has_epoch_changed: true,
111 identity,
112 buffered_messages: None,
113 crl_new_distribution_points,
114 })
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use crate::test_utils::*;
121 use openmls::prelude::{ProcessMessageError, ValidationError};
122
123 use crate::prelude::{CryptoError, MlsError};
124
125 use wasm_bindgen_test::*;
126
127 wasm_bindgen_test_configure!(run_in_browser);
128
129 #[apply(all_cred_cipher)]
131 #[wasm_bindgen_test]
132 pub async fn should_succeed_when_incoming_commit_same_as_pending(case: TestCase) {
133 if !case.is_pure_ciphertext() && case.is_x509() {
134 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
135 Box::pin(async move {
136 let x509_test_chain = alice_central
137 .x509_test_chain
138 .as_ref()
139 .as_ref()
140 .expect("No x509 test chain");
141
142 let id = conversation_id();
143 alice_central
144 .context
145 .new_conversation(&id, case.credential_type, case.cfg.clone())
146 .await
147 .unwrap();
148
149 assert!(alice_central.pending_commit(&id).await.is_none());
150
151 let alice_og_cert = &x509_test_chain
152 .actors
153 .iter()
154 .find(|actor| actor.name == "alice")
155 .unwrap()
156 .certificate;
157
158 let (new_handle, new_display_name) = ("new_alice_wire", "New Alice Smith");
160 let cb = alice_central
161 .rotate_credential(
162 &case,
163 new_handle,
164 new_display_name,
165 alice_og_cert,
166 x509_test_chain.find_local_intermediate_ca(),
167 )
168 .await;
169
170 let commit = alice_central.context.e2ei_rotate(&id, Some(&cb)).await.unwrap().commit;
172 assert!(alice_central.pending_commit(&id).await.is_some());
173
174 let decrypt_self = alice_central
176 .context
177 .decrypt_message(&id, &commit.to_bytes().unwrap())
178 .await;
179 assert!(decrypt_self.is_ok());
180 let decrypt_self = decrypt_self.unwrap();
181
182 assert!(decrypt_self.proposals.is_empty());
184
185 alice_central.verify_sender_identity(&case, &decrypt_self).await;
187 alice_central
188 .verify_local_credential_rotated(&id, new_handle, new_display_name)
189 .await;
190 })
191 })
192 .await
193 }
194 }
195
196 #[apply(all_cred_cipher)]
198 #[wasm_bindgen_test]
199 pub async fn should_succeed_when_incoming_commit_mismatches_pending_commit(case: TestCase) {
200 if !case.is_pure_ciphertext() {
201 run_test_with_client_ids(
202 case.clone(),
203 ["alice", "bob", "charlie"],
204 move |[alice_central, bob_central, charlie_central]| {
205 Box::pin(async move {
206 let id = conversation_id();
207 alice_central
208 .context
209 .new_conversation(&id, case.credential_type, case.cfg.clone())
210 .await
211 .unwrap();
212
213 assert!(alice_central.pending_commit(&id).await.is_none());
214
215 let bob = bob_central.rand_key_package(&case).await;
216 let charlie = charlie_central.rand_key_package(&case).await;
217
218 let add_bob = alice_central
220 .context
221 .add_members_to_conversation(&id, vec![bob])
222 .await
223 .unwrap();
224 assert!(alice_central.pending_commit(&id).await.is_some());
225 alice_central.context.clear_pending_commit(&id).await.unwrap();
226 assert!(alice_central.pending_commit(&id).await.is_none());
227
228 let add_charlie = alice_central
230 .context
231 .add_members_to_conversation(&id, vec![charlie])
232 .await
233 .unwrap();
234 assert!(alice_central.pending_commit(&id).await.is_some());
235 assert_ne!(add_bob.commit, add_charlie.commit);
236
237 let decrypt = alice_central
238 .context
239 .decrypt_message(&id, &add_bob.commit.to_bytes().unwrap())
240 .await;
241 assert!(matches!(decrypt.unwrap_err(), CryptoError::ClearingPendingCommitError));
242 })
243 },
244 )
245 .await
246 }
247 }
248
249 #[apply(all_cred_cipher)]
251 #[wasm_bindgen_test]
252 pub async fn should_ignore_self_incoming_commit_when_no_pending_commit(case: TestCase) {
253 if !case.is_pure_ciphertext() {
254 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
255 Box::pin(async move {
256 let id = conversation_id();
257 alice_central
258 .context
259 .new_conversation(&id, case.credential_type, case.cfg.clone())
260 .await
261 .unwrap();
262
263 assert!(alice_central.pending_commit(&id).await.is_none());
264
265 let commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
267 assert!(alice_central.pending_commit(&id).await.is_some());
268
269 alice_central.context.clear_pending_commit(&id).await.unwrap();
271 assert!(alice_central.pending_commit(&id).await.is_none());
272
273 let decrypt_self = alice_central
274 .context
275 .decrypt_message(&id, &commit.to_bytes().unwrap())
276 .await;
277 assert!(matches!(decrypt_self.unwrap_err(), CryptoError::SelfCommitIgnored));
279 })
280 })
281 .await
282 }
283 }
284
285 #[apply(all_cred_cipher)]
286 #[wasm_bindgen_test]
287 pub async fn should_fail_when_tampering_with_incoming_own_commit_same_as_pending(case: TestCase) {
288 if case.is_pure_ciphertext() {
289 return;
290 };
291 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
292 Box::pin(async move {
293 let conversation_id = conversation_id();
294 alice_central
295 .context
296 .new_conversation(&conversation_id, case.credential_type, case.cfg.clone())
297 .await
298 .unwrap();
299
300 assert!(alice_central.pending_commit(&conversation_id).await.is_none());
302
303 let bob_key_package = bob_central.rand_key_package(&case).await;
304
305 let add_bob_message = alice_central
307 .context
308 .add_members_to_conversation(&conversation_id, vec![bob_key_package])
309 .await
310 .unwrap();
311
312 assert!(alice_central.pending_commit(&conversation_id).await.is_some());
314
315 let commit_serialized = &mut add_bob_message.commit.to_bytes().unwrap();
316
317 commit_serialized[355] = commit_serialized[355].wrapping_add(1);
321
322 let decryption_result = alice_central
323 .context
324 .decrypt_message(&conversation_id, commit_serialized)
325 .await;
326 assert!(matches!(
327 decryption_result.unwrap_err(),
328 CryptoError::MlsError(MlsError::MlsMessageError(ProcessMessageError::ValidationError(
329 ValidationError::InvalidMembershipTag
330 )))
331 ));
332
333 assert!(alice_central.pending_commit(&conversation_id).await.is_some());
335
336 assert!(alice_central
338 .context
339 .decrypt_message(&conversation_id, &add_bob_message.commit.to_bytes().unwrap())
340 .await
341 .is_ok());
342
343 assert!(alice_central.pending_commit(&conversation_id).await.is_none());
345 })
346 })
347 .await
348 }
349}