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