core_crypto/mls/
mod.rs

1use std::sync::Arc;
2
3use async_lock::RwLock;
4use log::trace;
5
6use crate::{
7    CoreCrypto, LeafError, MlsError, RecursiveError,
8    prelude::{
9        Client, ClientId, ConversationId, MlsCentralConfiguration, MlsCiphersuite, MlsConversation,
10        MlsConversationConfiguration, MlsCredentialType, MlsTransport, identifier::ClientIdentifier,
11        key_package::INITIAL_KEYING_MATERIAL_COUNT,
12    },
13};
14use mls_crypto_provider::{EntropySeed, MlsCryptoProvider, MlsCryptoProviderConfiguration};
15use openmls_traits::OpenMlsCryptoProvider;
16
17use crate::context::CentralContext;
18
19pub(crate) mod ciphersuite;
20pub(crate) mod client;
21pub mod conversation;
22pub(crate) mod credential;
23mod error;
24pub(crate) mod external_commit;
25pub(crate) mod external_proposal;
26pub(crate) mod proposal;
27
28pub use client::EpochObserver;
29pub use error::{Error, Result};
30
31// Prevents direct instantiation of [MlsCentralConfiguration]
32pub(crate) mod config {
33    use ciphersuite::MlsCiphersuite;
34    use mls_crypto_provider::EntropySeed;
35
36    use super::*;
37
38    /// Configuration parameters for `MlsCentral`
39    #[derive(Debug, Clone)]
40    #[non_exhaustive]
41    pub struct MlsCentralConfiguration {
42        /// Location where the SQLite/IndexedDB database will be stored
43        pub store_path: String,
44        /// Identity key to be used to instantiate the [MlsCryptoProvider]
45        pub identity_key: String,
46        /// Identifier for the client to be used by [MlsCentral]
47        pub client_id: Option<ClientId>,
48        /// Entropy pool seed for the internal PRNG
49        pub external_entropy: Option<EntropySeed>,
50        /// All supported ciphersuites
51        pub ciphersuites: Vec<ciphersuite::MlsCiphersuite>,
52        /// Number of [openmls::prelude::KeyPackage] to create when creating a MLS client. Default to [INITIAL_KEYING_MATERIAL_COUNT]
53        pub nb_init_key_packages: Option<usize>,
54    }
55
56    impl MlsCentralConfiguration {
57        /// Creates a new instance of the configuration.
58        ///
59        /// # Arguments
60        /// * `store_path` - location where the SQLite/IndexedDB database will be stored
61        /// * `identity_key` - identity key to be used to instantiate the [MlsCryptoProvider]
62        /// * `client_id` - identifier for the client to be used by [MlsCentral]
63        /// * `ciphersuites` - Ciphersuites supported by this device
64        /// * `entropy` - External source of entropy for platforms where default source insufficient
65        ///
66        /// # Errors
67        /// Any empty string parameter will result in a [Error::MalformedIdentifier] error.
68        ///
69        /// # Examples
70        ///
71        /// This should fail:
72        /// ```
73        /// use core_crypto::{prelude::MlsCentralConfiguration, mls::Error};
74        ///
75        /// let result = MlsCentralConfiguration::try_new(String::new(), String::new(), Some(b"".to_vec().into()), vec![], None, Some(100));
76        /// assert!(matches!(result.unwrap_err(), Error::MalformedIdentifier(_)));
77        /// ```
78        ///
79        /// This should work:
80        /// ```
81        /// use core_crypto::prelude::{MlsCentralConfiguration, MlsCiphersuite};
82        ///
83        /// let result = MlsCentralConfiguration::try_new(
84        ///     "/tmp/crypto".to_string(),
85        ///     "MY_IDENTITY_KEY".to_string(),
86        ///     Some(b"MY_CLIENT_ID".to_vec().into()),
87        ///     vec![MlsCiphersuite::default()],
88        ///     None,
89        ///     Some(100),
90        /// );
91        /// assert!(result.is_ok());
92        /// ```
93        pub fn try_new(
94            store_path: String,
95            identity_key: String,
96            client_id: Option<ClientId>,
97            ciphersuites: Vec<MlsCiphersuite>,
98            entropy: Option<Vec<u8>>,
99            nb_init_key_packages: Option<usize>,
100        ) -> Result<Self> {
101            // TODO: probably more complex rules to enforce. Tracking issue: WPB-9598
102            if store_path.trim().is_empty() {
103                return Err(Error::MalformedIdentifier("store_path"));
104            }
105            // TODO: probably more complex rules to enforce. Tracking issue: WPB-9598
106            if identity_key.trim().is_empty() {
107                return Err(Error::MalformedIdentifier("identity_key"));
108            }
109            // TODO: probably more complex rules to enforce. Tracking issue: WPB-9598
110            if let Some(client_id) = client_id.as_ref() {
111                if client_id.is_empty() {
112                    return Err(Error::MalformedIdentifier("client_id"));
113                }
114            }
115            let external_entropy = entropy
116                .as_deref()
117                .map(|seed| &seed[..EntropySeed::EXPECTED_LEN])
118                .map(EntropySeed::try_from_slice)
119                .transpose()
120                .map_err(MlsError::wrap("gathering external entropy"))?;
121            Ok(Self {
122                store_path,
123                identity_key,
124                client_id,
125                ciphersuites,
126                external_entropy,
127                nb_init_key_packages,
128            })
129        }
130
131        /// Sets the entropy seed
132        pub fn set_entropy(&mut self, entropy: EntropySeed) {
133            self.external_entropy = Some(entropy);
134        }
135
136        #[cfg(test)]
137        #[allow(dead_code)]
138        /// Creates temporary file to prevent test collisions which would happen with hardcoded file path
139        /// Intended to be used only in tests.
140        pub(crate) fn tmp_store_path(tmp_dir: &tempfile::TempDir) -> String {
141            let path = tmp_dir.path().join("store.edb");
142            std::fs::File::create(&path).unwrap();
143            path.to_str().unwrap().to_string()
144        }
145    }
146}
147
148#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
149#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
150
151pub(crate) trait HasClientAndProvider: Send {
152    async fn client(&self) -> Result<Client>;
153    async fn mls_provider(&self) -> Result<MlsCryptoProvider>;
154}
155
156/// The entry point for the MLS CoreCrypto library. This struct provides all functionality to create
157/// and manage groups, make proposals and commits.
158#[derive(Debug, Clone)]
159pub struct MlsCentral {
160    pub(crate) mls_client: Client,
161    pub(crate) mls_backend: MlsCryptoProvider,
162    // this should be moved to the context
163    pub(crate) transport: Arc<RwLock<Option<Arc<dyn MlsTransport + 'static>>>>,
164}
165
166#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
167#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
168impl HasClientAndProvider for MlsCentral {
169    async fn client(&self) -> Result<Client> {
170        Ok(self.mls_client.clone())
171    }
172
173    async fn mls_provider(&self) -> Result<MlsCryptoProvider> {
174        Ok(self.mls_backend.clone())
175    }
176}
177
178impl MlsCentral {
179    /// Tries to initialize the MLS Central object.
180    /// Takes a store path (i.e. Disk location of the embedded database, should be consistent between messaging sessions)
181    /// And a root identity key (i.e. enclaved encryption key for this device)
182    ///
183    /// # Arguments
184    /// * `configuration` - the configuration for the `MlsCentral`
185    ///
186    /// # Errors
187    /// Failures in the initialization of the KeyStore can cause errors, such as IO, the same kind
188    /// of errors can happen when the groups are being restored from the KeyStore or even during
189    /// the client initialization (to fetch the identity signature). Other than that, `MlsError`
190    /// can be caused by group deserialization or during the initialization of the credentials:
191    /// * for x509 Credentials if the cetificate chain length is lower than 2
192    /// * for Basic Credentials if the signature key cannot be generated either by not supported
193    ///   scheme or the key generation fails
194    pub async fn try_new(configuration: MlsCentralConfiguration) -> Result<Self> {
195        // Init backend (crypto + rand + keystore)
196        let mls_backend = MlsCryptoProvider::try_new_with_configuration(MlsCryptoProviderConfiguration {
197            db_path: &configuration.store_path,
198            identity_key: &configuration.identity_key,
199            in_memory: false,
200            entropy_seed: configuration.external_entropy.clone(),
201        })
202        .await
203        .map_err(MlsError::wrap("trying to initialize mls crypto provider object"))?;
204        Self::new_with_backend(mls_backend, configuration).await
205    }
206
207    /// Same as the [MlsCentral::try_new] but instead, it uses an in memory KeyStore. Although required, the `store_path` parameter from the `MlsCentralConfiguration` won't be used here.
208    pub async fn try_new_in_memory(configuration: MlsCentralConfiguration) -> Result<Self> {
209        let mls_backend = MlsCryptoProvider::try_new_with_configuration(MlsCryptoProviderConfiguration {
210            db_path: &configuration.store_path,
211            identity_key: &configuration.identity_key,
212            in_memory: true,
213            entropy_seed: configuration.external_entropy.clone(),
214        })
215        .await
216        .map_err(MlsError::wrap(
217            "trying to initialize mls crypto provider object (in memory)",
218        ))?;
219        Self::new_with_backend(mls_backend, configuration).await
220    }
221
222    async fn new_with_backend(mls_backend: MlsCryptoProvider, configuration: MlsCentralConfiguration) -> Result<Self> {
223        let mls_client = Client::default();
224
225        // We create the core crypto instance first to enable creating a transaction from it and
226        // doing all subsequent actions inside a single transaction, though it forces us to clone
227        // a few Arcs and locks.
228        let central = Self {
229            mls_backend: mls_backend.clone(),
230            mls_client: mls_client.clone(),
231            transport: Arc::new(None.into()),
232        };
233
234        let cc = CoreCrypto::from(central);
235        let context = cc
236            .new_transaction()
237            .await
238            .map_err(RecursiveError::root("starting new transaction"))?;
239        if let Some(id) = configuration.client_id {
240            mls_client
241                .init(
242                    ClientIdentifier::Basic(id),
243                    configuration.ciphersuites.as_slice(),
244                    &mls_backend,
245                    configuration
246                        .nb_init_key_packages
247                        .unwrap_or(INITIAL_KEYING_MATERIAL_COUNT),
248                )
249                .await
250                .map_err(RecursiveError::mls_client("initializing mls client"))?
251        }
252        let central = cc.mls;
253        context
254            .init_pki_env()
255            .await
256            .map_err(RecursiveError::e2e_identity("initializing pki environment"))?;
257        context
258            .finish()
259            .await
260            .map_err(RecursiveError::root("finishing transaction"))?;
261        Ok(central)
262    }
263
264    /// Provide the implementation of functions to communicate with the delivery service
265    /// (see [MlsTransport]).
266    pub async fn provide_transport(&self, transport: Arc<dyn MlsTransport>) {
267        self.transport.write().await.replace(transport);
268    }
269
270    /// Returns the client's most recent public signature key as a buffer.
271    /// Used to upload a public key to the server in order to verify client's messages signature.
272    ///
273    /// # Arguments
274    /// * `ciphersuite` - a callback to be called to perform authorization
275    /// * `credential_type` - of the credential to look for
276    pub async fn client_public_key(
277        &self,
278        ciphersuite: MlsCiphersuite,
279        credential_type: MlsCredentialType,
280    ) -> Result<Vec<u8>> {
281        let cb = self
282            .mls_client
283            .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), credential_type)
284            .await
285            .map_err(RecursiveError::mls_client("finding most recent credential bundle"))?;
286        Ok(cb.signature_key.to_public_vec())
287    }
288
289    /// Returns the client's id as a buffer
290    pub async fn client_id(&self) -> Result<ClientId> {
291        self.mls_client
292            .id()
293            .await
294            .map_err(RecursiveError::mls_client("getting client id"))
295            .map_err(Into::into)
296    }
297
298    /// Checks if a given conversation id exists locally
299    pub async fn conversation_exists(&self, id: &ConversationId) -> Result<bool> {
300        match self.get_raw_conversation(id).await {
301            Ok(_) => Ok(true),
302            Err(conversation::Error::Leaf(LeafError::ConversationNotFound(_))) => Ok(false),
303            Err(e) => {
304                Err(RecursiveError::mls_conversation("getting conversation by id to check for existence")(e).into())
305            }
306        }
307    }
308
309    /// Generates a random byte array of the specified size
310    pub fn random_bytes(&self, len: usize) -> Result<Vec<u8>> {
311        use openmls_traits::random::OpenMlsRand as _;
312        self.mls_backend
313            .rand()
314            .random_vec(len)
315            .map_err(MlsError::wrap("generating random vector"))
316            .map_err(Into::into)
317    }
318
319    /// Closes the connection with the local KeyStore
320    ///
321    /// # Errors
322    /// KeyStore errors, such as IO
323    pub async fn close(self) -> Result<()> {
324        self.mls_backend
325            .close()
326            .await
327            .map_err(MlsError::wrap("closing connection with keystore"))
328            .map_err(Into::into)
329    }
330
331    /// see [mls_crypto_provider::MlsCryptoProvider::reseed]
332    pub async fn reseed(&self, seed: Option<EntropySeed>) -> Result<()> {
333        self.mls_backend
334            .reseed(seed)
335            .map_err(MlsError::wrap("reseeding mls backend"))
336            .map_err(Into::into)
337    }
338}
339
340impl CentralContext {
341    /// Initializes the MLS client if [super::CoreCrypto] has previously been initialized with
342    /// `CoreCrypto::deferred_init` instead of `CoreCrypto::new`.
343    /// This should stay as long as proteus is supported. Then it should be removed.
344    pub async fn mls_init(
345        &self,
346        identifier: ClientIdentifier,
347        ciphersuites: Vec<MlsCiphersuite>,
348        nb_init_key_packages: Option<usize>,
349    ) -> Result<()> {
350        let nb_key_package = nb_init_key_packages.unwrap_or(INITIAL_KEYING_MATERIAL_COUNT);
351        let mls_client = self
352            .mls_client()
353            .await
354            .map_err(RecursiveError::root("getting mls client"))?;
355        mls_client
356            .init(
357                identifier,
358                &ciphersuites,
359                &self
360                    .mls_provider()
361                    .await
362                    .map_err(RecursiveError::root("getting mls provider"))?,
363                nb_key_package,
364            )
365            .await
366            .map_err(RecursiveError::mls_client("initializing mls client"))?;
367
368        if mls_client.is_e2ei_capable().await {
369            let client_id = mls_client
370                .id()
371                .await
372                .map_err(RecursiveError::mls_client("getting client id"))?;
373            trace!(client_id:% = client_id; "Initializing PKI environment");
374            self.init_pki_env()
375                .await
376                .map_err(RecursiveError::e2e_identity("initializing pki env"))?;
377        }
378
379        Ok(())
380    }
381
382    /// Generates MLS KeyPairs/CredentialBundle with a temporary, random client ID.
383    /// This method is designed to be used in conjunction with [CentralContext::mls_init_with_client_id] and represents the first step in this process.
384    ///
385    /// This returns the TLS-serialized identity keys (i.e. the signature keypair's public key)
386    #[cfg_attr(test, crate::dispotent)]
387    pub async fn mls_generate_keypairs(&self, ciphersuites: Vec<MlsCiphersuite>) -> Result<Vec<ClientId>> {
388        self.mls_client()
389            .await
390            .map_err(RecursiveError::root("getting mls client"))?
391            .generate_raw_keypairs(
392                &ciphersuites,
393                &self
394                    .mls_provider()
395                    .await
396                    .map_err(RecursiveError::root("getting mls provider"))?,
397            )
398            .await
399            .map_err(RecursiveError::mls_client("generating raw keypairs"))
400            .map_err(Into::into)
401    }
402
403    /// Updates the current temporary Client ID with the newly provided one. This is the second step in the externally-generated clients process
404    ///
405    /// Important: This is designed to be called after [CentralContext::mls_generate_keypairs]
406    #[cfg_attr(test, crate::dispotent)]
407    pub async fn mls_init_with_client_id(
408        &self,
409        client_id: ClientId,
410        tmp_client_ids: Vec<ClientId>,
411        ciphersuites: Vec<MlsCiphersuite>,
412    ) -> Result<()> {
413        self.mls_client()
414            .await
415            .map_err(RecursiveError::root("getting mls client"))?
416            .init_with_external_client_id(
417                client_id,
418                tmp_client_ids,
419                &ciphersuites,
420                &self
421                    .mls_provider()
422                    .await
423                    .map_err(RecursiveError::root("getting mls provider"))?,
424            )
425            .await
426            .map_err(RecursiveError::mls_client(
427                "initializing mls client with external client id",
428            ))
429            .map_err(Into::into)
430    }
431
432    /// see [MlsCentral::client_public_key]
433    pub async fn client_public_key(
434        &self,
435        ciphersuite: MlsCiphersuite,
436        credential_type: MlsCredentialType,
437    ) -> Result<Vec<u8>> {
438        let cb = self
439            .mls_client()
440            .await
441            .map_err(RecursiveError::root("getting mls client"))?
442            .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), credential_type)
443            .await
444            .map_err(RecursiveError::mls_client("finding most recent credential bundle"))?;
445        Ok(cb.signature_key.to_public_vec())
446    }
447
448    /// see [MlsCentral::client_id]
449    pub async fn client_id(&self) -> Result<ClientId> {
450        self.mls_client()
451            .await
452            .map_err(RecursiveError::root("getting mls client"))?
453            .id()
454            .await
455            .map_err(RecursiveError::mls_client("getting client id"))
456            .map_err(Into::into)
457    }
458
459    /// Create a new empty conversation
460    ///
461    /// # Arguments
462    /// * `id` - identifier of the group/conversation (must be unique otherwise the existing group
463    ///   will be overridden)
464    /// * `creator_credential_type` - kind of credential the creator wants to create the group with
465    /// * `config` - configuration of the group/conversation
466    ///
467    /// # Errors
468    /// Errors can happen from the KeyStore or from OpenMls for ex if no [openmls::key_packages::KeyPackage] can
469    /// be found in the KeyStore
470    #[cfg_attr(test, crate::dispotent)]
471    pub async fn new_conversation(
472        &self,
473        id: &ConversationId,
474        creator_credential_type: MlsCredentialType,
475        config: MlsConversationConfiguration,
476    ) -> Result<()> {
477        if self.conversation_exists(id).await? || self.pending_conversation_exists(id).await? {
478            return Err(LeafError::ConversationAlreadyExists(id.clone()).into());
479        }
480        let conversation = MlsConversation::create(
481            id.clone(),
482            &self
483                .mls_client()
484                .await
485                .map_err(RecursiveError::root("getting mls client"))?,
486            creator_credential_type,
487            config,
488            &self
489                .mls_provider()
490                .await
491                .map_err(RecursiveError::root("getting mls provider"))?,
492        )
493        .await
494        .map_err(RecursiveError::mls_conversation("creating conversation"))?;
495
496        self.mls_groups()
497            .await
498            .map_err(RecursiveError::root("getting mls groups"))?
499            .insert(id.clone(), conversation);
500
501        Ok(())
502    }
503
504    /// Checks if a given conversation id exists locally
505    pub async fn conversation_exists(&self, id: &ConversationId) -> Result<bool> {
506        Ok(self
507            .mls_groups()
508            .await
509            .map_err(RecursiveError::root("getting mls groups"))?
510            .get_fetch(
511                id,
512                &self
513                    .mls_provider()
514                    .await
515                    .map_err(RecursiveError::root("getting mls provider"))?
516                    .keystore(),
517                None,
518            )
519            .await
520            .ok()
521            .flatten()
522            .is_some())
523    }
524
525    /// Generates a random byte array of the specified size
526    pub async fn random_bytes(&self, len: usize) -> Result<Vec<u8>> {
527        use openmls_traits::random::OpenMlsRand as _;
528        self.mls_provider()
529            .await
530            .map_err(RecursiveError::root("getting mls provider"))?
531            .rand()
532            .random_vec(len)
533            .map_err(MlsError::wrap("generating random vector"))
534            .map_err(Into::into)
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use wasm_bindgen_test::*;
541
542    use crate::prelude::{CertificateBundle, ClientIdentifier, INITIAL_KEYING_MATERIAL_COUNT, MlsCredentialType};
543    use crate::{
544        CoreCrypto,
545        mls::{MlsCentral, MlsCentralConfiguration},
546        test_utils::{x509::X509TestChain, *},
547    };
548
549    wasm_bindgen_test_configure!(run_in_browser);
550
551    mod conversation_epoch {
552        use super::*;
553        use crate::mls::conversation::Conversation as _;
554
555        #[apply(all_cred_cipher)]
556        #[wasm_bindgen_test]
557        async fn can_get_newly_created_conversation_epoch(case: TestCase) {
558            run_test_with_central(case.clone(), move |[central]| {
559                Box::pin(async move {
560                    let id = conversation_id();
561                    central
562                        .context
563                        .new_conversation(&id, case.credential_type, case.cfg.clone())
564                        .await
565                        .unwrap();
566                    let epoch = central.context.conversation(&id).await.unwrap().epoch().await;
567                    assert_eq!(epoch, 0);
568                })
569            })
570            .await;
571        }
572
573        #[apply(all_cred_cipher)]
574        #[wasm_bindgen_test]
575        async fn can_get_conversation_epoch(case: TestCase) {
576            run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
577                Box::pin(async move {
578                    let id = conversation_id();
579                    alice_central
580                        .context
581                        .new_conversation(&id, case.credential_type, case.cfg.clone())
582                        .await
583                        .unwrap();
584                    alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
585                    let epoch = alice_central.context.conversation(&id).await.unwrap().epoch().await;
586                    assert_eq!(epoch, 1);
587                })
588            })
589            .await;
590        }
591
592        #[apply(all_cred_cipher)]
593        #[wasm_bindgen_test]
594        async fn conversation_not_found(case: TestCase) {
595            use crate::{LeafError, mls};
596
597            run_test_with_central(case.clone(), move |[central]| {
598                Box::pin(async move {
599                    let id = conversation_id();
600                    let err = central.context.conversation(&id).await.unwrap_err();
601                    assert!(matches!(
602                        err,
603                        mls::conversation::Error::Leaf(LeafError::ConversationNotFound(i)) if i == id
604                    ));
605                })
606            })
607            .await;
608        }
609    }
610
611    mod invariants {
612        use crate::{mls, prelude::MlsCiphersuite};
613
614        use super::*;
615
616        #[apply(all_cred_cipher)]
617        #[wasm_bindgen_test]
618        async fn can_create_from_valid_configuration(case: TestCase) {
619            run_tests(move |[tmp_dir_argument]| {
620                Box::pin(async move {
621                    let configuration = MlsCentralConfiguration::try_new(
622                        tmp_dir_argument,
623                        "test".to_string(),
624                        Some("alice".into()),
625                        vec![case.ciphersuite()],
626                        None,
627                        Some(INITIAL_KEYING_MATERIAL_COUNT),
628                    )
629                    .unwrap();
630
631                    let central = MlsCentral::try_new(configuration).await;
632                    assert!(central.is_ok())
633                })
634            })
635            .await
636        }
637
638        #[test]
639        #[wasm_bindgen_test]
640        fn store_path_should_not_be_empty_nor_blank() {
641            let ciphersuites = vec![MlsCiphersuite::default()];
642            let configuration = MlsCentralConfiguration::try_new(
643                " ".to_string(),
644                "test".to_string(),
645                Some("alice".into()),
646                ciphersuites,
647                None,
648                Some(INITIAL_KEYING_MATERIAL_COUNT),
649            );
650            assert!(matches!(
651                configuration.unwrap_err(),
652                mls::Error::MalformedIdentifier("store_path")
653            ));
654        }
655
656        #[cfg_attr(not(target_family = "wasm"), async_std::test)]
657        #[wasm_bindgen_test]
658        async fn identity_key_should_not_be_empty_nor_blank() {
659            run_tests(|[tmp_dir_argument]| {
660                Box::pin(async move {
661                    let ciphersuites = vec![MlsCiphersuite::default()];
662                    let configuration = MlsCentralConfiguration::try_new(
663                        tmp_dir_argument,
664                        " ".to_string(),
665                        Some("alice".into()),
666                        ciphersuites,
667                        None,
668                        Some(INITIAL_KEYING_MATERIAL_COUNT),
669                    );
670                    assert!(matches!(
671                        configuration.unwrap_err(),
672                        mls::Error::MalformedIdentifier("identity_key")
673                    ));
674                })
675            })
676            .await
677        }
678
679        #[cfg_attr(not(target_family = "wasm"), async_std::test)]
680        #[wasm_bindgen_test]
681        async fn client_id_should_not_be_empty() {
682            run_tests(|[tmp_dir_argument]| {
683                Box::pin(async move {
684                    let ciphersuites = vec![MlsCiphersuite::default()];
685                    let configuration = MlsCentralConfiguration::try_new(
686                        tmp_dir_argument,
687                        "test".to_string(),
688                        Some("".into()),
689                        ciphersuites,
690                        None,
691                        Some(INITIAL_KEYING_MATERIAL_COUNT),
692                    );
693                    assert!(matches!(
694                        configuration.unwrap_err(),
695                        mls::Error::MalformedIdentifier("client_id")
696                    ));
697                })
698            })
699            .await
700        }
701    }
702
703    #[apply(all_cred_cipher)]
704    #[wasm_bindgen_test]
705    async fn create_conversation_should_fail_when_already_exists(case: TestCase) {
706        use crate::{LeafError, mls};
707
708        run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
709            Box::pin(async move {
710                let id = conversation_id();
711
712                let create = alice_central
713                    .context
714                    .new_conversation(&id, case.credential_type, case.cfg.clone())
715                    .await;
716                assert!(create.is_ok());
717
718                // creating a conversation should first verify that the conversation does not already exist ; only then create it
719                let repeat_create = alice_central
720                    .context
721                    .new_conversation(&id, case.credential_type, case.cfg.clone())
722                    .await;
723                assert!(matches!(repeat_create.unwrap_err(), mls::Error::Leaf(LeafError::ConversationAlreadyExists(i)) if i == id));
724            })
725        })
726        .await;
727    }
728
729    #[apply(all_cred_cipher)]
730    #[wasm_bindgen_test]
731    async fn can_fetch_client_public_key(case: TestCase) {
732        run_tests(move |[tmp_dir_argument]| {
733            Box::pin(async move {
734                let configuration = MlsCentralConfiguration::try_new(
735                    tmp_dir_argument,
736                    "test".to_string(),
737                    Some("potato".into()),
738                    vec![case.ciphersuite()],
739                    None,
740                    Some(INITIAL_KEYING_MATERIAL_COUNT),
741                )
742                .unwrap();
743
744                let result = MlsCentral::try_new(configuration.clone()).await;
745                assert!(result.is_ok());
746            })
747        })
748        .await
749    }
750
751    #[apply(all_cred_cipher)]
752    #[wasm_bindgen_test]
753    async fn can_2_phase_init_central(case: TestCase) {
754        run_tests(move |[tmp_dir_argument]| {
755            Box::pin(async move {
756                let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
757                let configuration = MlsCentralConfiguration::try_new(
758                    tmp_dir_argument,
759                    "test".to_string(),
760                    None,
761                    vec![case.ciphersuite()],
762                    None,
763                    Some(INITIAL_KEYING_MATERIAL_COUNT),
764                )
765                .unwrap();
766                // phase 1: init without mls_client
767                let central = MlsCentral::try_new(configuration).await.unwrap();
768                let cc = CoreCrypto::from(central);
769                let context = cc.new_transaction().await.unwrap();
770                x509_test_chain.register_with_central(&context).await;
771
772                assert!(!context.mls_client().await.unwrap().is_ready().await);
773                // phase 2: init mls_client
774                let client_id = "alice";
775                let identifier = match case.credential_type {
776                    MlsCredentialType::Basic => ClientIdentifier::Basic(client_id.into()),
777                    MlsCredentialType::X509 => {
778                        CertificateBundle::rand_identifier(client_id, &[x509_test_chain.find_local_intermediate_ca()])
779                    }
780                };
781                context
782                    .mls_init(
783                        identifier,
784                        vec![case.ciphersuite()],
785                        Some(INITIAL_KEYING_MATERIAL_COUNT),
786                    )
787                    .await
788                    .unwrap();
789                assert!(context.mls_client().await.unwrap().is_ready().await);
790                // expect mls_client to work
791                assert_eq!(
792                    context
793                        .get_or_create_client_keypackages(case.ciphersuite(), case.credential_type, 2)
794                        .await
795                        .unwrap()
796                        .len(),
797                    2
798                );
799            })
800        })
801        .await
802    }
803}