core_crypto/transaction_context/e2e_identity/
conversation_state.rs

1use crate::{
2    MlsError, RecursiveError,
3    prelude::{MlsCredentialType, Session},
4};
5
6use openmls_traits::OpenMlsCryptoProvider;
7
8use crate::transaction_context::TransactionContext;
9use openmls::{messages::group_info::VerifiableGroupInfo, prelude::Node};
10
11use super::Result;
12
13/// Indicates the state of a Conversation regarding end-to-end identity.
14///
15/// Note: this does not check pending state (pending commit, pending proposals) so it does not
16/// consider members about to be added/removed
17#[derive(Debug, Clone, Copy, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
18#[repr(u8)]
19pub enum E2eiConversationState {
20    /// All clients have a valid E2EI certificate
21    Verified = 1,
22    /// Some clients are either still Basic or their certificate is expired
23    NotVerified,
24    /// All clients are still Basic. If all client have expired certificates, [E2eiConversationState::NotVerified] is returned.
25    NotEnabled,
26}
27
28impl TransactionContext {
29    /// See [crate::mls::session::Session::e2ei_verify_group_state].
30    pub async fn e2ei_verify_group_state(&self, group_info: VerifiableGroupInfo) -> Result<E2eiConversationState> {
31        let mls_provider = self
32            .mls_provider()
33            .await
34            .map_err(RecursiveError::transaction("getting mls provider"))?;
35        let auth_service = mls_provider.authentication_service();
36        auth_service.refresh_time_of_interest().await;
37        let cs = group_info.ciphersuite().into();
38
39        let is_sender = true; // verify the ratchet tree as sender to turn on hardened verification
40        let Ok(rt) = group_info
41            .take_ratchet_tree(
42                &self
43                    .mls_provider()
44                    .await
45                    .map_err(RecursiveError::transaction("getting mls provider"))?,
46                is_sender,
47            )
48            .await
49        else {
50            return Ok(E2eiConversationState::NotVerified);
51        };
52
53        let credentials = rt.iter().filter_map(|n| match n {
54            Some(Node::LeafNode(ln)) => Some(ln.credential()),
55            _ => None,
56        });
57
58        let auth_service = auth_service.borrow().await;
59        Ok(Session::compute_conversation_state(cs, credentials, MlsCredentialType::X509, auth_service.as_ref()).await)
60    }
61
62    /// See [crate::mls::session::Session::get_credential_in_use].
63    pub async fn get_credential_in_use(
64        &self,
65        group_info: VerifiableGroupInfo,
66        credential_type: MlsCredentialType,
67    ) -> Result<E2eiConversationState> {
68        let cs = group_info.ciphersuite().into();
69        // Not verifying the supplied the GroupInfo here could let attackers lure the clients about
70        // the e2ei state of a conversation and as a consequence degrade this conversation for all
71        // participants once joining it.
72        // This 👇 verifies the GroupInfo and the RatchetTree btw
73        let rt = group_info
74            .take_ratchet_tree(
75                &self
76                    .mls_provider()
77                    .await
78                    .map_err(RecursiveError::transaction("getting mls provider"))?,
79                false,
80            )
81            .await
82            .map_err(MlsError::wrap("taking ratchet tree"))?;
83        let mls_provider = self
84            .mls_provider()
85            .await
86            .map_err(RecursiveError::transaction("getting mls provider"))?;
87        let auth_service = mls_provider.authentication_service().borrow().await;
88        Session::get_credential_in_use_in_ratchet_tree(cs, rt, credential_type, auth_service.as_ref())
89            .await
90            .map_err(RecursiveError::mls_client("getting credentials in use"))
91            .map_err(Into::into)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::mls::conversation::Conversation as _;
99    use crate::{
100        prelude::{CertificateBundle, MlsCredentialType, Session},
101        test_utils::*,
102    };
103    use wasm_bindgen_test::*;
104
105    wasm_bindgen_test_configure!(run_in_browser);
106
107    // testing the case where both Bob & Alice have the same Credential type
108    #[apply(all_cred_cipher)]
109    #[wasm_bindgen_test]
110    async fn uniform_conversation_should_be_not_verified_when_basic(case: TestContext) {
111        let [alice_central, bob_central] = case.sessions().await;
112        Box::pin(async move {
113            let id = conversation_id();
114
115            // That way the conversation creator (Alice) will have the same credential type as Bob
116            let creator_ct = case.credential_type;
117            alice_central
118                .transaction
119                .new_conversation(&id, creator_ct, case.cfg.clone())
120                .await
121                .unwrap();
122            alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
123
124            match case.credential_type {
125                MlsCredentialType::Basic => {
126                    let alice_state = alice_central
127                        .transaction
128                        .conversation(&id)
129                        .await
130                        .unwrap()
131                        .e2ei_conversation_state()
132                        .await
133                        .unwrap();
134                    let bob_state = bob_central
135                        .transaction
136                        .conversation(&id)
137                        .await
138                        .unwrap()
139                        .e2ei_conversation_state()
140                        .await
141                        .unwrap();
142                    assert_eq!(alice_state, E2eiConversationState::NotEnabled);
143                    assert_eq!(bob_state, E2eiConversationState::NotEnabled);
144
145                    let gi = alice_central.get_group_info(&id).await;
146                    let state = alice_central
147                        .transaction
148                        .get_credential_in_use(gi, MlsCredentialType::X509)
149                        .await
150                        .unwrap();
151                    assert_eq!(state, E2eiConversationState::NotEnabled);
152                }
153                MlsCredentialType::X509 => {
154                    let alice_state = alice_central
155                        .transaction
156                        .conversation(&id)
157                        .await
158                        .unwrap()
159                        .e2ei_conversation_state()
160                        .await
161                        .unwrap();
162                    let bob_state = bob_central
163                        .transaction
164                        .conversation(&id)
165                        .await
166                        .unwrap()
167                        .e2ei_conversation_state()
168                        .await
169                        .unwrap();
170                    assert_eq!(alice_state, E2eiConversationState::Verified);
171                    assert_eq!(bob_state, E2eiConversationState::Verified);
172
173                    let gi = alice_central.get_group_info(&id).await;
174                    let state = alice_central
175                        .transaction
176                        .get_credential_in_use(gi, MlsCredentialType::X509)
177                        .await
178                        .unwrap();
179                    assert_eq!(state, E2eiConversationState::Verified);
180                }
181            }
182        })
183        .await
184    }
185
186    // testing the case where Bob & Alice have different Credential type
187    #[apply(all_cred_cipher)]
188    #[wasm_bindgen_test]
189    async fn heterogeneous_conversation_should_be_not_verified(case: TestContext) {
190        use crate::e2e_identity::enrollment::test_utils::failsafe_ctx;
191
192        let [mut alice_central, mut bob_central] = case.sessions().await;
193        Box::pin(async move {
194            let id = conversation_id();
195            let x509_test_chain_arc =
196                failsafe_ctx(&mut [&mut alice_central, &mut bob_central], case.signature_scheme()).await;
197
198            let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
199
200            // That way the conversation creator (Alice) will have a different credential type than Bob
201            let alice_client = alice_central.transaction.session().await.unwrap();
202            let alice_provider = alice_central.transaction.mls_provider().await.unwrap();
203            let creator_ct = match case.credential_type {
204                MlsCredentialType::Basic => {
205                    let intermediate_ca = x509_test_chain.find_local_intermediate_ca();
206                    let cert_bundle = CertificateBundle::rand(&alice_client.id().await.unwrap(), intermediate_ca);
207                    alice_client
208                        .init_x509_credential_bundle_if_missing(&alice_provider, case.signature_scheme(), cert_bundle)
209                        .await
210                        .unwrap();
211                    MlsCredentialType::X509
212                }
213                MlsCredentialType::X509 => {
214                    alice_client
215                        .init_basic_credential_bundle_if_missing(&alice_provider, case.signature_scheme())
216                        .await
217                        .unwrap();
218                    MlsCredentialType::Basic
219                }
220            };
221
222            alice_central
223                .transaction
224                .new_conversation(&id, creator_ct, case.cfg.clone())
225                .await
226                .unwrap();
227            alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
228
229            // since in that case both have a different credential type the conversation is always not verified
230            let alice_state = alice_central
231                .transaction
232                .conversation(&id)
233                .await
234                .unwrap()
235                .e2ei_conversation_state()
236                .await
237                .unwrap();
238            let bob_state = bob_central
239                .transaction
240                .conversation(&id)
241                .await
242                .unwrap()
243                .e2ei_conversation_state()
244                .await
245                .unwrap();
246            assert_eq!(alice_state, E2eiConversationState::NotVerified);
247            assert_eq!(bob_state, E2eiConversationState::NotVerified);
248
249            let gi = alice_central.get_group_info(&id).await;
250            let state = alice_central
251                .transaction
252                .get_credential_in_use(gi, MlsCredentialType::X509)
253                .await
254                .unwrap();
255            assert_eq!(state, E2eiConversationState::NotVerified);
256        })
257        .await
258    }
259
260    #[apply(all_cred_cipher)]
261    #[wasm_bindgen_test]
262    async fn should_be_not_verified_when_one_expired(case: TestContext) {
263        if !case.is_x509() {
264            return;
265        }
266
267        let [alice_central, bob_central] = case.sessions().await;
268        Box::pin(async move {
269            let id = conversation_id();
270
271            alice_central
272                .transaction
273                .new_conversation(&id, case.credential_type, case.cfg.clone())
274                .await
275                .unwrap();
276            alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
277
278            let expiration_time = core::time::Duration::from_secs(14);
279            let start = web_time::Instant::now();
280
281            let intermediate_ca = alice_central
282                .x509_test_chain
283                .as_ref()
284                .as_ref()
285                .expect("No x509 test chain")
286                .find_local_intermediate_ca();
287            let cert = CertificateBundle::new_with_default_values(intermediate_ca, Some(expiration_time));
288            let cb = Session::new_x509_credential_bundle(cert.clone()).unwrap();
289            alice_central
290                .transaction
291                .conversation(&id)
292                .await
293                .unwrap()
294                .e2ei_rotate(Some(&cb))
295                .await
296                .unwrap();
297            let commit = alice_central.mls_transport().await.latest_commit().await;
298            bob_central
299                .transaction
300                .conversation(&id)
301                .await
302                .unwrap()
303                .decrypt_message(commit.to_bytes().unwrap())
304                .await
305                .unwrap();
306
307            let alice_client = alice_central.transaction.session().await.unwrap();
308            let alice_provider = alice_central.transaction.mls_provider().await.unwrap();
309            // Needed because 'e2ei_rotate' does not do it directly and it's required for 'get_group_info'
310            alice_client
311                .save_new_x509_credential_bundle(&alice_provider.keystore(), case.signature_scheme(), cert)
312                .await
313                .unwrap();
314
315            // Need to fetch it before it becomes invalid & expires
316            let gi = alice_central.get_group_info(&id).await;
317
318            let elapsed = start.elapsed();
319            // Give time to the certificate to expire
320            if expiration_time > elapsed {
321                async_std::task::sleep(expiration_time - elapsed + core::time::Duration::from_secs(1)).await;
322            }
323
324            let alice_state = alice_central
325                .transaction
326                .conversation(&id)
327                .await
328                .unwrap()
329                .e2ei_conversation_state()
330                .await
331                .unwrap();
332            let bob_state = bob_central
333                .transaction
334                .conversation(&id)
335                .await
336                .unwrap()
337                .e2ei_conversation_state()
338                .await
339                .unwrap();
340            assert_eq!(alice_state, E2eiConversationState::NotVerified);
341            assert_eq!(bob_state, E2eiConversationState::NotVerified);
342
343            let state = alice_central
344                .transaction
345                .get_credential_in_use(gi, MlsCredentialType::X509)
346                .await
347                .unwrap();
348            assert_eq!(state, E2eiConversationState::NotVerified);
349        })
350        .await
351    }
352
353    #[apply(all_cred_cipher)]
354    #[wasm_bindgen_test]
355    async fn should_be_not_verified_when_all_expired(case: TestContext) {
356        if !case.is_x509() {
357            return;
358        }
359        let alice_user_id = uuid::Uuid::new_v4();
360        let [client_id] = case.client_ids_for_user(&alice_user_id);
361        let [alice_central] = case.sessions_x509_with_client_ids([client_id]).await;
362        Box::pin(async move {
363            let id = conversation_id();
364
365            alice_central
366                .transaction
367                .new_conversation(&id, case.credential_type, case.cfg.clone())
368                .await
369                .unwrap();
370
371            let expiration_time = core::time::Duration::from_secs(14);
372            let start = web_time::Instant::now();
373            let alice_test_chain = alice_central.x509_test_chain.as_ref().as_ref().unwrap();
374
375            let alice_intermediate_ca = alice_test_chain.find_local_intermediate_ca();
376            let mut alice_cert = alice_test_chain
377                .actors
378                .iter()
379                .find(|actor| actor.name == alice_user_id.to_string())
380                .unwrap()
381                .clone();
382            alice_intermediate_ca.update_end_identity(&mut alice_cert.certificate, Some(expiration_time));
383
384            let cert_bundle =
385                CertificateBundle::from_certificate_and_issuer(&alice_cert.certificate, alice_intermediate_ca);
386            let cb = Session::new_x509_credential_bundle(cert_bundle.clone()).unwrap();
387            alice_central
388                .transaction
389                .conversation(&id)
390                .await
391                .unwrap()
392                .e2ei_rotate(Some(&cb))
393                .await
394                .unwrap();
395
396            let alice_client = alice_central.session().await;
397            let alice_provider = alice_central.transaction.mls_provider().await.unwrap();
398
399            // Needed because 'e2ei_rotate' does not do it directly and it's required for 'get_group_info'
400            alice_client
401                .save_new_x509_credential_bundle(&alice_provider.keystore(), case.signature_scheme(), cert_bundle)
402                .await
403                .unwrap();
404
405            let elapsed = start.elapsed();
406            // Give time to the certificate to expire
407            if expiration_time > elapsed {
408                async_std::task::sleep(expiration_time - elapsed + core::time::Duration::from_secs(1)).await;
409            }
410
411            let alice_state = alice_central
412                .transaction
413                .conversation(&id)
414                .await
415                .unwrap()
416                .e2ei_conversation_state()
417                .await
418                .unwrap();
419            assert_eq!(alice_state, E2eiConversationState::NotVerified);
420
421            // Need to fetch it before it becomes invalid & expires
422            let gi = alice_central.get_group_info(&id).await;
423
424            let state = alice_central
425                .transaction
426                .get_credential_in_use(gi, MlsCredentialType::X509)
427                .await
428                .unwrap();
429            assert_eq!(state, E2eiConversationState::NotVerified);
430        })
431        .await
432    }
433}