core_crypto/mls/session/
key_package.rs

1use std::collections::{HashMap, HashSet};
2
3use core_crypto_keystore::{
4    connection::FetchFromDatabase,
5    entities::{EntityFindParams, StoredEncryptionKeyPair, StoredHpkePrivateKey, StoredKeypackage},
6};
7use mls_crypto_provider::{Database, MlsCryptoProvider};
8use openmls::prelude::{
9    Credential as MlsCredential, CredentialWithKey, CryptoConfig, KeyPackage, KeyPackageRef, Lifetime,
10};
11use openmls_traits::OpenMlsCryptoProvider;
12use tls_codec::{Deserialize, Serialize};
13
14use super::{Error, Result};
15use crate::{
16    Ciphersuite, Credential, CredentialType, KeystoreError, MlsConversationConfiguration, MlsError, Session,
17    mls::session::SessionInner,
18};
19
20/// Default number of KeyPackages a client generates the first time it's created
21#[cfg(not(test))]
22pub const INITIAL_KEYING_MATERIAL_COUNT: usize = 100;
23/// Default number of KeyPackages a client generates the first time it's created
24#[cfg(test)]
25pub const INITIAL_KEYING_MATERIAL_COUNT: usize = 10;
26
27/// Default lifetime of all generated KeyPackages. Matches the limit defined in openmls
28pub(crate) const KEYPACKAGE_DEFAULT_LIFETIME: std::time::Duration =
29    std::time::Duration::from_secs(60 * 60 * 24 * 28 * 3); // ~3 months
30
31impl Session {
32    /// Generates a single new keypackage
33    ///
34    /// # Arguments
35    /// * `backend` - the KeyStorage to load the keypackages from
36    ///
37    /// # Errors
38    /// KeyStore and OpenMls errors
39    pub async fn generate_one_keypackage_from_credential(
40        &self,
41        backend: &MlsCryptoProvider,
42        cs: Ciphersuite,
43        cb: &Credential,
44    ) -> Result<KeyPackage> {
45        let guard = self.inner.read().await;
46        let SessionInner {
47            keypackage_lifetime, ..
48        } = guard.as_ref().ok_or(Error::MlsNotInitialized)?;
49
50        let keypackage = KeyPackage::builder()
51            .leaf_node_capabilities(MlsConversationConfiguration::default_leaf_capabilities())
52            .key_package_lifetime(Lifetime::new(keypackage_lifetime.as_secs()))
53            .build(
54                CryptoConfig {
55                    ciphersuite: cs.into(),
56                    version: openmls::versions::ProtocolVersion::default(),
57                },
58                backend,
59                &cb.signature_key_pair,
60                CredentialWithKey {
61                    credential: cb.mls_credential.clone(),
62                    signature_key: cb.signature_key_pair.public().into(),
63                },
64            )
65            .await
66            .map_err(KeystoreError::wrap("building keypackage"))?;
67
68        Ok(keypackage)
69    }
70
71    /// Requests `count` keying material to be present and returns
72    /// a reference to it for the consumer to copy/clone.
73    ///
74    /// # Arguments
75    /// * `count` - number of [openmls::key_packages::KeyPackage] to generate
76    /// * `ciphersuite` - of [openmls::key_packages::KeyPackage] to generate
77    /// * `backend` - the KeyStorage to load the keypackages from
78    ///
79    /// # Errors
80    /// KeyStore and OpenMls errors
81    pub async fn request_key_packages(
82        &self,
83        count: usize,
84        ciphersuite: Ciphersuite,
85        credential_type: CredentialType,
86        backend: &MlsCryptoProvider,
87    ) -> Result<Vec<KeyPackage>> {
88        // Auto-prune expired keypackages on request
89        self.prune_keypackages(backend, std::iter::empty()).await?;
90        use core_crypto_keystore::CryptoKeystoreMls as _;
91
92        let mut existing_kps = backend
93            .key_store()
94            .mls_fetch_keypackages::<KeyPackage>(count as u32)
95            .await.map_err(KeystoreError::wrap("fetching mls keypackages"))?
96            .into_iter()
97            // TODO: do this filtering in SQL when the schema is updated. Tracking issue: WPB-9599
98            .filter(|kp|
99                ciphersuite == kp.ciphersuite()  && credential_type == kp.leaf_node().credential().credential_type() )
100            .collect::<Vec<_>>();
101
102        let kpb_count = existing_kps.len();
103        let mut kps = if count > kpb_count {
104            let to_generate = count - kpb_count;
105            let cb = self
106                .find_most_recent_credential(ciphersuite.signature_algorithm(), credential_type)
107                .await?;
108            self.generate_new_keypackages(backend, ciphersuite, &cb, to_generate)
109                .await?
110        } else {
111            vec![]
112        };
113
114        existing_kps.reverse();
115
116        kps.append(&mut existing_kps);
117        Ok(kps)
118    }
119
120    pub(crate) async fn generate_new_keypackages(
121        &self,
122        backend: &MlsCryptoProvider,
123        ciphersuite: Ciphersuite,
124        cb: &Credential,
125        count: usize,
126    ) -> Result<Vec<KeyPackage>> {
127        let mut kps = Vec::with_capacity(count);
128
129        for _ in 0..count {
130            let kp = self
131                .generate_one_keypackage_from_credential(backend, ciphersuite, cb)
132                .await?;
133            kps.push(kp);
134        }
135
136        Ok(kps)
137    }
138
139    /// Returns the count of valid, non-expired, unclaimed keypackages in store
140    pub async fn valid_keypackages_count(
141        &self,
142        backend: &MlsCryptoProvider,
143        ciphersuite: Ciphersuite,
144        credential_type: CredentialType,
145    ) -> Result<usize> {
146        let kps: Vec<StoredKeypackage> = backend
147            .key_store()
148            .find_all(EntityFindParams::default())
149            .await
150            .map_err(KeystoreError::wrap("finding all key packages"))?;
151
152        let mut valid_count = 0;
153        for kp in kps
154            .into_iter()
155            .map(|kp| core_crypto_keystore::deser::<KeyPackage>(&kp.keypackage))
156            // TODO: do this filtering in SQL when the schema is updated. Tracking issue: WPB-9599
157            .filter(|kp_result| {
158                 kp_result.as_ref().ok().is_none_or(|key_package| ciphersuite == key_package.ciphersuite() && credential_type == key_package.leaf_node().credential().credential_type())
159            })
160        {
161            let kp = kp.map_err(KeystoreError::wrap("counting valid keypackages"))?;
162            if !Self::is_mls_keypackage_expired(&kp) {
163                valid_count += 1;
164            }
165        }
166
167        Ok(valid_count)
168    }
169
170    /// Checks if a given OpenMLS [`KeyPackage`] is expired by looking through its extensions,
171    /// finding a lifetime extension and checking if it's valid.
172    fn is_mls_keypackage_expired(kp: &KeyPackage) -> bool {
173        let Some(lifetime) = kp.leaf_node().life_time() else {
174            return false;
175        };
176
177        !(lifetime.has_acceptable_range() && lifetime.is_valid())
178    }
179
180    /// Prune the provided KeyPackageRefs from the keystore
181    ///
182    /// Warning: Despite this API being public, the caller should know what they're doing.
183    /// Provided KeypackageRefs **will** be purged regardless of their expiration state, so please be wary of what you are doing if you directly call this API.
184    /// This could result in still valid, uploaded keypackages being pruned from the system and thus being impossible to find when referenced in a future Welcome message.
185    pub async fn prune_keypackages(
186        &self,
187        backend: &MlsCryptoProvider,
188        refs: impl IntoIterator<Item = KeyPackageRef>,
189    ) -> Result<()> {
190        let keystore = backend.keystore();
191        let kps = self.find_all_keypackages(&keystore).await?;
192        let _ = self._prune_keypackages(&kps, &keystore, refs).await?;
193        Ok(())
194    }
195
196    pub(crate) async fn prune_keypackages_and_credential(
197        &mut self,
198        backend: &MlsCryptoProvider,
199        refs: impl IntoIterator<Item = KeyPackageRef>,
200    ) -> Result<()> {
201        let mut guard = self.inner.write().await;
202        let SessionInner { identities, .. } = guard.as_mut().ok_or(Error::MlsNotInitialized)?;
203
204        let keystore = backend.key_store();
205        let kps = self.find_all_keypackages(keystore).await?;
206        let kp_to_delete = self._prune_keypackages(&kps, keystore, refs).await?;
207
208        // Let's group KeyPackages by Credential
209        let mut grouped_kps = HashMap::<Vec<u8>, Vec<KeyPackageRef>>::new();
210        for (_, kp) in &kps {
211            let cred = kp
212                .leaf_node()
213                .credential()
214                .tls_serialize_detached()
215                .map_err(Error::tls_serialize("keypackage"))?;
216            let kp_ref = kp
217                .hash_ref(backend.crypto())
218                .map_err(MlsError::wrap("computing keypackage hashref"))?;
219            grouped_kps.entry(cred).or_default().push(kp_ref);
220        }
221
222        for (credential, kps) in &grouped_kps {
223            // If all KeyPackages are to be deleted for this given Credential
224            let all_to_delete = kps.iter().all(|kpr| kp_to_delete.contains(&kpr.as_slice()));
225            if all_to_delete {
226                // then delete this Credential
227                backend
228                    .keystore()
229                    .cred_delete_by_credential(credential.clone())
230                    .await
231                    .map_err(KeystoreError::wrap("deleting credential"))?;
232                let credential = MlsCredential::tls_deserialize(&mut credential.as_slice())
233                    .map_err(Error::tls_deserialize("credential"))?;
234                identities.remove_by_mls_credential(&credential);
235            }
236        }
237
238        Ok(())
239    }
240
241    /// Deletes all expired KeyPackages plus the ones in `refs`. It also deletes all associated:
242    /// * HPKE private keys
243    /// * HPKE Encryption KeyPairs
244    /// * Signature KeyPairs & Credentials (use [Self::prune_keypackages_and_credential])
245    async fn _prune_keypackages<'a>(
246        &self,
247        kps: &'a [(StoredKeypackage, KeyPackage)],
248        keystore: &Database,
249        refs: impl IntoIterator<Item = KeyPackageRef>,
250    ) -> Result<HashSet<&'a [u8]>, Error> {
251        let refs = refs
252            .into_iter()
253            .map(|kp| {
254                // If `KeyPackageRef` implemented `Hash + PartialEq<Rhs=[u8]> + Eq`, then we could just check whether
255                // an arbitrary reference existed in the hashset without moving data here at all; the type could just
256                // be `HashSet<KeyPackageRef>`.
257                //
258                // If `KeyPackageRef` implemented `fn into_inner(self) -> Vec<u8>` then we could at least extract the
259                // data without copying.
260                //
261                // As things stand, we're stuck with some pointless copying of (usually short) data around.
262                // Hopefully LLVM is smart enough to optimize some of it away!
263                kp.as_slice().to_owned()
264            })
265            .collect::<HashSet<_>>();
266
267        let kp_to_delete = kps.iter().filter_map(|(store_kp, kp)| {
268            let is_expired = Self::is_mls_keypackage_expired(kp);
269            let to_delete = is_expired || refs.contains(store_kp.keypackage_ref.as_slice());
270            to_delete.then_some((kp, &store_kp.keypackage_ref))
271        });
272
273        // note: we're cloning the iterator here, not the data
274        for (kp, kp_ref) in kp_to_delete.clone() {
275            keystore
276                .remove::<StoredKeypackage, &[u8]>(kp_ref.as_slice())
277                .await
278                .map_err(KeystoreError::wrap("removing key package from keystore"))?;
279            keystore
280                .remove::<StoredHpkePrivateKey, &[u8]>(kp.hpke_init_key().as_slice())
281                .await
282                .map_err(KeystoreError::wrap("removing private key from keystore"))?;
283            keystore
284                .remove::<StoredEncryptionKeyPair, &[u8]>(kp.leaf_node().encryption_key().as_slice())
285                .await
286                .map_err(KeystoreError::wrap("removing encryption keypair from keystore"))?;
287        }
288
289        Ok(kp_to_delete.map(|(_, kpref)| kpref.as_slice()).collect())
290    }
291
292    pub(super) async fn find_all_keypackages(
293        &self,
294        keystore: &Database,
295    ) -> Result<Vec<(StoredKeypackage, KeyPackage)>> {
296        let kps: Vec<StoredKeypackage> = keystore
297            .find_all(EntityFindParams::default())
298            .await
299            .map_err(KeystoreError::wrap("finding all keypackages"))?;
300
301        let kps = kps
302            .into_iter()
303            .map(|raw_kp| -> Result<_> {
304                let kp = core_crypto_keystore::deser::<KeyPackage>(&raw_kp.keypackage)
305                    .map_err(KeystoreError::wrap("deserializing keypackage"))?;
306                Ok((raw_kp, kp))
307            })
308            .collect::<Result<Vec<_>, _>>()?;
309
310        Ok(kps)
311    }
312
313    /// Allows to set the current default keypackage lifetime extension duration.
314    /// It will be embedded in the [openmls::key_packages::KeyPackage]'s [openmls::extensions::LifetimeExtension]
315    #[cfg(test)]
316    pub async fn set_keypackage_lifetime(&self, duration: std::time::Duration) -> Result<()> {
317        match &mut *self.inner.write().await {
318            None => Err(Error::MlsNotInitialized),
319            Some(SessionInner {
320                keypackage_lifetime, ..
321            }) => {
322                *keypackage_lifetime = duration;
323                Ok(())
324            }
325        }
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use core_crypto_keystore::{ConnectionType, DatabaseKey};
332    use mls_crypto_provider::{Database, MlsCryptoProvider};
333    use openmls::prelude::{KeyPackage, KeyPackageIn, KeyPackageRef, ProtocolVersion};
334    use openmls_traits::{OpenMlsCryptoProvider, types::VerifiableCiphersuite};
335
336    use super::Session;
337    use crate::{
338        MlsConversationConfiguration,
339        e2e_identity::enrollment::test_utils::{e2ei_enrollment, init_activation_or_rotation, noop_restore},
340        test_utils::*,
341    };
342
343    #[apply(all_cred_cipher)]
344    async fn can_assess_keypackage_expiration(case: TestContext) {
345        let [session_context] = case.sessions().await;
346        let (cs, ct) = (case.ciphersuite(), case.credential_type);
347        let key = DatabaseKey::generate();
348        let database = Database::open(ConnectionType::InMemory, &key).await.unwrap();
349        let backend = MlsCryptoProvider::new(database);
350        let x509_test_chain = if case.is_x509() {
351            let x509_test_chain = crate::test_utils::x509::X509TestChain::init_empty(case.signature_scheme());
352            x509_test_chain.register_with_provider(&backend).await;
353            Some(x509_test_chain)
354        } else {
355            None
356        };
357
358        backend.new_transaction().await.unwrap();
359        let session = session_context.session;
360        session
361            .random_generate(
362                &case,
363                x509_test_chain.as_ref().map(|chain| chain.find_local_intermediate_ca()),
364            )
365            .await
366            .unwrap();
367
368        // 90-day standard expiration
369        let kp_std_exp = session.generate_one_keypackage(&backend, cs, ct).await.unwrap();
370        assert!(!Session::is_mls_keypackage_expired(&kp_std_exp));
371
372        // 1-second expiration
373        session
374            .set_keypackage_lifetime(std::time::Duration::from_secs(1))
375            .await
376            .unwrap();
377        let kp_1s_exp = session.generate_one_keypackage(&backend, cs, ct).await.unwrap();
378        // Sleep 2 seconds to make sure we make the kp expire
379        smol::Timer::after(std::time::Duration::from_secs(2)).await;
380        assert!(Session::is_mls_keypackage_expired(&kp_1s_exp));
381    }
382
383    #[apply(all_cred_cipher)]
384    async fn requesting_x509_key_packages_after_basic(case: TestContext) {
385        // Basic test case
386        if !case.is_basic() {
387            return;
388        }
389
390        let [session_context] = case.sessions_basic_with_pki_env().await;
391        Box::pin(async move {
392            let signature_scheme = case.signature_scheme();
393            let cipher_suite = case.ciphersuite();
394
395            // Generate 5 Basic key packages first
396            let _basic_key_packages = session_context
397                .transaction
398                .get_or_create_client_keypackages(cipher_suite, CredentialType::Basic, 5)
399                .await
400                .unwrap();
401
402            // Set up E2E identity
403            let test_chain = session_context.x509_chain_unchecked();
404
405            let (mut enrollment, cert_chain) = e2ei_enrollment(
406                &session_context,
407                &case,
408                test_chain,
409                None,
410                false,
411                init_activation_or_rotation,
412                noop_restore,
413            )
414            .await
415            .unwrap();
416
417            let _rotate_bundle = session_context
418                .transaction
419                .save_x509_credential(&mut enrollment, cert_chain)
420                .await
421                .unwrap();
422
423            // E2E identity has been set up correctly
424            assert!(
425                session_context
426                    .transaction
427                    .e2ei_is_enabled(signature_scheme)
428                    .await
429                    .unwrap()
430            );
431
432            // Request X509 key packages
433            let x509_key_packages = session_context
434                .transaction
435                .get_or_create_client_keypackages(cipher_suite, CredentialType::X509, 5)
436                .await
437                .unwrap();
438
439            // Verify that the key packages are X509
440            assert!(
441                x509_key_packages
442                    .iter()
443                    .all(|kp| CredentialType::X509 == kp.leaf_node().credential().credential_type())
444            );
445        })
446        .await
447    }
448
449    #[apply(all_cred_cipher)]
450    async fn generates_correct_number_of_kpbs(case: TestContext) {
451        let [cc] = case.sessions().await;
452        Box::pin(async move {
453            const N: usize = 2;
454            const COUNT: usize = 109;
455
456            let init = cc.transaction.count_entities().await;
457            assert_eq!(init.key_package, 0);
458            assert_eq!(init.encryption_keypair, 0);
459            assert_eq!(init.hpke_private_key, 0);
460            assert_eq!(init.credential, 1);
461
462            // since 'delete_keypackages' will evict all Credentials unlinked to a KeyPackage, each iteration
463            // generates 1 extra KeyPackage in order for this Credential no to be evicted and next iteration sto succeed.
464            let transactional_provider = cc.transaction.mls_provider().await.unwrap();
465            let crypto_provider = transactional_provider.crypto();
466            let mut pinned_kp = None;
467
468            let mut prev_kps: Option<Vec<KeyPackage>> = None;
469            for _ in 0..N {
470                let mut kps = cc
471                    .transaction
472                    .get_or_create_client_keypackages(case.ciphersuite(), case.credential_type, COUNT + 1)
473                    .await
474                    .unwrap();
475
476                // this will always be the same, first KeyPackage
477                pinned_kp = Some(kps.pop().unwrap());
478
479                assert_eq!(kps.len(), COUNT);
480                let after_creation = cc.transaction.count_entities().await;
481                assert_eq!(after_creation.key_package, COUNT + 1);
482                assert_eq!(after_creation.encryption_keypair, COUNT + 1);
483                assert_eq!(after_creation.hpke_private_key, COUNT + 1);
484                assert_eq!(after_creation.credential, 1);
485
486                let kpbs_refs = kps
487                    .iter()
488                    .map(|kp| kp.hash_ref(crypto_provider).unwrap())
489                    .collect::<Vec<KeyPackageRef>>();
490
491                if let Some(pkpbs) = prev_kps.replace(kps) {
492                    let pkpbs_refs = pkpbs
493                        .into_iter()
494                        .map(|kpb| kpb.hash_ref(crypto_provider).unwrap())
495                        .collect::<Vec<KeyPackageRef>>();
496
497                    let has_duplicates = kpbs_refs.iter().any(|href| pkpbs_refs.contains(href));
498                    // Make sure we have no previous keypackages found (that were pruned) in our new batch of KPs
499                    assert!(!has_duplicates);
500                }
501                cc.transaction.delete_keypackages(kpbs_refs).await.unwrap();
502            }
503
504            let count = cc
505                .transaction
506                .client_valid_key_packages_count(case.ciphersuite(), case.credential_type)
507                .await
508                .unwrap();
509            assert_eq!(count, 1);
510
511            let pinned_kpr = pinned_kp.unwrap().hash_ref(crypto_provider).unwrap();
512            cc.transaction.delete_keypackages([pinned_kpr]).await.unwrap();
513            let count = cc
514                .transaction
515                .client_valid_key_packages_count(case.ciphersuite(), case.credential_type)
516                .await
517                .unwrap();
518            assert_eq!(count, 0);
519            let after_delete = cc.transaction.count_entities().await;
520            assert_eq!(after_delete.key_package, 0);
521            assert_eq!(after_delete.encryption_keypair, 0);
522            assert_eq!(after_delete.hpke_private_key, 0);
523            assert_eq!(after_delete.credential, 0);
524        })
525        .await
526    }
527
528    #[apply(all_cred_cipher)]
529    async fn automatically_prunes_lifetime_expired_keypackages(case: TestContext) {
530        let [session_context] = case.sessions().await;
531        const UNEXPIRED_COUNT: usize = 125;
532        const EXPIRED_COUNT: usize = 200;
533        let key = DatabaseKey::generate();
534        let key_store = Database::open(ConnectionType::InMemory, &key).await.unwrap();
535        let backend = MlsCryptoProvider::new(key_store);
536        let x509_test_chain = if case.is_x509() {
537            let x509_test_chain = crate::test_utils::x509::X509TestChain::init_empty(case.signature_scheme());
538            x509_test_chain.register_with_provider(&backend).await;
539            Some(x509_test_chain)
540        } else {
541            None
542        };
543        backend.new_transaction().await.unwrap();
544        let session = session_context.session().await;
545        session
546            .random_generate(
547                &case,
548                x509_test_chain.as_ref().map(|chain| chain.find_local_intermediate_ca()),
549            )
550            .await
551            .unwrap();
552
553        // Generate `UNEXPIRED_COUNT` kpbs that are with default 3 months expiration. We *should* keep them for the duration of the test
554        let unexpired_kpbs = session
555            .request_key_packages(UNEXPIRED_COUNT, case.ciphersuite(), case.credential_type, &backend)
556            .await
557            .unwrap();
558        let len = session
559            .valid_keypackages_count(&backend, case.ciphersuite(), case.credential_type)
560            .await
561            .unwrap();
562        assert_eq!(len, unexpired_kpbs.len());
563        assert_eq!(len, UNEXPIRED_COUNT);
564
565        // Set the keypackage expiration to be in 2 seconds
566        session
567            .set_keypackage_lifetime(std::time::Duration::from_secs(10))
568            .await
569            .unwrap();
570
571        // Generate new keypackages that are normally partially expired 2s after they're requested
572        let partially_expired_kpbs = session
573            .request_key_packages(EXPIRED_COUNT, case.ciphersuite(), case.credential_type, &backend)
574            .await
575            .unwrap();
576        assert_eq!(partially_expired_kpbs.len(), EXPIRED_COUNT);
577
578        // Sleep to trigger the expiration
579        smol::Timer::after(std::time::Duration::from_secs(10)).await;
580
581        // Request the same number of keypackages. The automatic lifetime-based expiration should take
582        // place and remove old expired keypackages and generate fresh ones instead
583        let fresh_kpbs = session
584            .request_key_packages(EXPIRED_COUNT, case.ciphersuite(), case.credential_type, &backend)
585            .await
586            .unwrap();
587        let len = session
588            .valid_keypackages_count(&backend, case.ciphersuite(), case.credential_type)
589            .await
590            .unwrap();
591        assert_eq!(len, fresh_kpbs.len());
592        assert_eq!(len, EXPIRED_COUNT);
593
594        // Try to deep compare and find kps matching expired and non-expired ones
595        let (unexpired_match, expired_match) =
596            fresh_kpbs
597                .iter()
598                .fold((0usize, 0usize), |(mut unexpired_match, mut expired_match), fresh| {
599                    if unexpired_kpbs.iter().any(|kp| kp == fresh) {
600                        unexpired_match += 1;
601                    } else if partially_expired_kpbs.iter().any(|kpb| kpb == fresh) {
602                        expired_match += 1;
603                    }
604
605                    (unexpired_match, expired_match)
606                });
607
608        // TADA!
609        assert_eq!(unexpired_match, UNEXPIRED_COUNT);
610        assert_eq!(expired_match, 0);
611    }
612
613    #[apply(all_cred_cipher)]
614    async fn new_keypackage_has_correct_extensions(case: TestContext) {
615        let [cc] = case.sessions().await;
616        Box::pin(async move {
617            let kps = cc
618                .transaction
619                .get_or_create_client_keypackages(case.ciphersuite(), case.credential_type, 1)
620                .await
621                .unwrap();
622            let kp = kps.first().unwrap();
623
624            // make sure it's valid
625            let _ = KeyPackageIn::from(kp.clone())
626                .standalone_validate(
627                    &cc.transaction.mls_provider().await.unwrap(),
628                    ProtocolVersion::Mls10,
629                    true,
630                )
631                .await
632                .unwrap();
633
634            // see https://www.rfc-editor.org/rfc/rfc9420.html#section-10-10
635            assert!(kp.extensions().is_empty());
636
637            assert_eq!(kp.leaf_node().capabilities().versions(), &[ProtocolVersion::Mls10]);
638            assert_eq!(
639                kp.leaf_node().capabilities().ciphersuites().to_vec(),
640                MlsConversationConfiguration::DEFAULT_SUPPORTED_CIPHERSUITES
641                    .iter()
642                    .map(|c| VerifiableCiphersuite::from(*c))
643                    .collect::<Vec<_>>()
644            );
645            assert!(kp.leaf_node().capabilities().proposals().is_empty());
646            assert!(kp.leaf_node().capabilities().extensions().is_empty());
647            assert_eq!(
648                kp.leaf_node().capabilities().credentials(),
649                MlsConversationConfiguration::DEFAULT_SUPPORTED_CREDENTIALS
650            );
651        })
652        .await
653    }
654}