1use 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};
1213impl MlsConversation {
14/// Returns the confirmation tag from a public message that is an own commit.
15 /// Returns an error if the confirmation tag in the own commit is missing.
16pub(crate) fn extract_confirmation_tag_from_own_commit<'a>(
17&self,
18 own_commit: &'a MlsMessageIn,
19 ) -> CryptoResult<&'a ConfirmationTag> {
20match own_commit.body_as_ref() {
21 MlsMessageInBody::PublicMessage(msg) => {
22let is_commit = matches!(msg.content_type(), ContentType::Commit);
23let own_index = self.group.own_leaf_index();
24let is_self_sent = matches!(msg.sender(), Sender::Member(i) if i == &own_index);
25let is_own_commit = is_commit && is_self_sent;
2627match is_own_commit.then_some(msg.body()) {
28Some(FramedContentBodyIn::Commit(_)) => {
29let confirmation_tag = msg
30 .auth
31 .confirmation_tag
32 .as_ref()
33 .ok_or(CryptoError::InternalMlsError)?;
34Ok(confirmation_tag)
35 }
36// Not an own commit. Should never be reached if this function
37 // is called correctly.
38_ => unreachable!(
39"extract_confirmation_tag_from_own_commit() must always be called \
40 with an own commit."
41),
42 }
43 }
44// Not a public message. Should never be reached if this function is called correctly.
45_ => unreachable!(
46"extract_confirmation_tag_from_own_commit() must always be called \
47 with an MlsMessageIn containing an MlsMessageInBody::PublicMessage"
48),
49 }
50 }
5152pub(crate) async fn handle_own_commit(
53&mut self,
54 backend: &MlsCryptoProvider,
55 ct: &ConfirmationTag,
56 ) -> CryptoResult<MlsConversationDecryptMessage> {
57if self.group.pending_commit().is_some() {
58if self.eq_pending_commit(ct) {
59// incoming is from ourselves and it's the same as the local pending commit
60 // => merge the pending commit & continue
61self.merge_pending_commit(backend).await
62} else {
63// this would mean we created a commit that got accepted by the DS but we cleared it locally
64 // then somehow retried and created another commit. This is a manifest client error
65 // and should be identified as such
66Err(CryptoError::ClearingPendingCommitError)
67 }
68 } else {
69// This either means the DS replayed one of our commit OR we cleared a commit accepted by the DS
70 // In both cases, CoreCrypto cannot be of any help since it cannot decrypt self commits
71 // => deflect this case and let the caller handle it
72Err(CryptoError::SelfCommitIgnored)
73 }
74 }
7576/// Compare incoming commit with local pending commit
77pub(crate) fn eq_pending_commit(&self, commit_ct: &ConfirmationTag) -> bool {
78if let Some(pending_commit) = self.group.pending_commit() {
79return pending_commit.get_confirmation_tag() == commit_ct;
80 }
81false
82}
8384/// When the incoming commit is sent by ourselves and it's the same as the local pending commit.
85 /// This adapts [Self::commit_accepted] to return the same as [MlsConversation::decrypt_message]
86pub(crate) async fn merge_pending_commit(
87&mut self,
88 backend: &MlsCryptoProvider,
89 ) -> CryptoResult<MlsConversationDecryptMessage> {
90self.commit_accepted(backend).await?;
9192let own_leaf = self.group.own_leaf().ok_or(CryptoError::InternalMlsError)?;
9394// We return self identity here, probably not necessary to check revocation
95let own_leaf_credential_with_key = CredentialWithKey {
96 credential: own_leaf.credential().clone(),
97 signature_key: own_leaf.signature_key().clone(),
98 };
99let identity = own_leaf_credential_with_key.extract_identity(self.ciphersuite(), None)?;
100101let crl_new_distribution_points =
102 get_new_crl_distribution_points(backend, extract_crl_uris_from_group(&self.group)?).await?;
103104Ok(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}
117118#[cfg(test)]
119mod tests {
120use crate::test_utils::*;
121use openmls::prelude::{ProcessMessageError, ValidationError};
122123use crate::prelude::{CryptoError, MlsError};
124125use wasm_bindgen_test::*;
126127wasm_bindgen_test_configure!(run_in_browser);
128129// If there’s a pending commit & it matches the incoming commit: mark pending commit as accepted
130#[apply(all_cred_cipher)]
131 #[wasm_bindgen_test]
132pub async fn should_succeed_when_incoming_commit_same_as_pending(case: TestCase) {
133if !case.is_pure_ciphertext() && case.is_x509() {
134 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
135 Box::pin(async move {
136let x509_test_chain = alice_central
137 .x509_test_chain
138 .as_ref()
139 .as_ref()
140 .expect("No x509 test chain");
141142let id = conversation_id();
143 alice_central
144 .context
145 .new_conversation(&id, case.credential_type, case.cfg.clone())
146 .await
147.unwrap();
148149assert!(alice_central.pending_commit(&id).await.is_none());
150151let alice_og_cert = &x509_test_chain
152 .actors
153 .iter()
154 .find(|actor| actor.name == "alice")
155 .unwrap()
156 .certificate;
157158// change credential to verify later what we return in the decrypt message
159let (new_handle, new_display_name) = ("new_alice_wire", "New Alice Smith");
160let 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;
169170// create a commit. This will also store it in the store
171let commit = alice_central.context.e2ei_rotate(&id, Some(&cb)).await.unwrap().commit;
172assert!(alice_central.pending_commit(&id).await.is_some());
173174// since the pending commit is the same as the incoming one, it should succeed
175let decrypt_self = alice_central
176 .context
177 .decrypt_message(&id, &commit.to_bytes().unwrap())
178 .await;
179assert!(decrypt_self.is_ok());
180let decrypt_self = decrypt_self.unwrap();
181182// there is no proposals to renew here since it's our own commit we merge
183assert!(decrypt_self.proposals.is_empty());
184185// verify that we return the new identity
186alice_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 }
195196// If there’s a pending commit & it does not match the self incoming commit: fail with dedicated error
197#[apply(all_cred_cipher)]
198 #[wasm_bindgen_test]
199pub async fn should_succeed_when_incoming_commit_mismatches_pending_commit(case: TestCase) {
200if !case.is_pure_ciphertext() {
201 run_test_with_client_ids(
202 case.clone(),
203 ["alice", "bob", "charlie"],
204move |[alice_central, bob_central, charlie_central]| {
205 Box::pin(async move {
206let id = conversation_id();
207 alice_central
208 .context
209 .new_conversation(&id, case.credential_type, case.cfg.clone())
210 .await
211.unwrap();
212213assert!(alice_central.pending_commit(&id).await.is_none());
214215let bob = bob_central.rand_key_package(&case).await;
216let charlie = charlie_central.rand_key_package(&case).await;
217218// create a first commit then discard it from the store to be able to create a second one
219let add_bob = alice_central
220 .context
221 .add_members_to_conversation(&id, vec![bob])
222 .await
223.unwrap();
224assert!(alice_central.pending_commit(&id).await.is_some());
225 alice_central.context.clear_pending_commit(&id).await.unwrap();
226assert!(alice_central.pending_commit(&id).await.is_none());
227228// create another commit for the sole purpose of having it in the store
229let add_charlie = alice_central
230 .context
231 .add_members_to_conversation(&id, vec![charlie])
232 .await
233.unwrap();
234assert!(alice_central.pending_commit(&id).await.is_some());
235assert_ne!(add_bob.commit, add_charlie.commit);
236237let decrypt = alice_central
238 .context
239 .decrypt_message(&id, &add_bob.commit.to_bytes().unwrap())
240 .await;
241assert!(matches!(decrypt.unwrap_err(), CryptoError::ClearingPendingCommitError));
242 })
243 },
244 )
245 .await
246}
247 }
248249// if there’s no pending commit & and the incoming commit originates from self: succeed by ignoring the incoming commit
250#[apply(all_cred_cipher)]
251 #[wasm_bindgen_test]
252pub async fn should_ignore_self_incoming_commit_when_no_pending_commit(case: TestCase) {
253if !case.is_pure_ciphertext() {
254 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
255 Box::pin(async move {
256let id = conversation_id();
257 alice_central
258 .context
259 .new_conversation(&id, case.credential_type, case.cfg.clone())
260 .await
261.unwrap();
262263assert!(alice_central.pending_commit(&id).await.is_none());
264265// create a commit, have it in store...
266let commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
267assert!(alice_central.pending_commit(&id).await.is_some());
268269// then delete the pending commit
270alice_central.context.clear_pending_commit(&id).await.unwrap();
271assert!(alice_central.pending_commit(&id).await.is_none());
272273let decrypt_self = alice_central
274 .context
275 .decrypt_message(&id, &commit.to_bytes().unwrap())
276 .await;
277// this means DS replayed the commit. In that case just ignore, we have already merged the commit anyway
278assert!(matches!(decrypt_self.unwrap_err(), CryptoError::SelfCommitIgnored));
279 })
280 })
281 .await
282}
283 }
284285#[apply(all_cred_cipher)]
286 #[wasm_bindgen_test]
287pub async fn should_fail_when_tampering_with_incoming_own_commit_same_as_pending(case: TestCase) {
288if case.is_pure_ciphertext() {
289return;
290 };
291 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
292 Box::pin(async move {
293let conversation_id = conversation_id();
294 alice_central
295 .context
296 .new_conversation(&conversation_id, case.credential_type, case.cfg.clone())
297 .await
298.unwrap();
299300// No pending commit yet.
301assert!(alice_central.pending_commit(&conversation_id).await.is_none());
302303let bob_key_package = bob_central.rand_key_package(&case).await;
304305// Create the commit that we're going to tamper with.
306let add_bob_message = alice_central
307 .context
308 .add_members_to_conversation(&conversation_id, vec![bob_key_package])
309 .await
310.unwrap();
311312// Now there is a pending commit.
313assert!(alice_central.pending_commit(&conversation_id).await.is_some());
314315let commit_serialized = &mut add_bob_message.commit.to_bytes().unwrap();
316317// Tamper with the commit; this is the signature region, however,
318 // the membership tag covers the signature, so this will result in an
319 // invalid membership tag error emitted by openmls.
320commit_serialized[355] = commit_serialized[355].wrapping_add(1);
321322let decryption_result = alice_central
323 .context
324 .decrypt_message(&conversation_id, commit_serialized)
325 .await;
326assert!(matches!(
327 decryption_result.unwrap_err(),
328 CryptoError::MlsError(MlsError::MlsMessageError(ProcessMessageError::ValidationError(
329 ValidationError::InvalidMembershipTag
330 )))
331 ));
332333// There is still a pending commit.
334assert!(alice_central.pending_commit(&conversation_id).await.is_some());
335336// Positive case: Alice decrypts the commit...
337assert!(alice_central
338 .context
339 .decrypt_message(&conversation_id, &add_bob_message.commit.to_bytes().unwrap())
340 .await
341.is_ok());
342343// ...and has cleared the pending commit.
344assert!(alice_central.pending_commit(&conversation_id).await.is_none());
345 })
346 })
347 .await
348}
349}