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