core_crypto/e2e_identity/
conversation_state.rs1use crate::{
2 MlsError, RecursiveError,
3 mls::credential::ext::CredentialExt,
4 prelude::{MlsCentral, MlsCredentialType},
5};
6
7use openmls_traits::OpenMlsCryptoProvider;
8use wire_e2e_identity::prelude::WireIdentityReader;
9
10use crate::context::CentralContext;
11use crate::prelude::MlsCiphersuite;
12use openmls::{
13 messages::group_info::VerifiableGroupInfo,
14 prelude::{Credential, Node},
15 treesync::RatchetTree,
16};
17
18use super::Result;
19
20#[derive(Debug, Clone, Copy, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
25#[repr(u8)]
26pub enum E2eiConversationState {
27 Verified = 1,
29 NotVerified,
31 NotEnabled,
33}
34
35impl CentralContext {
36 pub async fn e2ei_verify_group_state(&self, group_info: VerifiableGroupInfo) -> Result<E2eiConversationState> {
38 let mls_provider = self
39 .mls_provider()
40 .await
41 .map_err(RecursiveError::root("getting mls provider"))?;
42 let auth_service = mls_provider.authentication_service();
43 auth_service.refresh_time_of_interest().await;
44 let cs = group_info.ciphersuite().into();
45
46 let is_sender = true; let Ok(rt) = group_info
48 .take_ratchet_tree(
49 &self
50 .mls_provider()
51 .await
52 .map_err(RecursiveError::root("getting mls provider"))?,
53 is_sender,
54 )
55 .await
56 else {
57 return Ok(E2eiConversationState::NotVerified);
58 };
59
60 let credentials = rt.iter().filter_map(|n| match n {
61 Some(Node::LeafNode(ln)) => Some(ln.credential()),
62 _ => None,
63 });
64
65 let auth_service = auth_service.borrow().await;
66 Ok(compute_state(cs, credentials, MlsCredentialType::X509, auth_service.as_ref()).await)
67 }
68
69 pub async fn get_credential_in_use(
71 &self,
72 group_info: VerifiableGroupInfo,
73 credential_type: MlsCredentialType,
74 ) -> Result<E2eiConversationState> {
75 let cs = group_info.ciphersuite().into();
76 let rt = group_info
81 .take_ratchet_tree(
82 &self
83 .mls_provider()
84 .await
85 .map_err(RecursiveError::root("getting mls provider"))?,
86 false,
87 )
88 .await
89 .map_err(MlsError::wrap("taking ratchet tree"))?;
90 let mls_provider = self
91 .mls_provider()
92 .await
93 .map_err(RecursiveError::root("getting mls provider"))?;
94 let auth_service = mls_provider.authentication_service().borrow().await;
95 get_credential_in_use_in_ratchet_tree(cs, rt, credential_type, auth_service.as_ref()).await
96 }
97}
98
99impl MlsCentral {
100 pub async fn e2ei_verify_group_state(&self, group_info: VerifiableGroupInfo) -> Result<E2eiConversationState> {
102 self.mls_backend
103 .authentication_service()
104 .refresh_time_of_interest()
105 .await;
106
107 let cs = group_info.ciphersuite().into();
108
109 let is_sender = true; let Ok(rt) = group_info.take_ratchet_tree(&self.mls_backend, is_sender).await else {
111 return Ok(E2eiConversationState::NotVerified);
112 };
113
114 let credentials = rt.iter().filter_map(|n| match n {
115 Some(Node::LeafNode(ln)) => Some(ln.credential()),
116 _ => None,
117 });
118
119 Ok(compute_state(
120 cs,
121 credentials,
122 MlsCredentialType::X509,
123 self.mls_backend.authentication_service().borrow().await.as_ref(),
124 )
125 .await)
126 }
127
128 pub async fn get_credential_in_use(
131 &self,
132 group_info: VerifiableGroupInfo,
133 credential_type: MlsCredentialType,
134 ) -> Result<E2eiConversationState> {
135 let cs = group_info.ciphersuite().into();
136 let rt = group_info
141 .take_ratchet_tree(&self.mls_backend, false)
142 .await
143 .map_err(MlsError::wrap("taking ratchet tree"))?;
144 get_credential_in_use_in_ratchet_tree(
145 cs,
146 rt,
147 credential_type,
148 self.mls_backend.authentication_service().borrow().await.as_ref(),
149 )
150 .await
151 }
152}
153
154async fn get_credential_in_use_in_ratchet_tree(
155 ciphersuite: MlsCiphersuite,
156 ratchet_tree: RatchetTree,
157 credential_type: MlsCredentialType,
158 env: Option<&wire_e2e_identity::prelude::x509::revocation::PkiEnvironment>,
159) -> Result<E2eiConversationState> {
160 let credentials = ratchet_tree.iter().filter_map(|n| match n {
161 Some(Node::LeafNode(ln)) => Some(ln.credential()),
162 _ => None,
163 });
164 Ok(compute_state(ciphersuite, credentials, credential_type, env).await)
165}
166
167pub(crate) async fn compute_state<'a>(
170 ciphersuite: MlsCiphersuite,
171 credentials: impl Iterator<Item = &'a Credential>,
172 _credential_type: MlsCredentialType,
173 env: Option<&wire_e2e_identity::prelude::x509::revocation::PkiEnvironment>,
174) -> E2eiConversationState {
175 let mut is_e2ei = false;
176 let mut state = E2eiConversationState::Verified;
177
178 for credential in credentials {
179 let Ok(Some(cert)) = credential.parse_leaf_cert() else {
180 state = E2eiConversationState::NotVerified;
181 if is_e2ei {
182 break;
183 }
184 continue;
185 };
186
187 is_e2ei = true;
188
189 let invalid_identity = cert.extract_identity(env, ciphersuite.e2ei_hash_alg()).is_err();
190
191 use openmls_x509_credential::X509Ext as _;
192 let is_time_valid = cert.is_time_valid().unwrap_or(false);
193 let is_time_invalid = !is_time_valid;
194 let is_revoked_or_invalid = env
195 .map(|e| e.validate_cert_and_revocation(&cert).is_err())
196 .unwrap_or(false);
197
198 let is_invalid = invalid_identity || is_time_invalid || is_revoked_or_invalid;
199 if is_invalid {
200 state = E2eiConversationState::NotVerified;
201 break;
202 }
203 }
204
205 if is_e2ei {
206 state
207 } else {
208 E2eiConversationState::NotEnabled
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use crate::e2e_identity::rotate::tests::all::failsafe_ctx;
215 use wasm_bindgen_test::*;
216
217 use super::*;
218 use crate::mls::conversation::Conversation as _;
219 use crate::{
220 prelude::{CertificateBundle, Client, MlsCredentialType},
221 test_utils::*,
222 };
223
224 wasm_bindgen_test_configure!(run_in_browser);
225
226 #[apply(all_cred_cipher)]
228 #[wasm_bindgen_test]
229 async fn uniform_conversation_should_be_not_verified_when_basic(case: TestCase) {
230 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
231 Box::pin(async move {
232 let id = conversation_id();
233
234 let creator_ct = case.credential_type;
236 alice_central
237 .context
238 .new_conversation(&id, creator_ct, case.cfg.clone())
239 .await
240 .unwrap();
241 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
242
243 match case.credential_type {
244 MlsCredentialType::Basic => {
245 let alice_state = alice_central
246 .context
247 .conversation(&id)
248 .await
249 .unwrap()
250 .e2ei_conversation_state()
251 .await
252 .unwrap();
253 let bob_state = bob_central
254 .context
255 .conversation(&id)
256 .await
257 .unwrap()
258 .e2ei_conversation_state()
259 .await
260 .unwrap();
261 assert_eq!(alice_state, E2eiConversationState::NotEnabled);
262 assert_eq!(bob_state, E2eiConversationState::NotEnabled);
263
264 let gi = alice_central.get_group_info(&id).await;
265 let state = alice_central
266 .context
267 .get_credential_in_use(gi, MlsCredentialType::X509)
268 .await
269 .unwrap();
270 assert_eq!(state, E2eiConversationState::NotEnabled);
271 }
272 MlsCredentialType::X509 => {
273 let alice_state = alice_central
274 .context
275 .conversation(&id)
276 .await
277 .unwrap()
278 .e2ei_conversation_state()
279 .await
280 .unwrap();
281 let bob_state = bob_central
282 .context
283 .conversation(&id)
284 .await
285 .unwrap()
286 .e2ei_conversation_state()
287 .await
288 .unwrap();
289 assert_eq!(alice_state, E2eiConversationState::Verified);
290 assert_eq!(bob_state, E2eiConversationState::Verified);
291
292 let gi = alice_central.get_group_info(&id).await;
293 let state = alice_central
294 .context
295 .get_credential_in_use(gi, MlsCredentialType::X509)
296 .await
297 .unwrap();
298 assert_eq!(state, E2eiConversationState::Verified);
299 }
300 }
301 })
302 })
303 .await
304 }
305
306 #[apply(all_cred_cipher)]
308 #[wasm_bindgen_test]
309 async fn heterogeneous_conversation_should_be_not_verified(case: TestCase) {
310 run_test_with_client_ids(
311 case.clone(),
312 ["alice", "bob"],
313 move |[mut alice_central, mut bob_central]| {
314 Box::pin(async move {
315 let id = conversation_id();
316 let x509_test_chain_arc =
317 failsafe_ctx(&mut [&mut alice_central, &mut bob_central], case.signature_scheme()).await;
318
319 let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
320
321 let alice_client = alice_central.context.mls_client().await.unwrap();
323 let alice_provider = alice_central.context.mls_provider().await.unwrap();
324 let creator_ct = match case.credential_type {
325 MlsCredentialType::Basic => {
326 let intermediate_ca = x509_test_chain.find_local_intermediate_ca();
327 let cert_bundle =
328 CertificateBundle::rand(&alice_client.id().await.unwrap(), intermediate_ca);
329 alice_client
330 .init_x509_credential_bundle_if_missing(
331 &alice_provider,
332 case.signature_scheme(),
333 cert_bundle,
334 )
335 .await
336 .unwrap();
337 MlsCredentialType::X509
338 }
339 MlsCredentialType::X509 => {
340 alice_client
341 .init_basic_credential_bundle_if_missing(&alice_provider, case.signature_scheme())
342 .await
343 .unwrap();
344 MlsCredentialType::Basic
345 }
346 };
347
348 alice_central
349 .context
350 .new_conversation(&id, creator_ct, case.cfg.clone())
351 .await
352 .unwrap();
353 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
354
355 let alice_state = alice_central
357 .context
358 .conversation(&id)
359 .await
360 .unwrap()
361 .e2ei_conversation_state()
362 .await
363 .unwrap();
364 let bob_state = bob_central
365 .context
366 .conversation(&id)
367 .await
368 .unwrap()
369 .e2ei_conversation_state()
370 .await
371 .unwrap();
372 assert_eq!(alice_state, E2eiConversationState::NotVerified);
373 assert_eq!(bob_state, E2eiConversationState::NotVerified);
374
375 let gi = alice_central.get_group_info(&id).await;
376 let state = alice_central
377 .context
378 .get_credential_in_use(gi, MlsCredentialType::X509)
379 .await
380 .unwrap();
381 assert_eq!(state, E2eiConversationState::NotVerified);
382 })
383 },
384 )
385 .await
386 }
387
388 #[apply(all_cred_cipher)]
389 #[wasm_bindgen_test]
390 async fn should_be_not_verified_when_one_expired(case: TestCase) {
391 if !case.is_x509() {
392 return;
393 }
394 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
395 Box::pin(async move {
396 let id = conversation_id();
397
398 alice_central
399 .context
400 .new_conversation(&id, case.credential_type, case.cfg.clone())
401 .await
402 .unwrap();
403 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
404
405 let expiration_time = core::time::Duration::from_secs(14);
406 let start = web_time::Instant::now();
407
408 let intermediate_ca = alice_central
409 .x509_test_chain
410 .as_ref()
411 .as_ref()
412 .expect("No x509 test chain")
413 .find_local_intermediate_ca();
414 let cert = CertificateBundle::new_with_default_values(intermediate_ca, Some(expiration_time));
415 let cb = Client::new_x509_credential_bundle(cert.clone()).unwrap();
416 alice_central
417 .context
418 .conversation(&id)
419 .await
420 .unwrap()
421 .e2ei_rotate(Some(&cb))
422 .await
423 .unwrap();
424 let commit = alice_central.mls_transport.latest_commit().await;
425 bob_central
426 .context
427 .conversation(&id)
428 .await
429 .unwrap()
430 .decrypt_message(commit.to_bytes().unwrap())
431 .await
432 .unwrap();
433
434 let alice_client = alice_central.context.mls_client().await.unwrap();
435 let alice_provider = alice_central.context.mls_provider().await.unwrap();
436 alice_client
438 .save_new_x509_credential_bundle(&alice_provider.keystore(), case.signature_scheme(), cert)
439 .await
440 .unwrap();
441
442 let gi = alice_central.get_group_info(&id).await;
444
445 let elapsed = start.elapsed();
446 if expiration_time > elapsed {
448 async_std::task::sleep(expiration_time - elapsed + core::time::Duration::from_secs(1)).await;
449 }
450
451 let alice_state = alice_central
452 .context
453 .conversation(&id)
454 .await
455 .unwrap()
456 .e2ei_conversation_state()
457 .await
458 .unwrap();
459 let bob_state = bob_central
460 .context
461 .conversation(&id)
462 .await
463 .unwrap()
464 .e2ei_conversation_state()
465 .await
466 .unwrap();
467 assert_eq!(alice_state, E2eiConversationState::NotVerified);
468 assert_eq!(bob_state, E2eiConversationState::NotVerified);
469
470 let state = alice_central
471 .context
472 .get_credential_in_use(gi, MlsCredentialType::X509)
473 .await
474 .unwrap();
475 assert_eq!(state, E2eiConversationState::NotVerified);
476 })
477 })
478 .await
479 }
480
481 #[apply(all_cred_cipher)]
482 #[wasm_bindgen_test]
483 async fn should_be_not_verified_when_all_expired(case: TestCase) {
484 if case.is_x509() {
485 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
486 Box::pin(async move {
487 let id = conversation_id();
488
489 alice_central
490 .context
491 .new_conversation(&id, case.credential_type, case.cfg.clone())
492 .await
493 .unwrap();
494
495 let expiration_time = core::time::Duration::from_secs(14);
496 let start = web_time::Instant::now();
497 let alice_test_chain = alice_central.x509_test_chain.as_ref().as_ref().unwrap();
498
499 let alice_intermediate_ca = alice_test_chain.find_local_intermediate_ca();
500 let mut alice_cert = alice_test_chain
501 .actors
502 .iter()
503 .find(|actor| actor.name == "alice")
504 .unwrap()
505 .clone();
506 alice_intermediate_ca.update_end_identity(&mut alice_cert.certificate, Some(expiration_time));
507
508 let cert_bundle =
509 CertificateBundle::from_certificate_and_issuer(&alice_cert.certificate, alice_intermediate_ca);
510 let cb = Client::new_x509_credential_bundle(cert_bundle.clone()).unwrap();
511 alice_central
512 .context
513 .conversation(&id)
514 .await
515 .unwrap()
516 .e2ei_rotate(Some(&cb))
517 .await
518 .unwrap();
519
520 let alice_client = alice_central.client().await;
521 let alice_provider = alice_central.context.mls_provider().await.unwrap();
522
523 alice_client
525 .save_new_x509_credential_bundle(
526 &alice_provider.keystore(),
527 case.signature_scheme(),
528 cert_bundle,
529 )
530 .await
531 .unwrap();
532
533 let elapsed = start.elapsed();
534 if expiration_time > elapsed {
536 async_std::task::sleep(expiration_time - elapsed + core::time::Duration::from_secs(1)).await;
537 }
538
539 let alice_state = alice_central
540 .context
541 .conversation(&id)
542 .await
543 .unwrap()
544 .e2ei_conversation_state()
545 .await
546 .unwrap();
547 assert_eq!(alice_state, E2eiConversationState::NotVerified);
548
549 let gi = alice_central.get_group_info(&id).await;
551
552 let state = alice_central
553 .context
554 .get_credential_in_use(gi, MlsCredentialType::X509)
555 .await
556 .unwrap();
557 assert_eq!(state, E2eiConversationState::NotVerified);
558 })
559 })
560 .await
561 }
562 }
563}