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
14pub(crate) mod commit;
15mod commit_delay;
16pub(crate) mod config;
17pub(crate) mod conversation_guard;
18mod credential;
19mod duplicate;
20#[cfg(test)]
21mod durability;
22mod error;
23pub(crate) mod group_info;
24mod id;
25mod immutable_conversation;
26pub(crate) mod merge;
27mod orphan_welcome;
28mod own_commit;
29pub(crate) mod pending_conversation;
30mod persistence;
31pub(crate) mod proposal;
32mod renew;
33pub(crate) mod welcome;
34mod wipe;
35
36use std::{
37    collections::{HashMap, HashSet},
38    ops::Deref,
39    sync::Arc,
40};
41
42use itertools::Itertools as _;
43use log::trace;
44use mls_crypto_provider::MlsCryptoProvider;
45use openmls::{
46    group::MlsGroup,
47    prelude::{LeafNodeIndex, Proposal},
48};
49use openmls_traits::{OpenMlsCryptoProvider, types::SignatureScheme};
50
51use self::config::MlsConversationConfiguration;
52pub use self::{
53    conversation_guard::ConversationGuard,
54    error::{Error, Result},
55    id::{ConversationId, ConversationIdRef},
56    immutable_conversation::ImmutableConversation,
57};
58use super::credential::Credential;
59use crate::{
60    Ciphersuite, ClientId, CredentialType, E2eiConversationState, LeafError, MlsError, RecursiveError, UserId,
61    WireIdentity,
62    mls::{HasSessionAndCrypto, Session, credential::ext::CredentialExt as _},
63};
64
65/// The base layer for [Conversation].
66/// The trait is only exposed internally.
67#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
68#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
69pub(crate) trait ConversationWithMls<'a> {
70    /// [`Session`] and [`TransactionContext`][crate::transaction_context::TransactionContext] both implement [`HasSessionAndCrypto`].
71    type Context: HasSessionAndCrypto;
72
73    type Conversation: Deref<Target = MlsConversation> + Send;
74
75    async fn context(&self) -> Result<Self::Context>;
76
77    async fn conversation(&'a self) -> Self::Conversation;
78
79    async fn crypto_provider(&self) -> Result<MlsCryptoProvider> {
80        self.context()
81            .await?
82            .crypto_provider()
83            .await
84            .map_err(RecursiveError::mls("getting mls provider"))
85            .map_err(Into::into)
86    }
87
88    async fn session(&self) -> Result<Session> {
89        self.context()
90            .await?
91            .session()
92            .await
93            .map_err(RecursiveError::mls("getting mls client"))
94            .map_err(Into::into)
95    }
96}
97
98/// The `Conversation` trait provides a set of operations that can be done on
99/// an **immutable** conversation.
100// We keep the super trait internal intentionally, as it is not meant to be used by the public API,
101// hence #[expect(private_bounds)].
102#[expect(private_bounds)]
103#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
104#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
105pub trait Conversation<'a>: ConversationWithMls<'a> {
106    /// Returns the epoch of a given conversation
107    async fn epoch(&'a self) -> u64 {
108        self.conversation().await.group().epoch().as_u64()
109    }
110
111    /// Returns the ciphersuite of a given conversation
112    async fn ciphersuite(&'a self) -> Ciphersuite {
113        self.conversation().await.ciphersuite()
114    }
115
116    /// Derives a new key from the one in the group, to be used elsewhere.
117    ///
118    /// # Arguments
119    /// * `key_length` - the length of the key to be derived. If the value is higher than the
120    ///     bounds of `u16` or the context hash * 255, an error will be returned
121    ///
122    /// # Errors
123    /// OpenMls secret generation error
124    async fn export_secret_key(&'a self, key_length: usize) -> Result<Vec<u8>> {
125        const EXPORTER_LABEL: &str = "exporter";
126        const EXPORTER_CONTEXT: &[u8] = &[];
127        let backend = self.crypto_provider().await?;
128        let inner = self.conversation().await;
129        inner
130            .group()
131            .export_secret(&backend, EXPORTER_LABEL, EXPORTER_CONTEXT, key_length)
132            .map_err(MlsError::wrap("exporting secret key"))
133            .map_err(Into::into)
134    }
135
136    /// Exports the clients from a conversation
137    ///
138    /// # Arguments
139    /// * `conversation_id` - the group/conversation id
140    async fn get_client_ids(&'a self) -> Vec<ClientId> {
141        let inner = self.conversation().await;
142        inner
143            .group()
144            .members()
145            .map(|kp| ClientId::from(kp.credential.identity().to_owned()))
146            .collect()
147    }
148
149    /// Returns the raw public key of the single external sender present in this group.
150    /// This should be used to initialize a subconversation
151    async fn get_external_sender(&'a self) -> Result<Vec<u8>> {
152        let inner = self.conversation().await;
153        let ext_senders = inner
154            .group()
155            .group_context_extensions()
156            .external_senders()
157            .ok_or(Error::MissingExternalSenderExtension)?;
158        let ext_sender = ext_senders.first().ok_or(Error::MissingExternalSenderExtension)?;
159        let ext_sender_public_key = ext_sender.signature_key().as_slice().to_vec();
160        Ok(ext_sender_public_key)
161    }
162
163    /// Indicates when to mark a conversation as not verified i.e. when not all its members have a X509
164    /// Credential generated by Wire's end-to-end identity enrollment
165    async fn e2ei_conversation_state(&'a self) -> Result<E2eiConversationState> {
166        let backend = self.crypto_provider().await?;
167        let authentication_service = backend.authentication_service();
168        authentication_service.refresh_time_of_interest().await;
169        let inner = self.conversation().await;
170        let state = Session::compute_conversation_state(
171            inner.ciphersuite(),
172            inner.group.members_credentials(),
173            CredentialType::X509,
174            authentication_service.borrow().await.as_ref(),
175        )
176        .await;
177        Ok(state)
178    }
179
180    /// From a given conversation, get the identity of the members supplied. Identity is only present for
181    /// members with a Certificate Credential (after turning on end-to-end identity).
182    /// If no member has a x509 certificate, it will return an empty Vec
183    async fn get_device_identities(&'a self, device_ids: &[ClientId]) -> Result<Vec<WireIdentity>> {
184        if device_ids.is_empty() {
185            return Err(Error::CallerError(
186                "This function accepts a list of IDs as a parameter, but that list was empty.",
187            ));
188        }
189        let mls_provider = self.crypto_provider().await?;
190        let auth_service = mls_provider.authentication_service();
191        auth_service.refresh_time_of_interest().await;
192        let auth_service = auth_service.borrow().await;
193        let env = auth_service.as_ref();
194        let conversation = self.conversation().await;
195        conversation
196            .members_with_key()
197            .into_iter()
198            .filter(|(id, _)| device_ids.iter().any(|client_id| *client_id == id.as_slice()))
199            .map(|(_, c)| {
200                c.extract_identity(conversation.ciphersuite(), env)
201                    .map_err(RecursiveError::mls_credential("extracting identity"))
202            })
203            .collect::<Result<Vec<_>, _>>()
204            .map_err(Into::into)
205    }
206
207    /// From a given conversation, get the identity of the users (device holders) supplied.
208    /// Identity is only present for devices with a Certificate Credential (after turning on end-to-end identity).
209    /// If no member has a x509 certificate, it will return an empty Vec.
210    ///
211    /// Returns a Map with all the identities for a given users. Consumers are then recommended to
212    /// reduce those identities to determine the actual status of a user.
213    async fn get_user_identities(&'a self, user_ids: &[String]) -> Result<HashMap<String, Vec<WireIdentity>>> {
214        if user_ids.is_empty() {
215            return Err(Error::CallerError(
216                "This function accepts a list of IDs as a parameter, but that list was empty.",
217            ));
218        }
219        let mls_provider = self.crypto_provider().await?;
220        let auth_service = mls_provider.authentication_service();
221        auth_service.refresh_time_of_interest().await;
222        let auth_service = auth_service.borrow().await;
223        let env = auth_service.as_ref();
224        let conversation = self.conversation().await;
225        let user_ids = user_ids.iter().map(|uid| uid.as_bytes()).collect::<Vec<_>>();
226
227        conversation
228            .members_with_key()
229            .iter()
230            .filter_map(|(id, c)| UserId::try_from(id.as_slice()).ok().zip(Some(c)))
231            .filter(|(uid, _)| user_ids.contains(uid))
232            .map(|(uid, c)| {
233                let uid = String::try_from(uid).map_err(RecursiveError::mls_client("getting user identities"))?;
234                let identity = c
235                    .extract_identity(conversation.ciphersuite(), env)
236                    .map_err(RecursiveError::mls_credential("extracting identity"))?;
237                Ok((uid, identity))
238            })
239            .process_results(|iter| iter.into_group_map())
240    }
241
242    /// Generate a new [`crate::HistorySecret`].
243    ///
244    /// This is useful when it's this client's turn to generate a new history client.
245    ///
246    /// The generated secret is cryptographically unrelated to the current CoreCrypto client.
247    async fn generate_history_secret(&'a self) -> Result<crate::HistorySecret> {
248        let ciphersuite = self.ciphersuite().await;
249        crate::ephemeral::generate_history_secret(ciphersuite)
250            .await
251            .map_err(RecursiveError::root("generating history secret"))
252            .map_err(Into::into)
253    }
254
255    /// Check if history sharing is enabled, i.e., if any of the conversation members have a [ClientId] starting
256    /// with [crate::HISTORY_CLIENT_ID_PREFIX].
257    async fn is_history_sharing_enabled(&'a self) -> bool {
258        self.get_client_ids()
259            .await
260            .iter()
261            .any(|client_id| client_id.starts_with(crate::ephemeral::HISTORY_CLIENT_ID_PREFIX.as_bytes()))
262    }
263}
264
265impl<'a, T: ConversationWithMls<'a>> Conversation<'a> for T {}
266
267/// This is a wrapper on top of the OpenMls's [MlsGroup], that provides Core Crypto specific functionality
268///
269/// This type will store the state of a group. With the [MlsGroup] it holds, it provides all
270/// operations that can be done in a group, such as creating proposals and commits.
271/// More information [here](https://messaginglayersecurity.rocks/mls-architecture/draft-ietf-mls-architecture.html#name-general-setting)
272#[derive(Debug)]
273#[allow(dead_code)]
274pub struct MlsConversation {
275    pub(crate) id: ConversationId,
276    pub(crate) parent_id: Option<ConversationId>,
277    pub(crate) group: MlsGroup,
278    configuration: MlsConversationConfiguration,
279}
280
281impl MlsConversation {
282    /// Creates a new group/conversation
283    ///
284    /// # Arguments
285    /// * `id` - group/conversation identifier
286    /// * `author_client` - the client responsible for creating the group
287    /// * `creator_credential_type` - kind of credential the creator wants to join the group with
288    /// * `config` - group configuration
289    /// * `backend` - MLS Provider that will be used to persist the group
290    ///
291    /// # Errors
292    /// Errors can happen from OpenMls or from the KeyStore
293    pub async fn create(
294        id: ConversationId,
295        author_client: &Session,
296        creator_credential_type: CredentialType,
297        configuration: MlsConversationConfiguration,
298        backend: &MlsCryptoProvider,
299    ) -> Result<Self> {
300        let (cs, ct) = (configuration.ciphersuite, creator_credential_type);
301        let cb = author_client
302            .find_most_recent_or_create_basic_credential(cs.signature_algorithm(), ct)
303            .await
304            .map_err(RecursiveError::mls_client("getting or creating credential"))?;
305
306        let group = MlsGroup::new_with_group_id(
307            backend,
308            &cb.signature_key_pair,
309            &configuration.as_openmls_default_configuration()?,
310            openmls::prelude::GroupId::from_slice(id.as_ref()),
311            cb.to_mls_credential_with_key(),
312        )
313        .await
314        .map_err(MlsError::wrap("creating group with id"))?;
315
316        let mut conversation = Self {
317            id,
318            group,
319            parent_id: None,
320            configuration,
321        };
322
323        conversation
324            .persist_group_when_changed(&backend.keystore(), true)
325            .await?;
326
327        Ok(conversation)
328    }
329
330    /// Internal API: create a group from an existing conversation. For example by external commit
331    pub(crate) async fn from_mls_group(
332        group: MlsGroup,
333        configuration: MlsConversationConfiguration,
334        backend: &MlsCryptoProvider,
335    ) -> Result<Self> {
336        let id = ConversationId::from(group.group_id().as_slice());
337
338        let mut conversation = Self {
339            id,
340            group,
341            configuration,
342            parent_id: None,
343        };
344
345        conversation
346            .persist_group_when_changed(&backend.keystore(), true)
347            .await?;
348
349        Ok(conversation)
350    }
351
352    /// Group/conversation id
353    pub fn id(&self) -> &ConversationId {
354        &self.id
355    }
356
357    pub(crate) fn group(&self) -> &MlsGroup {
358        &self.group
359    }
360
361    /// Get actual group members and subtract pending remove proposals
362    pub fn members_in_next_epoch(&self) -> Vec<ClientId> {
363        let pending_removals = self.pending_removals();
364        let existing_clients = self
365            .group
366            .members()
367            .filter_map(|kp| {
368                if !pending_removals.contains(&kp.index) {
369                    Some(kp.credential.identity().to_owned().into())
370                } else {
371                    trace!(client_index:% = kp.index; "Client is pending removal");
372                    None
373                }
374            })
375            .collect::<HashSet<_>>();
376        existing_clients.into_iter().collect()
377    }
378
379    /// Gather pending remove proposals
380    fn pending_removals(&self) -> Vec<LeafNodeIndex> {
381        self.group
382            .pending_proposals()
383            .filter_map(|proposal| match proposal.proposal() {
384                Proposal::Remove(remove) => Some(remove.removed()),
385                _ => None,
386            })
387            .collect::<Vec<_>>()
388    }
389
390    pub(crate) fn ciphersuite(&self) -> Ciphersuite {
391        self.configuration.ciphersuite
392    }
393
394    pub(crate) fn signature_scheme(&self) -> SignatureScheme {
395        self.ciphersuite().signature_algorithm()
396    }
397
398    pub(crate) async fn find_current_credential(&self, client: &Session) -> Result<Arc<Credential>> {
399        let own_leaf = self.group.own_leaf().ok_or(LeafError::InternalMlsError)?;
400        let sc = self.ciphersuite().signature_algorithm();
401        let ct = self
402            .own_credential_type()
403            .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
404
405        client
406            .find_credential_by_public_key(sc, ct, own_leaf.signature_key())
407            .await
408            .map_err(RecursiveError::mls_client("finding current credential"))
409            .map_err(Into::into)
410    }
411
412    pub(crate) async fn find_most_recent_credential(&self, client: &Session) -> Result<Arc<Credential>> {
413        let sc = self.ciphersuite().signature_algorithm();
414        let ct = self
415            .own_credential_type()
416            .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
417
418        client
419            .find_most_recent_credential(sc, ct)
420            .await
421            .map_err(RecursiveError::mls_client("finding most recent credential"))
422            .map_err(Into::into)
423    }
424}
425
426#[cfg(test)]
427pub mod test_utils {
428    use openmls::prelude::SignaturePublicKey;
429
430    use super::*;
431
432    impl MlsConversation {
433        pub fn signature_keys(&self) -> impl Iterator<Item = SignaturePublicKey> + '_ {
434            self.group
435                .members()
436                .map(|m| m.signature_key)
437                .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
438        }
439
440        pub fn encryption_keys(&self) -> impl Iterator<Item = Vec<u8>> + '_ {
441            self.group.members().map(|m| m.encryption_key)
442        }
443
444        pub fn extensions(&self) -> &openmls::prelude::Extensions {
445            self.group.export_group_context().extensions()
446        }
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453    use crate::test_utils::*;
454
455    #[apply(all_cred_cipher)]
456    pub async fn create_self_conversation_should_succeed(case: TestContext) {
457        let [alice] = case.sessions().await;
458        Box::pin(async move {
459            let conversation = case.create_conversation([&alice]).await;
460            assert_eq!(1, conversation.member_count().await);
461            let alice_can_send_message = conversation.guard().await.encrypt_message(b"me").await;
462            assert!(alice_can_send_message.is_ok());
463        })
464        .await;
465    }
466
467    #[apply(all_cred_cipher)]
468    pub async fn create_1_1_conversation_should_succeed(case: TestContext) {
469        let [alice, bob] = case.sessions().await;
470        Box::pin(async move {
471            let conversation = case.create_conversation([&alice, &bob]).await;
472            assert_eq!(2, conversation.member_count().await);
473            assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
474        })
475        .await;
476    }
477
478    #[apply(all_cred_cipher)]
479    pub async fn create_many_people_conversation(case: TestContext) {
480        const SIZE_PLUS_1: usize = GROUP_SAMPLE_SIZE + 1;
481        let alice_and_friends = case.sessions::<SIZE_PLUS_1>().await;
482        Box::pin(async move {
483            let alice = &alice_and_friends[0];
484            let conversation = case.create_conversation([alice]).await;
485
486            let bob_and_friends = &alice_and_friends[1..];
487            let conversation = conversation.invite_notify(bob_and_friends).await;
488
489            assert_eq!(conversation.member_count().await, 1 + GROUP_SAMPLE_SIZE);
490            assert!(conversation.is_functional_and_contains(&alice_and_friends).await);
491        })
492        .await;
493    }
494
495    mod wire_identity_getters {
496        use super::Error;
497        use crate::{
498            ClientId, CredentialType, DeviceStatus, E2eiConversationState, mls::conversation::Conversation,
499            test_utils::*,
500        };
501
502        async fn all_identities_check<'a, C, const N: usize>(
503            conversation: &'a C,
504            user_ids: &[String; N],
505            expected_sizes: [usize; N],
506        ) where
507            C: Conversation<'a> + Sync,
508        {
509            let all_identities = conversation.get_user_identities(user_ids).await.unwrap();
510            assert_eq!(all_identities.len(), N);
511            for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
512                let alice_identities = all_identities.get(user_id).unwrap();
513                assert_eq!(alice_identities.len(), expected_size);
514            }
515            // Not found
516            let not_found = conversation
517                .get_user_identities(&["aaaaaaaaaaaaa".to_string()])
518                .await
519                .unwrap();
520            assert!(not_found.is_empty());
521
522            // Invalid usage
523            let invalid = conversation.get_user_identities(&[]).await;
524            assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
525        }
526
527        async fn check_identities_device_status<'a, C, const N: usize>(
528            conversation: &'a C,
529            client_ids: &[ClientId; N],
530            name_status: &[(impl ToString, DeviceStatus); N],
531        ) where
532            C: Conversation<'a> + Sync,
533        {
534            let mut identities = conversation.get_device_identities(client_ids).await.unwrap();
535
536            for (user_name, status) in name_status.iter() {
537                let client_identity = identities.remove(
538                    identities
539                        .iter()
540                        .position(|i| i.x509_identity.as_ref().unwrap().display_name == user_name.to_string())
541                        .unwrap(),
542                );
543                assert_eq!(client_identity.status, *status);
544            }
545            assert!(identities.is_empty());
546
547            assert_eq!(
548                conversation.e2ei_conversation_state().await.unwrap(),
549                E2eiConversationState::NotVerified
550            );
551        }
552
553        #[macro_rules_attribute::apply(smol_macros::test)]
554        async fn should_read_device_identities() {
555            let case = TestContext::default_x509();
556
557            let [alice_android, alice_ios] = case.sessions().await;
558            Box::pin(async move {
559                let conversation = case.create_conversation([&alice_android, &alice_ios]).await;
560
561                let (android_id, ios_id) = (alice_android.get_client_id().await, alice_ios.get_client_id().await);
562
563                let mut android_ids = conversation
564                    .guard()
565                    .await
566                    .get_device_identities(&[android_id.clone(), ios_id.clone()])
567                    .await
568                    .unwrap();
569                android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
570                assert_eq!(android_ids.len(), 2);
571                let mut ios_ids = conversation
572                    .guard_of(&alice_ios)
573                    .await
574                    .get_device_identities(&[android_id.clone(), ios_id.clone()])
575                    .await
576                    .unwrap();
577                ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
578                assert_eq!(ios_ids.len(), 2);
579
580                assert_eq!(android_ids, ios_ids);
581
582                let android_identities = conversation
583                    .guard()
584                    .await
585                    .get_device_identities(&[android_id])
586                    .await
587                    .unwrap();
588                let android_id = android_identities.first().unwrap();
589                assert_eq!(
590                    android_id.client_id.as_bytes(),
591                    alice_android.transaction.client_id().await.unwrap().0.as_slice()
592                );
593
594                let ios_identities = conversation
595                    .guard()
596                    .await
597                    .get_device_identities(&[ios_id])
598                    .await
599                    .unwrap();
600                let ios_id = ios_identities.first().unwrap();
601                assert_eq!(
602                    ios_id.client_id.as_bytes(),
603                    alice_ios.transaction.client_id().await.unwrap().0.as_slice()
604                );
605
606                let invalid = conversation.guard().await.get_device_identities(&[]).await;
607                assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
608            })
609            .await
610        }
611
612        #[macro_rules_attribute::apply(smol_macros::test)]
613        async fn should_read_revoked_device_cross_signed() {
614            let case = TestContext::default_x509();
615            let alice_user_id = uuid::Uuid::new_v4();
616            let bob_user_id = uuid::Uuid::new_v4();
617            let rupert_user_id = uuid::Uuid::new_v4();
618            let john_user_id = uuid::Uuid::new_v4();
619            let dilbert_user_id = uuid::Uuid::new_v4();
620
621            let [alice_client_id] = case.x509_client_ids_for_user(&alice_user_id);
622            let [bob_client_id] = case.x509_client_ids_for_user(&bob_user_id);
623            let [rupert_client_id] = case.x509_client_ids_for_user(&rupert_user_id);
624            let [john_client_id] = case.x509_client_ids_for_user(&john_user_id);
625            let [dilbert_client_id] = case.x509_client_ids_for_user(&dilbert_user_id);
626
627            let sessions = case
628                .sessions_x509_cross_signed_with_client_ids_and_revocation(
629                    [alice_client_id, bob_client_id, rupert_client_id],
630                    [john_client_id, dilbert_client_id],
631                    &[dilbert_user_id.to_string(), rupert_user_id.to_string()],
632                )
633                .await;
634
635            Box::pin(async move {
636                let ([alice, bob, rupert], [john, dilbert]) = &sessions;
637                let mut sessions = sessions.0.iter().chain(sessions.1.iter());
638                let conversation = case.create_conversation(&mut sessions).await;
639
640                let (alice_id, bob_id, rupert_id, john_id, dilbert_id) = (
641                    alice.get_client_id().await,
642                    bob.get_client_id().await,
643                    rupert.get_client_id().await,
644                    john.get_client_id().await,
645                    dilbert.get_client_id().await,
646                );
647
648                let client_ids = [alice_id, bob_id, rupert_id, john_id, dilbert_id];
649                let name_status = [
650                    (alice_user_id, DeviceStatus::Valid),
651                    (bob_user_id, DeviceStatus::Valid),
652                    (rupert_user_id, DeviceStatus::Revoked),
653                    (john_user_id, DeviceStatus::Valid),
654                    (dilbert_user_id, DeviceStatus::Revoked),
655                ];
656                // Do it a multiple times to avoid WPB-6904 happening again
657                for _ in 0..2 {
658                    for session in sessions.clone() {
659                        let conversation = conversation.guard_of(session).await;
660                        check_identities_device_status(&conversation, &client_ids, &name_status).await;
661                    }
662                }
663            })
664            .await
665        }
666
667        #[macro_rules_attribute::apply(smol_macros::test)]
668        async fn should_read_revoked_device() {
669            let case = TestContext::default_x509();
670            let rupert_user_id = uuid::Uuid::new_v4();
671            let bob_user_id = uuid::Uuid::new_v4();
672            let alice_user_id = uuid::Uuid::new_v4();
673
674            let [rupert_client_id] = case.x509_client_ids_for_user(&rupert_user_id);
675            let [alice_client_id] = case.x509_client_ids_for_user(&alice_user_id);
676            let [bob_client_id] = case.x509_client_ids_for_user(&bob_user_id);
677
678            let sessions = case
679                .sessions_x509_with_client_ids_and_revocation(
680                    [alice_client_id.clone(), bob_client_id.clone(), rupert_client_id.clone()],
681                    &[rupert_user_id.to_string()],
682                )
683                .await;
684
685            Box::pin(async move {
686                let [alice, bob, rupert] = &sessions;
687                let conversation = case.create_conversation(&sessions).await;
688
689                let (alice_id, bob_id, rupert_id) = (
690                    alice.get_client_id().await,
691                    bob.get_client_id().await,
692                    rupert.get_client_id().await,
693                );
694
695                let client_ids = [alice_id, bob_id, rupert_id];
696                let name_status = [
697                    (alice_user_id, DeviceStatus::Valid),
698                    (bob_user_id, DeviceStatus::Valid),
699                    (rupert_user_id, DeviceStatus::Revoked),
700                ];
701
702                // Do it a multiple times to avoid WPB-6904 happening again
703                for _ in 0..2 {
704                    for session in sessions.iter() {
705                        let conversation = conversation.guard_of(session).await;
706                        check_identities_device_status(&conversation, &client_ids, &name_status).await;
707                    }
708                }
709            })
710            .await
711        }
712
713        #[macro_rules_attribute::apply(smol_macros::test)]
714        async fn should_not_fail_when_basic() {
715            let case = TestContext::default();
716
717            let [alice_android, alice_ios] = case.sessions().await;
718            Box::pin(async move {
719                let conversation = case.create_conversation([&alice_android, &alice_ios]).await;
720
721                let (android_id, ios_id) = (alice_android.get_client_id().await, alice_ios.get_client_id().await);
722
723                let mut android_ids = conversation
724                    .guard()
725                    .await
726                    .get_device_identities(&[android_id.clone(), ios_id.clone()])
727                    .await
728                    .unwrap();
729                android_ids.sort();
730
731                let mut ios_ids = conversation
732                    .guard_of(&alice_ios)
733                    .await
734                    .get_device_identities(&[android_id, ios_id])
735                    .await
736                    .unwrap();
737                ios_ids.sort();
738
739                assert_eq!(ios_ids.len(), 2);
740                assert_eq!(ios_ids, android_ids);
741
742                assert!(ios_ids.iter().all(|i| {
743                    matches!(i.credential_type, CredentialType::Basic)
744                        && matches!(i.status, DeviceStatus::Valid)
745                        && i.x509_identity.is_none()
746                        && !i.thumbprint.is_empty()
747                        && !i.client_id.is_empty()
748                }));
749            })
750            .await
751        }
752
753        // this test is a duplicate of its counterpart but taking federation into account
754        // The heavy lifting of cross-signing the certificates is being done by the test utils.
755        #[macro_rules_attribute::apply(smol_macros::test)]
756        async fn should_read_users_cross_signed() {
757            let case = TestContext::default_x509();
758            let [alice_1_id, alice_2_id] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
759            let [federated_alice_1_id, federated_alice_2_id] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
760            let [bob_id, federated_bob_id] = case.x509_client_ids();
761
762            let ([alice_1, alice_2, bob], [federated_alice_1, federated_alice_2, federated_bob]) = case
763                .sessions_x509_cross_signed_with_client_ids(
764                    [alice_1_id, alice_2_id, bob_id],
765                    [federated_alice_1_id, federated_alice_2_id, federated_bob_id],
766                )
767                .await;
768            Box::pin(async move {
769                let sessions = [
770                    &alice_1,
771                    &alice_2,
772                    &bob,
773                    &federated_bob,
774                    &federated_alice_1,
775                    &federated_alice_2,
776                ];
777                let conversation = case.create_conversation(sessions).await;
778
779                let nb_members = conversation.member_count().await;
780                assert_eq!(nb_members, 6);
781                let conversation_guard = conversation.guard().await;
782
783                assert_eq!(alice_1.get_user_id().await, alice_2.get_user_id().await);
784
785                let alicem_user_id = federated_alice_2.get_user_id().await;
786                let bobt_user_id = federated_bob.get_user_id().await;
787
788                // Finds both Alice's devices
789                let alice_user_id = alice_1.get_user_id().await;
790                let alice_identities = conversation_guard
791                    .get_user_identities(std::slice::from_ref(&alice_user_id))
792                    .await
793                    .unwrap();
794                assert_eq!(alice_identities.len(), 1);
795                let identities = alice_identities.get(&alice_user_id).unwrap();
796                assert_eq!(identities.len(), 2);
797
798                // Finds Bob only device
799                let bob_user_id = bob.get_user_id().await;
800                let bob_identities = conversation_guard
801                    .get_user_identities(std::slice::from_ref(&bob_user_id))
802                    .await
803                    .unwrap();
804                assert_eq!(bob_identities.len(), 1);
805                let identities = bob_identities.get(&bob_user_id).unwrap();
806                assert_eq!(identities.len(), 1);
807
808                // Finds all devices
809                let user_ids = [alice_user_id, bob_user_id, alicem_user_id, bobt_user_id];
810                let expected_sizes = [2, 1, 2, 1];
811
812                for session in sessions {
813                    all_identities_check(&conversation.guard_of(session).await, &user_ids, expected_sizes).await;
814                }
815            })
816            .await
817        }
818
819        #[macro_rules_attribute::apply(smol_macros::test)]
820        async fn should_read_users() {
821            let case = TestContext::default_x509();
822            let [alice_android, alice_ios] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
823            let [bob_android] = case.x509_client_ids();
824
825            let sessions = case
826                .sessions_x509_with_client_ids([alice_android, alice_ios, bob_android])
827                .await;
828
829            Box::pin(async move {
830                let conversation = case.create_conversation(&sessions).await;
831
832                let nb_members = conversation.member_count().await;
833                assert_eq!(nb_members, 3);
834
835                let [alice_android, alice_ios, bob_android] = &sessions;
836                assert_eq!(alice_android.get_user_id().await, alice_ios.get_user_id().await);
837
838                // Finds both Alice's devices
839                let alice_user_id = alice_android.get_user_id().await;
840                let alice_identities = conversation
841                    .guard()
842                    .await
843                    .get_user_identities(std::slice::from_ref(&alice_user_id))
844                    .await
845                    .unwrap();
846                assert_eq!(alice_identities.len(), 1);
847                let identities = alice_identities.get(&alice_user_id).unwrap();
848                assert_eq!(identities.len(), 2);
849
850                // Finds Bob only device
851                let bob_user_id = bob_android.get_user_id().await;
852                let bob_identities = conversation
853                    .guard()
854                    .await
855                    .get_user_identities(std::slice::from_ref(&bob_user_id))
856                    .await
857                    .unwrap();
858                assert_eq!(bob_identities.len(), 1);
859                let identities = bob_identities.get(&bob_user_id).unwrap();
860                assert_eq!(identities.len(), 1);
861
862                let user_ids = [alice_user_id, bob_user_id];
863                let expected_sizes = [2, 1];
864
865                for session in &sessions {
866                    all_identities_check(&conversation.guard_of(session).await, &user_ids, expected_sizes).await;
867                }
868            })
869            .await
870        }
871
872        #[macro_rules_attribute::apply(smol_macros::test)]
873        async fn should_exchange_messages_cross_signed() {
874            let case = TestContext::default_x509();
875            let sessions = case.sessions_x509_cross_signed::<3, 3>().await;
876            Box::pin(async move {
877                let sessions = sessions.0.iter().chain(sessions.1.iter());
878                let conversation = case.create_conversation(sessions.clone()).await;
879
880                assert_eq!(conversation.member_count().await, 6);
881
882                assert!(conversation.is_functional_and_contains(sessions).await);
883            })
884            .await;
885        }
886    }
887
888    mod export_secret {
889        use openmls::prelude::ExportSecretError;
890
891        use super::*;
892        use crate::MlsErrorKind;
893
894        #[apply(all_cred_cipher)]
895        pub async fn can_export_secret_key(case: TestContext) {
896            let [alice] = case.sessions().await;
897            Box::pin(async move {
898                let conversation = case.create_conversation([&alice]).await;
899
900                let key_length = 128;
901                let result = conversation.guard().await.export_secret_key(key_length).await;
902                assert!(result.is_ok());
903                assert_eq!(result.unwrap().len(), key_length);
904            })
905            .await
906        }
907
908        #[apply(all_cred_cipher)]
909        pub async fn cannot_export_secret_key_invalid_length(case: TestContext) {
910            let [alice] = case.sessions().await;
911            Box::pin(async move {
912                let conversation = case.create_conversation([&alice]).await;
913
914                let result = conversation.guard().await.export_secret_key(usize::MAX).await;
915                let error = result.unwrap_err();
916                assert!(innermost_source_matches!(
917                    error,
918                    MlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
919                ));
920            })
921            .await
922        }
923    }
924
925    mod get_client_ids {
926        use super::*;
927
928        #[apply(all_cred_cipher)]
929        pub async fn can_get_client_ids(case: TestContext) {
930            let [alice, bob] = case.sessions().await;
931            Box::pin(async move {
932                let conversation = case.create_conversation([&alice]).await;
933
934                assert_eq!(conversation.guard().await.get_client_ids().await.len(), 1);
935
936                let conversation = conversation.invite_notify([&bob]).await;
937
938                assert_eq!(conversation.guard().await.get_client_ids().await.len(), 2);
939            })
940            .await
941        }
942    }
943
944    mod external_sender {
945        use super::*;
946
947        #[apply(all_cred_cipher)]
948        pub async fn should_fetch_ext_sender(mut case: TestContext) {
949            let [alice, external_sender] = case.sessions().await;
950            Box::pin(async move {
951                let conversation = case
952                    .create_conversation_with_external_sender(&external_sender, [&alice])
953                    .await;
954
955                let alice_ext_sender = conversation.guard().await.get_external_sender().await.unwrap();
956                assert!(!alice_ext_sender.is_empty());
957                assert_eq!(
958                    alice_ext_sender,
959                    external_sender.client_signature_key(&case).await.as_slice().to_vec()
960                );
961            })
962            .await
963        }
964    }
965}