core_crypto/transaction_context/e2e_identity/
conversation_state.rs1use 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#[derive(Debug, Clone, Copy, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
18#[repr(u8)]
19pub enum E2eiConversationState {
20 Verified = 1,
22 NotVerified,
24 NotEnabled,
26}
27
28impl TransactionContext {
29 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; 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 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 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 #[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 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 #[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 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 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 alice_client
311 .save_new_x509_credential_bundle(&alice_provider.keystore(), case.signature_scheme(), cert)
312 .await
313 .unwrap();
314
315 let gi = alice_central.get_group_info(&id).await;
317
318 let elapsed = start.elapsed();
319 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 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 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 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}