core_crypto/mls/conversation/
mod.rs

1//! MLS groups (aka conversation) are the actual entities cementing all the participants in a
2//! conversation.
3//!
4//! This table summarizes what operations are permitted on a group depending its state:
5//! *(PP=pending proposal, PC=pending commit)*
6//!
7//! | can I ?   | 0 PP / 0 PC | 1+ PP / 0 PC | 0 PP / 1 PC | 1+ PP / 1 PC |
8//! |-----------|-------------|--------------|-------------|--------------|
9//! | encrypt   | ✅           | ❌            | ❌           | ❌            |
10//! | handshake | ✅           | ✅            | ❌           | ❌            |
11//! | merge     | ❌           | ❌            | ✅           | ✅            |
12//! | decrypt   | ✅           | ✅            | ✅           | ✅            |
13
14use config::MlsConversationConfiguration;
15use core_crypto_keystore::CryptoKeystoreMls;
16use itertools::Itertools as _;
17use log::trace;
18use mls_crypto_provider::{CryptoKeystore, MlsCryptoProvider};
19use openmls::{
20    group::MlsGroup,
21    prelude::{Credential, CredentialWithKey, LeafNodeIndex, Proposal, SignaturePublicKey},
22};
23use openmls_traits::OpenMlsCryptoProvider;
24use openmls_traits::types::SignatureScheme;
25use std::{collections::HashMap, sync::Arc};
26use std::{collections::HashSet, ops::Deref};
27
28use crate::{
29    KeystoreError, LeafError, MlsError, RecursiveError,
30    mls::Session,
31    prelude::{ClientId, E2eiConversationState, MlsCiphersuite, MlsCredentialType, WireIdentity},
32};
33
34pub(crate) mod commit;
35mod commit_delay;
36pub(crate) mod config;
37pub(crate) mod conversation_guard;
38mod duplicate;
39#[cfg(test)]
40mod durability;
41mod error;
42pub(crate) mod group_info;
43mod immutable_conversation;
44mod leaf_node_validation;
45pub(crate) mod merge;
46mod orphan_welcome;
47mod own_commit;
48pub(crate) mod pending_conversation;
49pub(crate) mod proposal;
50mod renew;
51pub(crate) mod welcome;
52mod wipe;
53
54use crate::mls::HasSessionAndCrypto;
55use crate::mls::credential::ext::CredentialExt as _;
56use crate::prelude::user_id::UserId;
57pub use conversation_guard::ConversationGuard;
58pub use error::{Error, Result};
59pub use immutable_conversation::ImmutableConversation;
60
61use super::credential::CredentialBundle;
62
63/// The base layer for [Conversation].
64/// The trait is only exposed internally.
65#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
66#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
67pub(crate) trait ConversationWithMls<'a> {
68    /// [Session] or [TransactionContext] both implement [HasSessionAndCrypto].
69    type Context: HasSessionAndCrypto;
70
71    type Conversation: Deref<Target = MlsConversation> + Send;
72
73    async fn context(&self) -> Result<Self::Context>;
74
75    async fn conversation(&'a self) -> Self::Conversation;
76
77    async fn crypto_provider(&self) -> Result<MlsCryptoProvider> {
78        self.context()
79            .await?
80            .crypto_provider()
81            .await
82            .map_err(RecursiveError::mls("getting mls provider"))
83            .map_err(Into::into)
84    }
85
86    async fn session(&self) -> Result<Session> {
87        self.context()
88            .await?
89            .session()
90            .await
91            .map_err(RecursiveError::mls("getting mls client"))
92            .map_err(Into::into)
93    }
94}
95
96/// The `Conversation` trait provides a set of operations that can be done on
97/// an **immutable** conversation.
98// We keep the super trait internal intentionally, as it is not meant to be used by the public API,
99// hence #[expect(private_bounds)].
100#[expect(private_bounds)]
101#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
102#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
103pub trait Conversation<'a>: ConversationWithMls<'a> {
104    /// Returns the epoch of a given conversation
105    async fn epoch(&'a self) -> u64 {
106        self.conversation().await.group().epoch().as_u64()
107    }
108
109    /// Returns the ciphersuite of a given conversation
110    async fn ciphersuite(&'a self) -> MlsCiphersuite {
111        self.conversation().await.ciphersuite()
112    }
113
114    /// Derives a new key from the one in the group, to be used elsewhere.
115    ///
116    /// # Arguments
117    /// * `key_length` - the length of the key to be derived. If the value is higher than the
118    ///     bounds of `u16` or the context hash * 255, an error will be returned
119    ///
120    /// # Errors
121    /// OpenMls secret generation error
122    async fn export_secret_key(&'a self, key_length: usize) -> Result<Vec<u8>> {
123        const EXPORTER_LABEL: &str = "exporter";
124        const EXPORTER_CONTEXT: &[u8] = &[];
125        let backend = self.crypto_provider().await?;
126        let inner = self.conversation().await;
127        inner
128            .group()
129            .export_secret(&backend, EXPORTER_LABEL, EXPORTER_CONTEXT, key_length)
130            .map_err(MlsError::wrap("exporting secret key"))
131            .map_err(Into::into)
132    }
133
134    /// Exports the clients from a conversation
135    ///
136    /// # Arguments
137    /// * `conversation_id` - the group/conversation id
138    async fn get_client_ids(&'a self) -> Vec<ClientId> {
139        let inner = self.conversation().await;
140        inner
141            .group()
142            .members()
143            .map(|kp| ClientId::from(kp.credential.identity()))
144            .collect()
145    }
146
147    /// Returns the raw public key of the single external sender present in this group.
148    /// This should be used to initialize a subconversation
149    async fn get_external_sender(&'a self) -> Result<Vec<u8>> {
150        let inner = self.conversation().await;
151        let ext_senders = inner
152            .group()
153            .group_context_extensions()
154            .external_senders()
155            .ok_or(Error::MissingExternalSenderExtension)?;
156        let ext_sender = ext_senders.first().ok_or(Error::MissingExternalSenderExtension)?;
157        let ext_sender_public_key = ext_sender.signature_key().as_slice().to_vec();
158        Ok(ext_sender_public_key)
159    }
160
161    /// Indicates when to mark a conversation as not verified i.e. when not all its members have a X509
162    /// Credential generated by Wire's end-to-end identity enrollment
163    async fn e2ei_conversation_state(&'a self) -> Result<E2eiConversationState> {
164        let backend = self.crypto_provider().await?;
165        let authentication_service = backend.authentication_service();
166        authentication_service.refresh_time_of_interest().await;
167        let inner = self.conversation().await;
168        let state = Session::compute_conversation_state(
169            inner.ciphersuite(),
170            inner.group.members_credentials(),
171            MlsCredentialType::X509,
172            authentication_service.borrow().await.as_ref(),
173        )
174        .await;
175        Ok(state)
176    }
177
178    /// From a given conversation, get the identity of the members supplied. Identity is only present for
179    /// members with a Certificate Credential (after turning on end-to-end identity).
180    /// If no member has a x509 certificate, it will return an empty Vec
181    async fn get_device_identities(&'a self, device_ids: &[ClientId]) -> Result<Vec<WireIdentity>> {
182        if device_ids.is_empty() {
183            return Err(Error::CallerError(
184                "This function accepts a list of IDs as a parameter, but that list was empty.",
185            ));
186        }
187        let mls_provider = self.crypto_provider().await?;
188        let auth_service = mls_provider.authentication_service();
189        auth_service.refresh_time_of_interest().await;
190        let auth_service = auth_service.borrow().await;
191        let env = auth_service.as_ref();
192        let conversation = self.conversation().await;
193        conversation
194            .members_with_key()
195            .into_iter()
196            .filter(|(id, _)| device_ids.contains(&ClientId::from(id.as_slice())))
197            .map(|(_, c)| {
198                c.extract_identity(conversation.ciphersuite(), env)
199                    .map_err(RecursiveError::mls_credential("extracting identity"))
200            })
201            .collect::<Result<Vec<_>, _>>()
202            .map_err(Into::into)
203    }
204
205    /// From a given conversation, get the identity of the users (device holders) supplied.
206    /// Identity is only present for devices with a Certificate Credential (after turning on end-to-end identity).
207    /// If no member has a x509 certificate, it will return an empty Vec.
208    ///
209    /// Returns a Map with all the identities for a given users. Consumers are then recommended to
210    /// reduce those identities to determine the actual status of a user.
211    async fn get_user_identities(&'a self, user_ids: &[String]) -> Result<HashMap<String, Vec<WireIdentity>>> {
212        if user_ids.is_empty() {
213            return Err(Error::CallerError(
214                "This function accepts a list of IDs as a parameter, but that list was empty.",
215            ));
216        }
217        let mls_provider = self.crypto_provider().await?;
218        let auth_service = mls_provider.authentication_service();
219        auth_service.refresh_time_of_interest().await;
220        let auth_service = auth_service.borrow().await;
221        let env = auth_service.as_ref();
222        let conversation = self.conversation().await;
223        let user_ids = user_ids.iter().map(|uid| uid.as_bytes()).collect::<Vec<_>>();
224
225        conversation
226            .members_with_key()
227            .iter()
228            .filter_map(|(id, c)| UserId::try_from(id.as_slice()).ok().zip(Some(c)))
229            .filter(|(uid, _)| user_ids.contains(uid))
230            .map(|(uid, c)| {
231                let uid = String::try_from(uid).map_err(RecursiveError::mls_client("getting user identities"))?;
232                let identity = c
233                    .extract_identity(conversation.ciphersuite(), env)
234                    .map_err(RecursiveError::mls_credential("extracting identity"))?;
235                Ok((uid, identity))
236            })
237            .process_results(|iter| iter.into_group_map())
238    }
239}
240
241impl<'a, T: ConversationWithMls<'a>> Conversation<'a> for T {}
242
243/// A unique identifier for a group/conversation. The identifier must be unique within a client.
244pub type ConversationId = Vec<u8>;
245
246/// This is a wrapper on top of the OpenMls's [MlsGroup], that provides Core Crypto specific functionality
247///
248/// This type will store the state of a group. With the [MlsGroup] it holds, it provides all
249/// operations that can be done in a group, such as creating proposals and commits.
250/// More information [here](https://messaginglayersecurity.rocks/mls-architecture/draft-ietf-mls-architecture.html#name-general-setting)
251#[derive(Debug)]
252#[allow(dead_code)]
253pub struct MlsConversation {
254    pub(crate) id: ConversationId,
255    pub(crate) parent_id: Option<ConversationId>,
256    pub(crate) group: MlsGroup,
257    configuration: MlsConversationConfiguration,
258}
259
260impl MlsConversation {
261    /// Creates a new group/conversation
262    ///
263    /// # Arguments
264    /// * `id` - group/conversation identifier
265    /// * `author_client` - the client responsible for creating the group
266    /// * `creator_credential_type` - kind of credential the creator wants to join the group with
267    /// * `config` - group configuration
268    /// * `backend` - MLS Provider that will be used to persist the group
269    ///
270    /// # Errors
271    /// Errors can happen from OpenMls or from the KeyStore
272    pub async fn create(
273        id: ConversationId,
274        author_client: &Session,
275        creator_credential_type: MlsCredentialType,
276        configuration: MlsConversationConfiguration,
277        backend: &MlsCryptoProvider,
278    ) -> Result<Self> {
279        let (cs, ct) = (configuration.ciphersuite, creator_credential_type);
280        let cb = author_client
281            .get_most_recent_or_create_credential_bundle(backend, cs.signature_algorithm(), ct)
282            .await
283            .map_err(RecursiveError::mls_client("getting or creating credential bundle"))?;
284
285        let group = MlsGroup::new_with_group_id(
286            backend,
287            &cb.signature_key,
288            &configuration.as_openmls_default_configuration()?,
289            openmls::prelude::GroupId::from_slice(id.as_slice()),
290            cb.to_mls_credential_with_key(),
291        )
292        .await
293        .map_err(MlsError::wrap("creating group with id"))?;
294
295        let mut conversation = Self {
296            id,
297            group,
298            parent_id: None,
299            configuration,
300        };
301
302        conversation
303            .persist_group_when_changed(&backend.keystore(), true)
304            .await?;
305
306        Ok(conversation)
307    }
308
309    /// Internal API: create a group from an existing conversation. For example by external commit
310    pub(crate) async fn from_mls_group(
311        group: MlsGroup,
312        configuration: MlsConversationConfiguration,
313        backend: &MlsCryptoProvider,
314    ) -> Result<Self> {
315        let id = ConversationId::from(group.group_id().as_slice());
316
317        let mut conversation = Self {
318            id,
319            group,
320            configuration,
321            parent_id: None,
322        };
323
324        conversation
325            .persist_group_when_changed(&backend.keystore(), true)
326            .await?;
327
328        Ok(conversation)
329    }
330
331    /// Internal API: restore the conversation from a persistence-saved serialized Group State.
332    pub(crate) fn from_serialized_state(buf: Vec<u8>, parent_id: Option<ConversationId>) -> Result<Self> {
333        let group: MlsGroup =
334            core_crypto_keystore::deser(&buf).map_err(KeystoreError::wrap("deserializing group state"))?;
335        let id = ConversationId::from(group.group_id().as_slice());
336        let configuration = MlsConversationConfiguration {
337            ciphersuite: group.ciphersuite().into(),
338            ..Default::default()
339        };
340
341        Ok(Self {
342            id,
343            group,
344            parent_id,
345            configuration,
346        })
347    }
348
349    /// Group/conversation id
350    pub fn id(&self) -> &ConversationId {
351        &self.id
352    }
353
354    pub(crate) fn group(&self) -> &MlsGroup {
355        &self.group
356    }
357
358    /// Returns all members credentials from the group/conversation
359    pub fn members(&self) -> HashMap<Vec<u8>, Credential> {
360        self.group.members().fold(HashMap::new(), |mut acc, kp| {
361            let credential = kp.credential;
362            let id = credential.identity().to_vec();
363            acc.entry(id).or_insert(credential);
364            acc
365        })
366    }
367
368    /// Get actual group members and subtract pending remove proposals
369    pub fn members_in_next_epoch(&self) -> Vec<ClientId> {
370        let pending_removals = self.pending_removals();
371        let existing_clients = self
372            .group
373            .members()
374            .filter_map(|kp| {
375                if !pending_removals.contains(&kp.index) {
376                    Some(kp.credential.identity().into())
377                } else {
378                    trace!(client_index:% = kp.index; "Client is pending removal");
379                    None
380                }
381            })
382            .collect::<HashSet<_>>();
383        existing_clients.into_iter().collect()
384    }
385
386    /// Gather pending remove proposals
387    fn pending_removals(&self) -> Vec<LeafNodeIndex> {
388        self.group
389            .pending_proposals()
390            .filter_map(|proposal| match proposal.proposal() {
391                Proposal::Remove(remove) => Some(remove.removed()),
392                _ => None,
393            })
394            .collect::<Vec<_>>()
395    }
396
397    /// Returns all members credentials with their signature public key from the group/conversation
398    pub fn members_with_key(&self) -> HashMap<Vec<u8>, CredentialWithKey> {
399        self.group.members().fold(HashMap::new(), |mut acc, kp| {
400            let credential = kp.credential;
401            let id = credential.identity().to_vec();
402            let signature_key = SignaturePublicKey::from(kp.signature_key);
403            let credential = CredentialWithKey {
404                credential,
405                signature_key,
406            };
407            acc.entry(id).or_insert(credential);
408            acc
409        })
410    }
411
412    pub(crate) async fn persist_group_when_changed(&mut self, keystore: &CryptoKeystore, force: bool) -> Result<()> {
413        if force || self.group.state_changed() == openmls::group::InnerState::Changed {
414            keystore
415                .mls_group_persist(
416                    &self.id,
417                    &core_crypto_keystore::ser(&self.group).map_err(KeystoreError::wrap("serializing group state"))?,
418                    self.parent_id.as_deref(),
419                )
420                .await
421                .map_err(KeystoreError::wrap("persisting mls group"))?;
422
423            self.group.set_state(openmls::group::InnerState::Persisted);
424        }
425
426        Ok(())
427    }
428
429    pub(crate) fn own_credential_type(&self) -> Result<MlsCredentialType> {
430        Ok(self
431            .group
432            .own_leaf_node()
433            .ok_or(Error::MlsGroupInvalidState("own_leaf_node not present in group"))?
434            .credential()
435            .credential_type()
436            .into())
437    }
438
439    pub(crate) fn ciphersuite(&self) -> MlsCiphersuite {
440        self.configuration.ciphersuite
441    }
442
443    pub(crate) fn signature_scheme(&self) -> SignatureScheme {
444        self.ciphersuite().signature_algorithm()
445    }
446
447    pub(crate) async fn find_current_credential_bundle(&self, client: &Session) -> Result<Arc<CredentialBundle>> {
448        let own_leaf = self.group.own_leaf().ok_or(LeafError::InternalMlsError)?;
449        let sc = self.ciphersuite().signature_algorithm();
450        let ct = self
451            .own_credential_type()
452            .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
453
454        client
455            .find_credential_bundle_by_public_key(sc, ct, own_leaf.signature_key())
456            .await
457            .map_err(RecursiveError::mls_client("finding current credential bundle"))
458            .map_err(Into::into)
459    }
460
461    pub(crate) async fn find_most_recent_credential_bundle(&self, client: &Session) -> Result<Arc<CredentialBundle>> {
462        let sc = self.ciphersuite().signature_algorithm();
463        let ct = self
464            .own_credential_type()
465            .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
466
467        client
468            .find_most_recent_credential_bundle(sc, ct)
469            .await
470            .map_err(RecursiveError::mls_client("finding most recent credential bundle"))
471            .map_err(Into::into)
472    }
473}
474
475#[cfg(test)]
476pub mod test_utils {
477    use super::*;
478
479    impl MlsConversation {
480        pub fn signature_keys(&self) -> impl Iterator<Item = SignaturePublicKey> + '_ {
481            self.group
482                .members()
483                .map(|m| m.signature_key)
484                .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
485        }
486
487        pub fn encryption_keys(&self) -> impl Iterator<Item = Vec<u8>> + '_ {
488            self.group.members().map(|m| m.encryption_key)
489        }
490
491        pub fn extensions(&self) -> &openmls::prelude::Extensions {
492            self.group.export_group_context().extensions()
493        }
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use crate::test_utils::*;
501    use wasm_bindgen_test::*;
502
503    wasm_bindgen_test_configure!(run_in_browser);
504
505    #[apply(all_cred_cipher)]
506    #[wasm_bindgen_test]
507    pub async fn create_self_conversation_should_succeed(case: TestContext) {
508        run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
509            Box::pin(async move {
510                let id = conversation_id();
511                alice_central
512                    .transaction
513                    .new_conversation(&id, case.credential_type, case.cfg.clone())
514                    .await
515                    .unwrap();
516                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
517                assert_eq!(
518                    alice_central
519                        .get_conversation_unchecked(&id)
520                        .await
521                        .group
522                        .group_id()
523                        .as_slice(),
524                    id
525                );
526                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
527                let alice_can_send_message = alice_central
528                    .transaction
529                    .conversation(&id)
530                    .await
531                    .unwrap()
532                    .encrypt_message(b"me")
533                    .await;
534                assert!(alice_can_send_message.is_ok());
535            })
536        })
537        .await;
538    }
539
540    #[apply(all_cred_cipher)]
541    #[wasm_bindgen_test]
542    pub async fn create_1_1_conversation_should_succeed(case: TestContext) {
543        run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
544            Box::pin(async move {
545                let id = conversation_id();
546
547                alice_central
548                    .transaction
549                    .new_conversation(&id, case.credential_type, case.cfg.clone())
550                    .await
551                    .unwrap();
552
553                let bob = bob_central.rand_key_package(&case).await;
554                alice_central
555                    .transaction
556                    .conversation(&id)
557                    .await
558                    .unwrap()
559                    .add_members(vec![bob])
560                    .await
561                    .unwrap();
562
563                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
564                assert_eq!(
565                    alice_central
566                        .get_conversation_unchecked(&id)
567                        .await
568                        .group
569                        .group_id()
570                        .as_slice(),
571                    id
572                );
573                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
574
575                let welcome = alice_central.mls_transport.latest_welcome_message().await;
576                bob_central
577                    .transaction
578                    .process_welcome_message(welcome.into(), case.custom_cfg())
579                    .await
580                    .unwrap();
581
582                assert_eq!(
583                    bob_central.get_conversation_unchecked(&id).await.id(),
584                    alice_central.get_conversation_unchecked(&id).await.id()
585                );
586                assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
587            })
588        })
589        .await;
590    }
591
592    #[apply(all_cred_cipher)]
593    #[wasm_bindgen_test]
594    pub async fn create_many_people_conversation(case: TestContext) {
595        use crate::e2e_identity::enrollment::test_utils::failsafe_ctx;
596
597        run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
598            Box::pin(async move {
599                let x509_test_chain_arc = failsafe_ctx(&mut [&mut alice_central], case.signature_scheme()).await;
600                let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
601
602                let id = conversation_id();
603                alice_central
604                    .transaction
605                    .new_conversation(&id, case.credential_type, case.cfg.clone())
606                    .await
607                    .unwrap();
608                let bob_and_friends = case.sessions_x509::<GROUP_SAMPLE_SIZE>(Some(x509_test_chain)).await;
609
610                let mut bob_and_friends_kps = vec![];
611                for c in &bob_and_friends {
612                    bob_and_friends_kps.push(c.rand_key_package(&case).await);
613                }
614
615                alice_central
616                    .transaction
617                    .conversation(&id)
618                    .await
619                    .unwrap()
620                    .add_members(bob_and_friends_kps)
621                    .await
622                    .unwrap();
623                let welcome = alice_central.mls_transport.latest_welcome_message().await;
624
625                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
626                assert_eq!(
627                    alice_central
628                        .get_conversation_unchecked(&id)
629                        .await
630                        .group
631                        .group_id()
632                        .as_slice(),
633                    id
634                );
635                assert_eq!(
636                    alice_central.get_conversation_unchecked(&id).await.members().len(),
637                    1 + GROUP_SAMPLE_SIZE
638                );
639
640                let mut bob_and_friends_groups = Vec::with_capacity(bob_and_friends.len());
641                // TODO: Do things in parallel, this is waaaaay too slow (takes around 5 minutes). Tracking issue: WPB-9624
642                for c in bob_and_friends {
643                    c.transaction
644                        .process_welcome_message(welcome.clone().into(), case.custom_cfg())
645                        .await
646                        .unwrap();
647                    assert!(c.try_talk_to(&id, &alice_central).await.is_ok());
648                    bob_and_friends_groups.push(c);
649                }
650
651                assert_eq!(bob_and_friends_groups.len(), GROUP_SAMPLE_SIZE);
652            })
653        })
654        .await;
655    }
656
657    mod wire_identity_getters {
658        use wasm_bindgen_test::*;
659
660        use super::Error;
661        use crate::mls::conversation::Conversation as _;
662        use crate::prelude::{ClientId, ConversationId, MlsCredentialType};
663        use crate::transaction_context::TransactionContext;
664        use crate::{
665            prelude::{DeviceStatus, E2eiConversationState},
666            test_utils::*,
667        };
668
669        wasm_bindgen_test_configure!(run_in_browser);
670
671        async fn all_identities_check<const N: usize>(
672            central: &TransactionContext,
673            id: &ConversationId,
674            user_ids: &[String; N],
675            expected_sizes: [usize; N],
676        ) {
677            let all_identities = central
678                .conversation(id)
679                .await
680                .unwrap()
681                .get_user_identities(user_ids)
682                .await
683                .unwrap();
684            assert_eq!(all_identities.len(), N);
685            for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
686                let alice_identities = all_identities.get(user_id).unwrap();
687                assert_eq!(alice_identities.len(), expected_size);
688            }
689            // Not found
690            let not_found = central
691                .conversation(id)
692                .await
693                .unwrap()
694                .get_user_identities(&["aaaaaaaaaaaaa".to_string()])
695                .await
696                .unwrap();
697            assert!(not_found.is_empty());
698
699            // Invalid usage
700            let invalid = central.conversation(id).await.unwrap().get_user_identities(&[]).await;
701            assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
702        }
703
704        async fn check_identities_device_status<const N: usize>(
705            central: &TransactionContext,
706            id: &ConversationId,
707            client_ids: &[ClientId; N],
708            name_status: &[(&'static str, DeviceStatus); N],
709        ) {
710            let mut identities = central
711                .conversation(id)
712                .await
713                .unwrap()
714                .get_device_identities(client_ids)
715                .await
716                .unwrap();
717
718            for j in 0..N {
719                let client_identity = identities.remove(
720                    identities
721                        .iter()
722                        .position(|i| i.x509_identity.as_ref().unwrap().display_name == name_status[j].0)
723                        .unwrap(),
724                );
725                assert_eq!(client_identity.status, name_status[j].1);
726            }
727            assert!(identities.is_empty());
728
729            assert_eq!(
730                central
731                    .conversation(id)
732                    .await
733                    .unwrap()
734                    .e2ei_conversation_state()
735                    .await
736                    .unwrap(),
737                E2eiConversationState::NotVerified
738            );
739        }
740
741        #[async_std::test]
742        #[wasm_bindgen_test]
743        async fn should_read_device_identities() {
744            let case = TestContext::default_x509();
745            run_test_with_client_ids(
746                case.clone(),
747                ["alice_android", "alice_ios"],
748                move |[alice_android_central, alice_ios_central]| {
749                    Box::pin(async move {
750                        let id = conversation_id();
751                        alice_android_central
752                            .transaction
753                            .new_conversation(&id, case.credential_type, case.cfg.clone())
754                            .await
755                            .unwrap();
756                        alice_android_central
757                            .invite_all(&case, &id, [&alice_ios_central])
758                            .await
759                            .unwrap();
760
761                        let (android_id, ios_id) = (
762                            alice_android_central.get_client_id().await,
763                            alice_ios_central.get_client_id().await,
764                        );
765
766                        let mut android_ids = alice_android_central
767                            .transaction
768                            .conversation(&id)
769                            .await
770                            .unwrap()
771                            .get_device_identities(&[android_id.clone(), ios_id.clone()])
772                            .await
773                            .unwrap();
774                        android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
775                        assert_eq!(android_ids.len(), 2);
776                        let mut ios_ids = alice_ios_central
777                            .transaction
778                            .conversation(&id)
779                            .await
780                            .unwrap()
781                            .get_device_identities(&[android_id.clone(), ios_id.clone()])
782                            .await
783                            .unwrap();
784                        ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
785                        assert_eq!(ios_ids.len(), 2);
786
787                        assert_eq!(android_ids, ios_ids);
788
789                        let android_identities = alice_android_central
790                            .transaction
791                            .conversation(&id)
792                            .await
793                            .unwrap()
794                            .get_device_identities(&[android_id])
795                            .await
796                            .unwrap();
797                        let android_id = android_identities.first().unwrap();
798                        assert_eq!(
799                            android_id.client_id.as_bytes(),
800                            alice_android_central
801                                .transaction
802                                .client_id()
803                                .await
804                                .unwrap()
805                                .0
806                                .as_slice()
807                        );
808
809                        let ios_identities = alice_android_central
810                            .transaction
811                            .conversation(&id)
812                            .await
813                            .unwrap()
814                            .get_device_identities(&[ios_id])
815                            .await
816                            .unwrap();
817                        let ios_id = ios_identities.first().unwrap();
818                        assert_eq!(
819                            ios_id.client_id.as_bytes(),
820                            alice_ios_central.transaction.client_id().await.unwrap().0.as_slice()
821                        );
822
823                        let invalid = alice_android_central
824                            .transaction
825                            .conversation(&id)
826                            .await
827                            .unwrap()
828                            .get_device_identities(&[])
829                            .await;
830                        assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
831                    })
832                },
833            )
834            .await
835        }
836
837        #[async_std::test]
838        #[wasm_bindgen_test]
839        async fn should_read_revoked_device_cross_signed() {
840            let case = TestContext::default_x509();
841            run_test_with_client_ids_and_revocation(
842                case.clone(),
843                ["alice", "bob", "rupert"],
844                ["john", "dilbert"],
845                &["rupert", "dilbert"],
846                move |[mut alice, mut bob, mut rupert], [mut john, mut dilbert]| {
847                    Box::pin(async move {
848                        let id = conversation_id();
849                        alice
850                            .transaction
851                            .new_conversation(&id, case.credential_type, case.cfg.clone())
852                            .await
853                            .unwrap();
854                        alice
855                            .invite_all(&case, &id, [&bob, &rupert, &dilbert, &john])
856                            .await
857                            .unwrap();
858
859                        let (alice_id, bob_id, rupert_id, dilbert_id, john_id) = (
860                            alice.get_client_id().await,
861                            bob.get_client_id().await,
862                            rupert.get_client_id().await,
863                            dilbert.get_client_id().await,
864                            john.get_client_id().await,
865                        );
866
867                        let client_ids = [alice_id, bob_id, rupert_id, dilbert_id, john_id];
868                        let name_status = [
869                            ("alice", DeviceStatus::Valid),
870                            ("bob", DeviceStatus::Valid),
871                            ("rupert", DeviceStatus::Revoked),
872                            ("john", DeviceStatus::Valid),
873                            ("dilbert", DeviceStatus::Revoked),
874                        ];
875                        // Do it a multiple times to avoid WPB-6904 happening again
876                        for _ in 0..2 {
877                            check_identities_device_status(&mut alice.transaction, &id, &client_ids, &name_status)
878                                .await;
879                            check_identities_device_status(&mut bob.transaction, &id, &client_ids, &name_status).await;
880                            check_identities_device_status(&mut rupert.transaction, &id, &client_ids, &name_status)
881                                .await;
882                            check_identities_device_status(&mut john.transaction, &id, &client_ids, &name_status).await;
883                            check_identities_device_status(&mut dilbert.transaction, &id, &client_ids, &name_status)
884                                .await;
885                        }
886                    })
887                },
888            )
889            .await
890        }
891
892        #[async_std::test]
893        #[wasm_bindgen_test]
894        async fn should_read_revoked_device() {
895            let case = TestContext::default_x509();
896            run_test_with_client_ids_and_revocation(
897                case.clone(),
898                ["alice", "bob", "rupert"],
899                [],
900                &["rupert"],
901                move |[mut alice, mut bob, mut rupert], []| {
902                    Box::pin(async move {
903                        let id = conversation_id();
904                        alice
905                            .transaction
906                            .new_conversation(&id, case.credential_type, case.cfg.clone())
907                            .await
908                            .unwrap();
909                        alice.invite_all(&case, &id, [&bob, &rupert]).await.unwrap();
910
911                        let (alice_id, bob_id, rupert_id) = (
912                            alice.get_client_id().await,
913                            bob.get_client_id().await,
914                            rupert.get_client_id().await,
915                        );
916
917                        let client_ids = [alice_id, bob_id, rupert_id];
918                        let name_status = [
919                            ("alice", DeviceStatus::Valid),
920                            ("bob", DeviceStatus::Valid),
921                            ("rupert", DeviceStatus::Revoked),
922                        ];
923
924                        // Do it a multiple times to avoid WPB-6904 happening again
925                        for _ in 0..2 {
926                            check_identities_device_status(&mut alice.transaction, &id, &client_ids, &name_status)
927                                .await;
928                            check_identities_device_status(&mut bob.transaction, &id, &client_ids, &name_status).await;
929                            check_identities_device_status(&mut rupert.transaction, &id, &client_ids, &name_status)
930                                .await;
931                        }
932                    })
933                },
934            )
935            .await
936        }
937
938        #[async_std::test]
939        #[wasm_bindgen_test]
940        async fn should_not_fail_when_basic() {
941            let case = TestContext::default();
942            run_test_with_client_ids(
943                case.clone(),
944                ["alice_android", "alice_ios"],
945                move |[alice_android_central, alice_ios_central]| {
946                    Box::pin(async move {
947                        let id = conversation_id();
948                        alice_android_central
949                            .transaction
950                            .new_conversation(&id, case.credential_type, case.cfg.clone())
951                            .await
952                            .unwrap();
953                        alice_android_central
954                            .invite_all(&case, &id, [&alice_ios_central])
955                            .await
956                            .unwrap();
957
958                        let (android_id, ios_id) = (
959                            alice_android_central.get_client_id().await,
960                            alice_ios_central.get_client_id().await,
961                        );
962
963                        let mut android_ids = alice_android_central
964                            .transaction
965                            .conversation(&id)
966                            .await
967                            .unwrap()
968                            .get_device_identities(&[android_id.clone(), ios_id.clone()])
969                            .await
970                            .unwrap();
971                        android_ids.sort();
972
973                        let mut ios_ids = alice_ios_central
974                            .transaction
975                            .conversation(&id)
976                            .await
977                            .unwrap()
978                            .get_device_identities(&[android_id, ios_id])
979                            .await
980                            .unwrap();
981                        ios_ids.sort();
982
983                        assert_eq!(ios_ids.len(), 2);
984                        assert_eq!(ios_ids, android_ids);
985
986                        assert!(ios_ids.iter().all(|i| {
987                            matches!(i.credential_type, MlsCredentialType::Basic)
988                                && matches!(i.status, DeviceStatus::Valid)
989                                && i.x509_identity.is_none()
990                                && !i.thumbprint.is_empty()
991                                && !i.client_id.is_empty()
992                        }));
993                    })
994                },
995            )
996            .await
997        }
998
999        // this test is a duplicate of its counterpart but taking federation into account
1000        // The heavy lifting of cross-signing the certificates is being done by the test utils.
1001        #[async_std::test]
1002        #[wasm_bindgen_test]
1003        async fn should_read_users_cross_signed() {
1004            let case = TestContext::default_x509();
1005
1006            let (alice_android, alice_ios) = (
1007                "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
1008                "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
1009            );
1010            let (alicem_android, alicem_ios) = (
1011                "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@world.com",
1012                "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@world.com",
1013            );
1014            let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
1015            let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@world.com";
1016
1017            run_test_with_deterministic_client_ids_and_revocation(
1018                case.clone(),
1019                [
1020                    [alice_android, "alice_wire", "Alice Smith"],
1021                    [alice_ios, "alice_wire", "Alice Smith"],
1022                    [bob_android, "bob_wire", "Bob Doe"],
1023                ],
1024                [
1025                    [alicem_android, "alice_zeta", "Alice Muller"],
1026                    [alicem_ios, "alice_zeta", "Alice Muller"],
1027                    [bobt_android, "bob_zeta", "Bob Tables"],
1028                ],
1029                &[],
1030                move |[alice_android_central, alice_ios_central, bob_android_central],
1031                      [alicem_android_central, alicem_ios_central, bobt_android_central]| {
1032                    Box::pin(async move {
1033                        let id = conversation_id();
1034                        alice_android_central
1035                            .transaction
1036                            .new_conversation(&id, case.credential_type, case.cfg.clone())
1037                            .await
1038                            .unwrap();
1039                        alice_android_central
1040                            .invite_all(
1041                                &case,
1042                                &id,
1043                                [
1044                                    &alice_ios_central,
1045                                    &bob_android_central,
1046                                    &bobt_android_central,
1047                                    &alicem_ios_central,
1048                                    &alicem_android_central,
1049                                ],
1050                            )
1051                            .await
1052                            .unwrap();
1053
1054                        let nb_members = alice_android_central
1055                            .get_conversation_unchecked(&id)
1056                            .await
1057                            .members()
1058                            .len();
1059                        assert_eq!(nb_members, 6);
1060
1061                        assert_eq!(
1062                            alice_android_central.get_user_id().await,
1063                            alice_ios_central.get_user_id().await
1064                        );
1065
1066                        let alicem_user_id = alicem_ios_central.get_user_id().await;
1067                        let bobt_user_id = bobt_android_central.get_user_id().await;
1068
1069                        // Finds both Alice's devices
1070                        let alice_user_id = alice_android_central.get_user_id().await;
1071                        let alice_identities = alice_android_central
1072                            .transaction
1073                            .conversation(&id)
1074                            .await
1075                            .unwrap()
1076                            .get_user_identities(&[alice_user_id.clone()])
1077                            .await
1078                            .unwrap();
1079                        assert_eq!(alice_identities.len(), 1);
1080                        let identities = alice_identities.get(&alice_user_id).unwrap();
1081                        assert_eq!(identities.len(), 2);
1082
1083                        // Finds Bob only device
1084                        let bob_user_id = bob_android_central.get_user_id().await;
1085                        let bob_identities = alice_android_central
1086                            .transaction
1087                            .conversation(&id)
1088                            .await
1089                            .unwrap()
1090                            .get_user_identities(&[bob_user_id.clone()])
1091                            .await
1092                            .unwrap();
1093                        assert_eq!(bob_identities.len(), 1);
1094                        let identities = bob_identities.get(&bob_user_id).unwrap();
1095                        assert_eq!(identities.len(), 1);
1096
1097                        // Finds all devices
1098                        let user_ids = [alice_user_id, bob_user_id, alicem_user_id, bobt_user_id];
1099                        let expected_sizes = [2, 1, 2, 1];
1100
1101                        all_identities_check(&alice_android_central.transaction, &id, &user_ids, expected_sizes).await;
1102                        all_identities_check(&alicem_android_central.transaction, &id, &user_ids, expected_sizes).await;
1103                        all_identities_check(&alice_ios_central.transaction, &id, &user_ids, expected_sizes).await;
1104                        all_identities_check(&alicem_ios_central.transaction, &id, &user_ids, expected_sizes).await;
1105                        all_identities_check(&bob_android_central.transaction, &id, &user_ids, expected_sizes).await;
1106                        all_identities_check(&bobt_android_central.transaction, &id, &user_ids, expected_sizes).await;
1107                    })
1108                },
1109            )
1110            .await
1111        }
1112
1113        #[async_std::test]
1114        #[wasm_bindgen_test]
1115        async fn should_read_users() {
1116            let case = TestContext::default_x509();
1117
1118            let (alice_android, alice_ios) = (
1119                "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
1120                "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
1121            );
1122            let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
1123
1124            run_test_with_deterministic_client_ids(
1125                case.clone(),
1126                [
1127                    [alice_android, "alice_wire", "Alice Smith"],
1128                    [alice_ios, "alice_wire", "Alice Smith"],
1129                    [bob_android, "bob_wire", "Bob Doe"],
1130                ],
1131                move |[
1132                    mut alice_android_central,
1133                    mut alice_ios_central,
1134                    mut bob_android_central,
1135                ]| {
1136                    Box::pin(async move {
1137                        let id = conversation_id();
1138                        alice_android_central
1139                            .transaction
1140                            .new_conversation(&id, case.credential_type, case.cfg.clone())
1141                            .await
1142                            .unwrap();
1143                        alice_android_central
1144                            .invite_all(&case, &id, [&alice_ios_central, &bob_android_central])
1145                            .await
1146                            .unwrap();
1147
1148                        let nb_members = alice_android_central
1149                            .get_conversation_unchecked(&id)
1150                            .await
1151                            .members()
1152                            .len();
1153                        assert_eq!(nb_members, 3);
1154
1155                        assert_eq!(
1156                            alice_android_central.get_user_id().await,
1157                            alice_ios_central.get_user_id().await
1158                        );
1159
1160                        // Finds both Alice's devices
1161                        let alice_user_id = alice_android_central.get_user_id().await;
1162                        let alice_identities = alice_android_central
1163                            .transaction
1164                            .conversation(&id)
1165                            .await
1166                            .unwrap()
1167                            .get_user_identities(&[alice_user_id.clone()])
1168                            .await
1169                            .unwrap();
1170                        assert_eq!(alice_identities.len(), 1);
1171                        let identities = alice_identities.get(&alice_user_id).unwrap();
1172                        assert_eq!(identities.len(), 2);
1173
1174                        // Finds Bob only device
1175                        let bob_user_id = bob_android_central.get_user_id().await;
1176                        let bob_identities = alice_android_central
1177                            .transaction
1178                            .conversation(&id)
1179                            .await
1180                            .unwrap()
1181                            .get_user_identities(&[bob_user_id.clone()])
1182                            .await
1183                            .unwrap();
1184                        assert_eq!(bob_identities.len(), 1);
1185                        let identities = bob_identities.get(&bob_user_id).unwrap();
1186                        assert_eq!(identities.len(), 1);
1187
1188                        let user_ids = [alice_user_id, bob_user_id];
1189                        let expected_sizes = [2, 1];
1190
1191                        all_identities_check(&mut alice_android_central.transaction, &id, &user_ids, expected_sizes)
1192                            .await;
1193                        all_identities_check(&mut alice_ios_central.transaction, &id, &user_ids, expected_sizes).await;
1194                        all_identities_check(&mut bob_android_central.transaction, &id, &user_ids, expected_sizes)
1195                            .await;
1196                    })
1197                },
1198            )
1199            .await
1200        }
1201
1202        #[async_std::test]
1203        #[wasm_bindgen_test]
1204        async fn should_exchange_messages_cross_signed() {
1205            let (alice_android, alice_ios) = (
1206                "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@wire.com",
1207                "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@wire.com",
1208            );
1209            let (alicem_android, alicem_ios) = (
1210                "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@zeta.com",
1211                "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@zeta.com",
1212            );
1213            let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@wire.com";
1214            let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@zeta.com";
1215
1216            let case = TestContext::default_x509();
1217
1218            run_cross_signed_tests_with_client_ids(
1219                case.clone(),
1220                [
1221                    [alice_android, "alice_wire", "Alice Smith"],
1222                    [alice_ios, "alice_wire", "Alice Smith"],
1223                    [bob_android, "bob_wire", "Bob Doe"],
1224                ],
1225                [
1226                    [alicem_android, "alice_zeta", "Alice Muller"],
1227                    [alicem_ios, "alice_zeta", "Alice Muller"],
1228                    [bobt_android, "bob_zeta", "Bob Tables"],
1229                ],
1230                ("wire.com", "zeta.com"),
1231                move |[mut alices_android_central, alices_ios_central, mut bob_android_central],
1232                      [
1233                    mut alicem_android_central,
1234                    mut alicem_ios_central,
1235                    mut bobt_android_central,
1236                ]| {
1237                    Box::pin(async move {
1238                        let id = conversation_id();
1239                        alices_ios_central
1240                            .transaction
1241                            .new_conversation(&id, case.credential_type, case.cfg.clone())
1242                            .await
1243                            .unwrap();
1244
1245                        alices_ios_central
1246                            .invite_all(
1247                                &case,
1248                                &id,
1249                                [
1250                                    &mut alices_android_central,
1251                                    &mut bob_android_central,
1252                                    &mut alicem_android_central,
1253                                    &mut alicem_ios_central,
1254                                    &mut bobt_android_central,
1255                                ],
1256                            )
1257                            .await
1258                            .unwrap();
1259
1260                        let nb_members = alices_android_central
1261                            .get_conversation_unchecked(&id)
1262                            .await
1263                            .members()
1264                            .len();
1265                        assert_eq!(nb_members, 6);
1266
1267                        assert_eq!(
1268                            alicem_android_central.get_user_id().await,
1269                            alicem_ios_central.get_user_id().await
1270                        );
1271
1272                        // cross server communication
1273                        bobt_android_central
1274                            .try_talk_to(&id, &alices_ios_central)
1275                            .await
1276                            .unwrap();
1277
1278                        // same server communication
1279                        bob_android_central.try_talk_to(&id, &alices_ios_central).await.unwrap();
1280                    })
1281                },
1282            )
1283            .await;
1284        }
1285    }
1286
1287    mod export_secret {
1288        use super::*;
1289        use crate::MlsErrorKind;
1290        use openmls::prelude::ExportSecretError;
1291
1292        #[apply(all_cred_cipher)]
1293        #[wasm_bindgen_test]
1294        pub async fn can_export_secret_key(case: TestContext) {
1295            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1296                Box::pin(async move {
1297                    let id = conversation_id();
1298                    alice_central
1299                        .transaction
1300                        .new_conversation(&id, case.credential_type, case.cfg.clone())
1301                        .await
1302                        .unwrap();
1303
1304                    let key_length = 128;
1305                    let result = alice_central
1306                        .transaction
1307                        .conversation(&id)
1308                        .await
1309                        .unwrap()
1310                        .export_secret_key(key_length)
1311                        .await;
1312                    assert!(result.is_ok());
1313                    assert_eq!(result.unwrap().len(), key_length);
1314                })
1315            })
1316            .await
1317        }
1318
1319        #[apply(all_cred_cipher)]
1320        #[wasm_bindgen_test]
1321        pub async fn cannot_export_secret_key_invalid_length(case: TestContext) {
1322            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1323                Box::pin(async move {
1324                    let id = conversation_id();
1325                    alice_central
1326                        .transaction
1327                        .new_conversation(&id, case.credential_type, case.cfg.clone())
1328                        .await
1329                        .unwrap();
1330
1331                    let result = alice_central
1332                        .transaction
1333                        .conversation(&id)
1334                        .await
1335                        .unwrap()
1336                        .export_secret_key(usize::MAX)
1337                        .await;
1338                    let error = result.unwrap_err();
1339                    assert!(innermost_source_matches!(
1340                        error,
1341                        MlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
1342                    ));
1343                })
1344            })
1345            .await
1346        }
1347    }
1348
1349    mod get_client_ids {
1350        use super::*;
1351
1352        #[apply(all_cred_cipher)]
1353        #[wasm_bindgen_test]
1354        pub async fn can_get_client_ids(case: TestContext) {
1355            run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1356                Box::pin(async move {
1357                    let id = conversation_id();
1358                    alice_central
1359                        .transaction
1360                        .new_conversation(&id, case.credential_type, case.cfg.clone())
1361                        .await
1362                        .unwrap();
1363
1364                    assert_eq!(
1365                        alice_central
1366                            .transaction
1367                            .conversation(&id)
1368                            .await
1369                            .unwrap()
1370                            .get_client_ids()
1371                            .await
1372                            .len(),
1373                        1
1374                    );
1375
1376                    alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1377                    assert_eq!(
1378                        alice_central
1379                            .transaction
1380                            .conversation(&id)
1381                            .await
1382                            .unwrap()
1383                            .get_client_ids()
1384                            .await
1385                            .len(),
1386                        2
1387                    );
1388                })
1389            })
1390            .await
1391        }
1392    }
1393
1394    mod external_sender {
1395        use super::*;
1396
1397        #[apply(all_cred_cipher)]
1398        #[wasm_bindgen_test]
1399        pub async fn should_fetch_ext_sender(case: TestContext) {
1400            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1401                Box::pin(async move {
1402                    let id = conversation_id();
1403
1404                    // by default in test no external sender is set. Let's add one
1405                    let mut cfg = case.cfg.clone();
1406                    let external_sender = alice_central.rand_external_sender(&case).await;
1407                    cfg.external_senders = vec![external_sender.clone()];
1408
1409                    alice_central
1410                        .transaction
1411                        .new_conversation(&id, case.credential_type, cfg)
1412                        .await
1413                        .unwrap();
1414
1415                    let alice_ext_sender = alice_central
1416                        .transaction
1417                        .conversation(&id)
1418                        .await
1419                        .unwrap()
1420                        .get_external_sender()
1421                        .await
1422                        .unwrap();
1423                    assert!(!alice_ext_sender.is_empty());
1424                    assert_eq!(alice_ext_sender, external_sender.signature_key().as_slice().to_vec());
1425                })
1426            })
1427            .await
1428        }
1429    }
1430}