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