core_crypto/mls/
mod.rs

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