core_crypto/e2e_identity/
identity.rs

1use std::collections::HashMap;
2use std::str::FromStr;
3
4use itertools::Itertools;
5use openmls_traits::OpenMlsCryptoProvider;
6use x509_cert::der::pem::LineEnding;
7
8use crate::context::CentralContext;
9use crate::e2e_identity::id::WireQualifiedClientId;
10use crate::mls::credential::ext::CredentialExt;
11use crate::prelude::MlsCredentialType;
12use crate::{
13    e2e_identity::device_status::DeviceStatus,
14    prelude::{user_id::UserId, ClientId, ConversationId, CryptoError, CryptoResult, MlsCentral, MlsConversation},
15};
16
17/// Represents the identity claims identifying a client
18/// Those claims are verifiable by any member in the group
19#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
20pub struct WireIdentity {
21    /// Unique client identifier e.g. `T4Coy4vdRzianwfOgXpn6A:6add501bacd1d90e@whitehouse.gov`
22    pub client_id: String,
23    /// MLS thumbprint
24    pub thumbprint: String,
25    /// Status of the Credential at the moment T when this object is created
26    pub status: DeviceStatus,
27    /// Indicates whether the credential is Basic or X509
28    pub credential_type: MlsCredentialType,
29    /// In case 'credential_type' is [MlsCredentialType::X509] this is populated
30    pub x509_identity: Option<X509Identity>,
31}
32
33/// Represents the parts of [WireIdentity] that are specific to a X509 certificate (and not a Basic one).
34///
35/// We don't use an enum here since the sole purpose of this is to be exposed through the FFI (and
36/// union types are impossible to carry over the FFI boundary)
37#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
38pub struct X509Identity {
39    /// user handle e.g. `john_wire`
40    pub handle: String,
41    /// Name as displayed in the messaging application e.g. `John Fitzgerald Kennedy`
42    pub display_name: String,
43    /// DNS domain for which this identity proof was generated e.g. `whitehouse.gov`
44    pub domain: String,
45    /// X509 certificate identifying this client in the MLS group ; PEM encoded
46    pub certificate: String,
47    /// X509 certificate serial number
48    pub serial_number: String,
49    /// X509 certificate not before as Unix timestamp
50    pub not_before: u64,
51    /// X509 certificate not after as Unix timestamp
52    pub not_after: u64,
53}
54
55impl<'a> TryFrom<(wire_e2e_identity::prelude::WireIdentity, &'a [u8])> for WireIdentity {
56    type Error = CryptoError;
57
58    fn try_from((i, cert): (wire_e2e_identity::prelude::WireIdentity, &'a [u8])) -> CryptoResult<Self> {
59        use x509_cert::der::Decode as _;
60        let document = x509_cert::der::Document::from_der(cert)?;
61        let certificate = document.to_pem("CERTIFICATE", LineEnding::LF)?;
62
63        let client_id = WireQualifiedClientId::from_str(&i.client_id)?;
64
65        Ok(Self {
66            client_id: client_id.try_into()?,
67            status: i.status.into(),
68            thumbprint: i.thumbprint,
69            credential_type: MlsCredentialType::X509,
70            x509_identity: Some(X509Identity {
71                handle: i.handle.to_string(),
72                display_name: i.display_name,
73                domain: i.domain,
74                certificate,
75                serial_number: i.serial_number,
76                not_before: i.not_before,
77                not_after: i.not_after,
78            }),
79        })
80    }
81}
82
83impl CentralContext {
84    /// See [MlsCentral::get_device_identities].
85    pub async fn get_device_identities(
86        &self,
87        conversation_id: &ConversationId,
88        client_ids: &[ClientId],
89    ) -> CryptoResult<Vec<WireIdentity>> {
90        let mls_provider = self.mls_provider().await?;
91        let auth_service = mls_provider.authentication_service();
92        auth_service.refresh_time_of_interest().await;
93        let auth_service = auth_service.borrow().await;
94        let conversation = self.get_conversation(conversation_id).await?;
95        let conversation_guard = conversation.read().await;
96        conversation_guard.get_device_identities(client_ids, auth_service.as_ref())
97    }
98
99    /// See [MlsCentral::get_user_identities].
100    pub async fn get_user_identities(
101        &self,
102        conversation_id: &ConversationId,
103        user_ids: &[String],
104    ) -> CryptoResult<HashMap<String, Vec<WireIdentity>>> {
105        let mls_provider = self.mls_provider().await?;
106        let auth_service = mls_provider.authentication_service();
107        auth_service.refresh_time_of_interest().await;
108        let auth_service = auth_service.borrow().await;
109        let conversation = self.get_conversation(conversation_id).await?;
110        let conversation_guard = conversation.read().await;
111        conversation_guard.get_user_identities(user_ids, auth_service.as_ref())
112    }
113}
114
115impl MlsCentral {
116    /// From a given conversation, get the identity of the members supplied. Identity is only present for
117    /// members with a Certificate Credential (after turning on end-to-end identity).
118    /// If no member has a x509 certificate, it will return an empty Vec
119    pub async fn get_device_identities(
120        &self,
121        conversation_id: &ConversationId,
122        client_ids: &[ClientId],
123    ) -> CryptoResult<Vec<WireIdentity>> {
124        self.mls_backend
125            .authentication_service()
126            .refresh_time_of_interest()
127            .await;
128        let conversation = self.get_conversation(conversation_id).await?;
129        let Some(conversation) = conversation else {
130            return Err(CryptoError::ConversationNotFound(conversation_id.clone()));
131        };
132        conversation.get_device_identities(
133            client_ids,
134            self.mls_backend.authentication_service().borrow().await.as_ref(),
135        )
136    }
137
138    /// From a given conversation, get the identity of the users (device holders) supplied.
139    /// Identity is only present for devices with a Certificate Credential (after turning on end-to-end identity).
140    /// If no member has a x509 certificate, it will return an empty Vec.
141    ///
142    /// Returns a Map with all the identities for a given users. Consumers are then recommended to
143    /// reduce those identities to determine the actual status of a user.
144    pub async fn get_user_identities(
145        &self,
146        conversation_id: &ConversationId,
147        user_ids: &[String],
148    ) -> CryptoResult<HashMap<String, Vec<WireIdentity>>> {
149        self.mls_backend
150            .authentication_service()
151            .refresh_time_of_interest()
152            .await;
153        let Some(conversation) = self.get_conversation(conversation_id).await? else {
154            return Err(CryptoError::ConversationNotFound(conversation_id.clone()));
155        };
156        conversation.get_user_identities(
157            user_ids,
158            self.mls_backend.authentication_service().borrow().await.as_ref(),
159        )
160    }
161}
162
163impl MlsConversation {
164    fn get_device_identities(
165        &self,
166        device_ids: &[ClientId],
167        env: Option<&wire_e2e_identity::prelude::x509::revocation::PkiEnvironment>,
168    ) -> CryptoResult<Vec<WireIdentity>> {
169        if device_ids.is_empty() {
170            return Err(CryptoError::ConsumerError);
171        }
172        self.members_with_key()
173            .into_iter()
174            .filter(|(id, _)| device_ids.contains(&ClientId::from(id.as_slice())))
175            .map(|(_, c)| c.extract_identity(self.ciphersuite(), env))
176            .collect::<CryptoResult<Vec<_>>>()
177    }
178
179    fn get_user_identities(
180        &self,
181        user_ids: &[String],
182        env: Option<&wire_e2e_identity::prelude::x509::revocation::PkiEnvironment>,
183    ) -> CryptoResult<HashMap<String, Vec<WireIdentity>>> {
184        if user_ids.is_empty() {
185            return Err(CryptoError::ConsumerError);
186        }
187        let user_ids = user_ids.iter().map(|uid| uid.as_bytes()).collect::<Vec<_>>();
188
189        self.members_with_key()
190            .iter()
191            .filter_map(|(id, c)| UserId::try_from(id.as_slice()).ok().zip(Some(c)))
192            .filter(|(uid, _)| user_ids.contains(uid))
193            .map(|(uid, c)| (uid, c.extract_identity(self.ciphersuite(), env)))
194            .map(|(uid, identity)| {
195                let uid = String::try_from(uid);
196                // could be simplified if `Result::zip` was available
197                uid.and_then(|uid| identity.map(|id| (uid, id)))
198            })
199            .process_results(|iter| iter.into_group_map())
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use wasm_bindgen_test::*;
206
207    use crate::context::CentralContext;
208    use crate::prelude::{ClientId, ConversationId, MlsCredentialType};
209    use crate::{
210        prelude::{DeviceStatus, E2eiConversationState},
211        test_utils::*,
212        CryptoError,
213    };
214
215    wasm_bindgen_test_configure!(run_in_browser);
216
217    async fn all_identities_check<const N: usize>(
218        central: &CentralContext,
219        id: &ConversationId,
220        user_ids: &[String; N],
221        expected_sizes: [usize; N],
222    ) {
223        let all_identities = central.get_user_identities(id, user_ids).await.unwrap();
224        assert_eq!(all_identities.len(), N);
225        for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
226            let alice_identities = all_identities.get(user_id).unwrap();
227            assert_eq!(alice_identities.len(), expected_size);
228        }
229        // Not found
230        let not_found = central
231            .get_user_identities(id, &["aaaaaaaaaaaaa".to_string()])
232            .await
233            .unwrap();
234        assert!(not_found.is_empty());
235
236        // Invalid usage
237        let invalid = central.get_user_identities(id, &[]).await;
238        assert!(matches!(invalid.unwrap_err(), CryptoError::ConsumerError));
239    }
240
241    async fn check_identities_device_status<const N: usize>(
242        central: &CentralContext,
243        id: &ConversationId,
244        client_ids: &[ClientId; N],
245        name_status: &[(&'static str, DeviceStatus); N],
246    ) {
247        let mut identities = central.get_device_identities(id, client_ids).await.unwrap();
248
249        for j in 0..N {
250            let client_identity = identities.remove(
251                identities
252                    .iter()
253                    .position(|i| i.x509_identity.as_ref().unwrap().display_name == name_status[j].0)
254                    .unwrap(),
255            );
256            assert_eq!(client_identity.status, name_status[j].1);
257        }
258        assert!(identities.is_empty());
259
260        assert_eq!(
261            central.e2ei_conversation_state(id).await.unwrap(),
262            E2eiConversationState::NotVerified
263        );
264    }
265
266    #[async_std::test]
267    #[wasm_bindgen_test]
268    async fn should_read_device_identities() {
269        let case = TestCase::default_x509();
270        run_test_with_client_ids(
271            case.clone(),
272            ["alice_android", "alice_ios"],
273            move |[alice_android_central, alice_ios_central]| {
274                Box::pin(async move {
275                    let id = conversation_id();
276                    alice_android_central
277                        .context
278                        .new_conversation(&id, case.credential_type, case.cfg.clone())
279                        .await
280                        .unwrap();
281                    alice_android_central
282                        .invite_all(&case, &id, [&alice_ios_central])
283                        .await
284                        .unwrap();
285
286                    let (android_id, ios_id) = (
287                        alice_android_central.get_client_id().await,
288                        alice_ios_central.get_client_id().await,
289                    );
290
291                    let mut android_ids = alice_android_central
292                        .context
293                        .get_device_identities(&id, &[android_id.clone(), ios_id.clone()])
294                        .await
295                        .unwrap();
296                    android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
297                    assert_eq!(android_ids.len(), 2);
298                    let mut ios_ids = alice_ios_central
299                        .context
300                        .get_device_identities(&id, &[android_id.clone(), ios_id.clone()])
301                        .await
302                        .unwrap();
303                    ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
304                    assert_eq!(ios_ids.len(), 2);
305
306                    assert_eq!(android_ids, ios_ids);
307
308                    let android_identities = alice_android_central
309                        .context
310                        .get_device_identities(&id, &[android_id])
311                        .await
312                        .unwrap();
313                    let android_id = android_identities.first().unwrap();
314                    assert_eq!(
315                        android_id.client_id.as_bytes(),
316                        alice_android_central.context.client_id().await.unwrap().0.as_slice()
317                    );
318
319                    let ios_identities = alice_android_central
320                        .context
321                        .get_device_identities(&id, &[ios_id])
322                        .await
323                        .unwrap();
324                    let ios_id = ios_identities.first().unwrap();
325                    assert_eq!(
326                        ios_id.client_id.as_bytes(),
327                        alice_ios_central.context.client_id().await.unwrap().0.as_slice()
328                    );
329
330                    let invalid = alice_android_central.context.get_device_identities(&id, &[]).await;
331                    assert!(matches!(invalid.unwrap_err(), CryptoError::ConsumerError));
332                })
333            },
334        )
335        .await
336    }
337
338    #[async_std::test]
339    #[wasm_bindgen_test]
340    async fn should_read_revoked_device_cross_signed() {
341        let case = TestCase::default_x509();
342        run_test_with_client_ids_and_revocation(
343            case.clone(),
344            ["alice", "bob", "rupert"],
345            ["john", "dilbert"],
346            &["rupert", "dilbert"],
347            move |[mut alice, mut bob, mut rupert], [mut john, mut dilbert]| {
348                Box::pin(async move {
349                    let id = conversation_id();
350                    alice
351                        .context
352                        .new_conversation(&id, case.credential_type, case.cfg.clone())
353                        .await
354                        .unwrap();
355                    alice
356                        .invite_all(&case, &id, [&bob, &rupert, &dilbert, &john])
357                        .await
358                        .unwrap();
359
360                    let (alice_id, bob_id, rupert_id, dilbert_id, john_id) = (
361                        alice.get_client_id().await,
362                        bob.get_client_id().await,
363                        rupert.get_client_id().await,
364                        dilbert.get_client_id().await,
365                        john.get_client_id().await,
366                    );
367
368                    let client_ids = [alice_id, bob_id, rupert_id, dilbert_id, john_id];
369                    let name_status = [
370                        ("alice", DeviceStatus::Valid),
371                        ("bob", DeviceStatus::Valid),
372                        ("rupert", DeviceStatus::Revoked),
373                        ("john", DeviceStatus::Valid),
374                        ("dilbert", DeviceStatus::Revoked),
375                    ];
376                    // Do it a multiple times to avoid WPB-6904 happening again
377                    for _ in 0..2 {
378                        check_identities_device_status(&mut alice.context, &id, &client_ids, &name_status).await;
379                        check_identities_device_status(&mut bob.context, &id, &client_ids, &name_status).await;
380                        check_identities_device_status(&mut rupert.context, &id, &client_ids, &name_status).await;
381                        check_identities_device_status(&mut john.context, &id, &client_ids, &name_status).await;
382                        check_identities_device_status(&mut dilbert.context, &id, &client_ids, &name_status).await;
383                    }
384                })
385            },
386        )
387        .await
388    }
389
390    #[async_std::test]
391    #[wasm_bindgen_test]
392    async fn should_read_revoked_device() {
393        let case = TestCase::default_x509();
394        run_test_with_client_ids_and_revocation(
395            case.clone(),
396            ["alice", "bob", "rupert"],
397            [],
398            &["rupert"],
399            move |[mut alice, mut bob, mut rupert], []| {
400                Box::pin(async move {
401                    let id = conversation_id();
402                    alice
403                        .context
404                        .new_conversation(&id, case.credential_type, case.cfg.clone())
405                        .await
406                        .unwrap();
407                    alice.invite_all(&case, &id, [&bob, &rupert]).await.unwrap();
408
409                    let (alice_id, bob_id, rupert_id) = (
410                        alice.get_client_id().await,
411                        bob.get_client_id().await,
412                        rupert.get_client_id().await,
413                    );
414
415                    let client_ids = [alice_id, bob_id, rupert_id];
416                    let name_status = [
417                        ("alice", DeviceStatus::Valid),
418                        ("bob", DeviceStatus::Valid),
419                        ("rupert", DeviceStatus::Revoked),
420                    ];
421
422                    // Do it a multiple times to avoid WPB-6904 happening again
423                    for _ in 0..2 {
424                        check_identities_device_status(&mut alice.context, &id, &client_ids, &name_status).await;
425                        check_identities_device_status(&mut bob.context, &id, &client_ids, &name_status).await;
426                        check_identities_device_status(&mut rupert.context, &id, &client_ids, &name_status).await;
427                    }
428                })
429            },
430        )
431        .await
432    }
433
434    #[async_std::test]
435    #[wasm_bindgen_test]
436    async fn should_not_fail_when_basic() {
437        let case = TestCase::default();
438        run_test_with_client_ids(
439            case.clone(),
440            ["alice_android", "alice_ios"],
441            move |[alice_android_central, alice_ios_central]| {
442                Box::pin(async move {
443                    let id = conversation_id();
444                    alice_android_central
445                        .context
446                        .new_conversation(&id, case.credential_type, case.cfg.clone())
447                        .await
448                        .unwrap();
449                    alice_android_central
450                        .invite_all(&case, &id, [&alice_ios_central])
451                        .await
452                        .unwrap();
453
454                    let (android_id, ios_id) = (
455                        alice_android_central.get_client_id().await,
456                        alice_ios_central.get_client_id().await,
457                    );
458
459                    let mut android_ids = alice_android_central
460                        .context
461                        .get_device_identities(&id, &[android_id.clone(), ios_id.clone()])
462                        .await
463                        .unwrap();
464                    android_ids.sort();
465
466                    let mut ios_ids = alice_ios_central
467                        .context
468                        .get_device_identities(&id, &[android_id, ios_id])
469                        .await
470                        .unwrap();
471                    ios_ids.sort();
472
473                    assert_eq!(ios_ids.len(), 2);
474                    assert_eq!(ios_ids, android_ids);
475
476                    assert!(ios_ids.iter().all(|i| {
477                        matches!(i.credential_type, MlsCredentialType::Basic)
478                            && matches!(i.status, DeviceStatus::Valid)
479                            && i.x509_identity.is_none()
480                            && !i.thumbprint.is_empty()
481                            && !i.client_id.is_empty()
482                    }));
483                })
484            },
485        )
486        .await
487    }
488
489    // this test is a duplicate of its counterpart but taking federation into account
490    // The heavy lifting of cross-signing the certificates is being done by the test utils.
491    #[async_std::test]
492    #[wasm_bindgen_test]
493    async fn should_read_users_cross_signed() {
494        let case = TestCase::default_x509();
495
496        let (alice_android, alice_ios) = (
497            "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
498            "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
499        );
500        let (alicem_android, alicem_ios) = (
501            "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@world.com",
502            "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@world.com",
503        );
504        let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
505        let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@world.com";
506
507        run_test_with_deterministic_client_ids_and_revocation(
508            case.clone(),
509            [
510                [alice_android, "alice_wire", "Alice Smith"],
511                [alice_ios, "alice_wire", "Alice Smith"],
512                [bob_android, "bob_wire", "Bob Doe"],
513            ],
514            [
515                [alicem_android, "alice_zeta", "Alice Muller"],
516                [alicem_ios, "alice_zeta", "Alice Muller"],
517                [bobt_android, "bob_zeta", "Bob Tables"],
518            ],
519            &[],
520            move |[alice_android_central, alice_ios_central, bob_android_central],
521                  [alicem_android_central, alicem_ios_central, bobt_android_central]| {
522                Box::pin(async move {
523                    let id = conversation_id();
524                    alice_android_central
525                        .context
526                        .new_conversation(&id, case.credential_type, case.cfg.clone())
527                        .await
528                        .unwrap();
529                    alice_android_central
530                        .invite_all(
531                            &case,
532                            &id,
533                            [
534                                &alice_ios_central,
535                                &bob_android_central,
536                                &bobt_android_central,
537                                &alicem_ios_central,
538                                &alicem_android_central,
539                            ],
540                        )
541                        .await
542                        .unwrap();
543
544                    let nb_members = alice_android_central
545                        .get_conversation_unchecked(&id)
546                        .await
547                        .members()
548                        .len();
549                    assert_eq!(nb_members, 6);
550
551                    assert_eq!(
552                        alice_android_central.get_user_id().await,
553                        alice_ios_central.get_user_id().await
554                    );
555
556                    let alicem_user_id = alicem_ios_central.get_user_id().await;
557                    let bobt_user_id = bobt_android_central.get_user_id().await;
558
559                    // Finds both Alice's devices
560                    let alice_user_id = alice_android_central.get_user_id().await;
561                    let alice_identities = alice_android_central
562                        .context
563                        .get_user_identities(&id, &[alice_user_id.clone()])
564                        .await
565                        .unwrap();
566                    assert_eq!(alice_identities.len(), 1);
567                    let identities = alice_identities.get(&alice_user_id).unwrap();
568                    assert_eq!(identities.len(), 2);
569
570                    // Finds Bob only device
571                    let bob_user_id = bob_android_central.get_user_id().await;
572                    let bob_identities = alice_android_central
573                        .context
574                        .get_user_identities(&id, &[bob_user_id.clone()])
575                        .await
576                        .unwrap();
577                    assert_eq!(bob_identities.len(), 1);
578                    let identities = bob_identities.get(&bob_user_id).unwrap();
579                    assert_eq!(identities.len(), 1);
580
581                    // Finds all devices
582                    let user_ids = [alice_user_id, bob_user_id, alicem_user_id, bobt_user_id];
583                    let expected_sizes = [2, 1, 2, 1];
584
585                    all_identities_check(&alice_android_central.context, &id, &user_ids, expected_sizes).await;
586                    all_identities_check(&alicem_android_central.context, &id, &user_ids, expected_sizes).await;
587                    all_identities_check(&alice_ios_central.context, &id, &user_ids, expected_sizes).await;
588                    all_identities_check(&alicem_ios_central.context, &id, &user_ids, expected_sizes).await;
589                    all_identities_check(&bob_android_central.context, &id, &user_ids, expected_sizes).await;
590                    all_identities_check(&bobt_android_central.context, &id, &user_ids, expected_sizes).await;
591                })
592            },
593        )
594        .await
595    }
596
597    #[async_std::test]
598    #[wasm_bindgen_test]
599    async fn should_read_users() {
600        let case = TestCase::default_x509();
601
602        let (alice_android, alice_ios) = (
603            "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
604            "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
605        );
606        let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
607
608        run_test_with_deterministic_client_ids(
609            case.clone(),
610            [
611                [alice_android, "alice_wire", "Alice Smith"],
612                [alice_ios, "alice_wire", "Alice Smith"],
613                [bob_android, "bob_wire", "Bob Doe"],
614            ],
615            move |[mut alice_android_central, mut alice_ios_central, mut bob_android_central]| {
616                Box::pin(async move {
617                    let id = conversation_id();
618                    alice_android_central
619                        .context
620                        .new_conversation(&id, case.credential_type, case.cfg.clone())
621                        .await
622                        .unwrap();
623                    alice_android_central
624                        .invite_all(&case, &id, [&alice_ios_central, &bob_android_central])
625                        .await
626                        .unwrap();
627
628                    let nb_members = alice_android_central
629                        .get_conversation_unchecked(&id)
630                        .await
631                        .members()
632                        .len();
633                    assert_eq!(nb_members, 3);
634
635                    assert_eq!(
636                        alice_android_central.get_user_id().await,
637                        alice_ios_central.get_user_id().await
638                    );
639
640                    // Finds both Alice's devices
641                    let alice_user_id = alice_android_central.get_user_id().await;
642                    let alice_identities = alice_android_central
643                        .context
644                        .get_user_identities(&id, &[alice_user_id.clone()])
645                        .await
646                        .unwrap();
647                    assert_eq!(alice_identities.len(), 1);
648                    let identities = alice_identities.get(&alice_user_id).unwrap();
649                    assert_eq!(identities.len(), 2);
650
651                    // Finds Bob only device
652                    let bob_user_id = bob_android_central.get_user_id().await;
653                    let bob_identities = alice_android_central
654                        .context
655                        .get_user_identities(&id, &[bob_user_id.clone()])
656                        .await
657                        .unwrap();
658                    assert_eq!(bob_identities.len(), 1);
659                    let identities = bob_identities.get(&bob_user_id).unwrap();
660                    assert_eq!(identities.len(), 1);
661
662                    let user_ids = [alice_user_id, bob_user_id];
663                    let expected_sizes = [2, 1];
664
665                    all_identities_check(&mut alice_android_central.context, &id, &user_ids, expected_sizes).await;
666                    all_identities_check(&mut alice_ios_central.context, &id, &user_ids, expected_sizes).await;
667                    all_identities_check(&mut bob_android_central.context, &id, &user_ids, expected_sizes).await;
668                })
669            },
670        )
671        .await
672    }
673
674    #[async_std::test]
675    #[wasm_bindgen_test]
676    async fn should_exchange_messages_cross_signed() {
677        let (alice_android, alice_ios) = (
678            "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@wire.com",
679            "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@wire.com",
680        );
681        let (alicem_android, alicem_ios) = (
682            "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@zeta.com",
683            "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@zeta.com",
684        );
685        let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@wire.com";
686        let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@zeta.com";
687
688        let case = TestCase::default_x509();
689
690        run_cross_signed_tests_with_client_ids(
691            case.clone(),
692            [
693                [alice_android, "alice_wire", "Alice Smith"],
694                [alice_ios, "alice_wire", "Alice Smith"],
695                [bob_android, "bob_wire", "Bob Doe"],
696            ],
697            [
698                [alicem_android, "alice_zeta", "Alice Muller"],
699                [alicem_ios, "alice_zeta", "Alice Muller"],
700                [bobt_android, "bob_zeta", "Bob Tables"],
701            ],
702            ("wire.com", "zeta.com"),
703            move |[mut alices_android_central, mut alices_ios_central, mut bob_android_central],
704                  [mut alicem_android_central, mut alicem_ios_central, mut bobt_android_central]| {
705                Box::pin(async move {
706                    let id = conversation_id();
707                    alices_ios_central
708                        .context
709                        .new_conversation(&id, case.credential_type, case.cfg.clone())
710                        .await
711                        .unwrap();
712
713                    alices_ios_central
714                        .invite_all(
715                            &case,
716                            &id,
717                            [
718                                &mut alices_android_central,
719                                &mut bob_android_central,
720                                &mut alicem_android_central,
721                                &mut alicem_ios_central,
722                                &mut bobt_android_central,
723                            ],
724                        )
725                        .await
726                        .unwrap();
727
728                    let nb_members = alices_android_central
729                        .get_conversation_unchecked(&id)
730                        .await
731                        .members()
732                        .len();
733                    assert_eq!(nb_members, 6);
734
735                    assert_eq!(
736                        alicem_android_central.get_user_id().await,
737                        alicem_ios_central.get_user_id().await
738                    );
739
740                    // cross server communication
741                    bobt_android_central
742                        .try_talk_to(&id, &mut alices_ios_central)
743                        .await
744                        .unwrap();
745
746                    // same server communication
747                    bob_android_central
748                        .try_talk_to(&id, &mut alices_ios_central)
749                        .await
750                        .unwrap();
751                })
752            },
753        )
754        .await;
755    }
756}