Skip to main content

openmls/group/mls_group/
builder.rs

1use openmls_traits::{crypto::OpenMlsCrypto, signatures::Signer, types::Ciphersuite};
2use tls_codec::Serialize;
3
4#[cfg(feature = "extensions-draft")]
5use crate::schedule::application_export_tree::ApplicationExportTree;
6use crate::{
7    binary_tree::{array_representation::TreeSize, LeafNodeIndex},
8    credentials::CredentialWithKey,
9    error::LibraryError,
10    extensions::Extensions,
11    group::{
12        config::PastEpochDeletionPolicy, past_secrets::MessageSecretsStore,
13        public_group::errors::PublicGroupBuildError, GroupContext, GroupId, MlsGroup,
14        MlsGroupCreateConfig, MlsGroupCreateConfigBuilder, MlsGroupState, NewGroupError,
15        PublicGroup, WireFormatPolicy,
16    },
17    key_packages::Lifetime,
18    schedule::{
19        psk::{load_psks, store::ResumptionPskStore, PskSecret},
20        EpochSecretsResult, InitSecret, JoinerSecret, KeySchedule, PreSharedKeyId,
21    },
22    storage::OpenMlsProvider,
23    tree::sender_ratchet::SenderRatchetConfiguration,
24    treesync::{
25        errors::LeafNodeValidationError,
26        node::leaf_node::{Capabilities, LeafNode},
27    },
28};
29
30/// Builder struct for an [`MlsGroup`].
31#[derive(Default, Debug)]
32pub struct MlsGroupBuilder {
33    group_id: Option<GroupId>,
34    mls_group_create_config_builder: MlsGroupCreateConfigBuilder,
35    replace_old_group: bool,
36    psk_ids: Vec<PreSharedKeyId>,
37    /// The emulation group to create this group as a virtual client of. The
38    /// derivation epoch is resolved from its state when [`Self::build`] runs.
39    #[cfg(feature = "virtual-clients-draft")]
40    vc_emulation_group_id: Option<GroupId>,
41}
42
43impl MlsGroupBuilder {
44    pub(super) fn new() -> Self {
45        Self::default()
46    }
47
48    /// Sets the group ID of the [`MlsGroup`].
49    pub fn with_group_id(mut self, group_id: GroupId) -> Self {
50        self.group_id = Some(group_id);
51        self
52    }
53
54    /// Create the group as a virtual client of the emulation group named by
55    /// `emulation_group_id`.
56    ///
57    /// The group is created from the newest derivation epoch of the emulation
58    /// group, which is what the draft requires of every new virtual-client
59    /// operation. The epoch is resolved when [`Self::build`] runs, against the
60    /// emulation group's state at that point.
61    ///
62    /// The creator's leaf is `key_package`-sourced and its key material is
63    /// derived from a fresh `key_package` operation secret of that epoch (so
64    /// sibling emulator clients can reconstruct it), and the epoch-0
65    /// `epoch_secret` is derived from the same KeyPackage seed rather than
66    /// transmitted. Siblings bootstrap into the group with
67    /// [`MlsGroup::vc_join_at_creation`].
68    ///
69    /// [`MlsGroup::vc_join_at_creation`]: crate::group::MlsGroup::vc_join_at_creation
70    #[cfg(feature = "virtual-clients-draft")]
71    pub fn vc_emulation(mut self, emulation_group_id: &GroupId) -> Self {
72        self.vc_emulation_group_id = Some(emulation_group_id.clone());
73        self
74    }
75
76    /// Instruct the builder to replace any existing group with the same ID.
77    pub fn replace_old_group(mut self) -> Self {
78        self.replace_old_group = true;
79        self
80    }
81
82    /// Build a new group as configured by this builder.
83    pub fn build<Provider: OpenMlsProvider>(
84        self,
85        provider: &Provider,
86        signer: &impl Signer,
87        credential_with_key: CredentialWithKey,
88    ) -> Result<MlsGroup, NewGroupError<Provider::StorageError>> {
89        self.build_internal(provider, signer, credential_with_key, None)
90    }
91
92    /// Build a new group with the given group ID.
93    ///
94    /// If an [`MlsGroupCreateConfig`] is provided, it will be used to configure the
95    /// group. Otherwise, the internal builder is used to build one with the
96    /// parameters set on this builder.
97    ///
98    /// If a group with the same ID already exists in storage and
99    /// `replace_old_group` was not set, an error will be returned.
100    pub(super) fn build_internal<Provider: OpenMlsProvider>(
101        self,
102        provider: &Provider,
103        signer: &impl Signer,
104        credential_with_key: CredentialWithKey,
105        mls_group_create_config_option: Option<MlsGroupCreateConfig>,
106    ) -> Result<MlsGroup, NewGroupError<Provider::StorageError>> {
107        let mls_group_create_config = mls_group_create_config_option
108            .unwrap_or_else(|| self.mls_group_create_config_builder.build());
109        let group_id = self
110            .group_id
111            .unwrap_or_else(|| GroupId::random(provider.rand()));
112        let ciphersuite = mls_group_create_config.ciphersuite;
113
114        provider
115            .crypto()
116            .supports(ciphersuite)
117            .map_err(|_| NewGroupError::UnsupportedCiphersuite(ciphersuite))?;
118
119        #[cfg(feature = "virtual-clients-draft")]
120        if let Some(emulation_group_id) = &self.vc_emulation_group_id {
121            let epoch_id =
122                crate::components::vc_derivation_info::require_newest_vc_derivation_epoch(
123                    provider.storage(),
124                    emulation_group_id,
125                )?;
126            return build_vc_internal(
127                provider,
128                signer,
129                credential_with_key,
130                mls_group_create_config,
131                group_id,
132                self.replace_old_group,
133                epoch_id,
134            );
135        }
136
137        if !self.replace_old_group
138            && MlsGroup::load(provider.storage(), &group_id)
139                .map_err(NewGroupError::StorageError)?
140                .is_some()
141        {
142            return Err(NewGroupError::GroupAlreadyExists);
143        }
144
145        let (public_group_builder, commit_secret, leaf_keypair) =
146            PublicGroup::builder(group_id, ciphersuite, credential_with_key)
147                .with_group_context_extensions(
148                    mls_group_create_config.group_context_extensions.clone(),
149                )
150                .with_leaf_node_extensions(mls_group_create_config.leaf_node_extensions.clone())
151                .with_lifetime(*mls_group_create_config.lifetime())
152                .with_capabilities(mls_group_create_config.capabilities.clone())
153                .get_secrets(provider, signer)
154                .map_err(|e| match e {
155                    PublicGroupBuildError::LibraryError(e) => NewGroupError::LibraryError(e),
156                    PublicGroupBuildError::InvalidExtensions(e) => e.into(),
157                })?;
158
159        let serialized_group_context = public_group_builder
160            .group_context()
161            .tls_serialize_detached()
162            .map_err(LibraryError::missing_bound_check)?;
163
164        // Derive an initial joiner secret based on the commit secret.
165        // Derive an epoch secret from the joiner secret.
166        // We use a random `InitSecret` for initialization.
167        let joiner_secret = JoinerSecret::new(
168            provider.crypto(),
169            ciphersuite,
170            commit_secret,
171            &InitSecret::random(ciphersuite, provider.rand())
172                .map_err(LibraryError::unexpected_crypto_error)?,
173            &serialized_group_context,
174        )
175        .map_err(LibraryError::unexpected_crypto_error)?;
176
177        // TODO(#1357)
178        let mut resumption_psk_store = ResumptionPskStore::new(32);
179
180        // Prepare the PskSecret
181        let psk_secret = load_psks(provider.storage(), &resumption_psk_store, &self.psk_ids)
182            .and_then(|psks| PskSecret::new(provider.crypto(), ciphersuite, psks))
183            .map_err(|e| {
184                log::debug!("Unexpected PSK error: {e:?}");
185                LibraryError::custom("Unexpected PSK error")
186            })?;
187
188        let mut key_schedule =
189            KeySchedule::init(ciphersuite, provider.crypto(), &joiner_secret, psk_secret)?;
190        key_schedule
191            .add_context(provider.crypto(), &serialized_group_context)
192            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
193
194        let EpochSecretsResult {
195            epoch_secrets,
196            #[cfg(feature = "extensions-draft")]
197            application_exporter,
198        } = key_schedule
199            .epoch_secrets(provider.crypto(), ciphersuite)
200            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
201
202        let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
203            serialized_group_context,
204            TreeSize::new(1),
205            LeafNodeIndex::new(0u32),
206        );
207
208        let initial_confirmation_tag = message_secrets
209            .confirmation_key()
210            .tag(provider.crypto(), ciphersuite, &[])
211            .map_err(LibraryError::unexpected_crypto_error)?;
212
213        let message_secrets_store = MessageSecretsStore::new_with_secret(
214            mls_group_create_config
215                .join_config
216                .past_epoch_deletion_policy(),
217            message_secrets,
218        );
219
220        let public_group = public_group_builder
221            .with_confirmation_tag(initial_confirmation_tag)
222            .build(provider.crypto())?;
223
224        // We already add a resumption PSK for epoch 0 to make things more unified.
225        let resumption_psk = group_epoch_secrets.resumption_psk();
226        resumption_psk_store.add(public_group.group_context().epoch(), resumption_psk.clone());
227
228        #[cfg(feature = "extensions-draft")]
229        #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
230        let mut application_export_tree = ApplicationExportTree::new(application_exporter);
231
232        // The initial epoch of an emulation group is a derivation epoch.
233        #[cfg(feature = "virtual-clients-draft")]
234        if mls_group_create_config.emulation_group {
235            crate::components::vc_derivation_info::register_vc_derivation_epoch(
236                provider.crypto(),
237                provider.storage(),
238                Some(&mut application_export_tree),
239                crate::components::vc_derivation_info::VcDerivationEpochParams::for_public_group(
240                    &public_group,
241                    LeafNodeIndex::new(0),
242                ),
243            )?;
244        }
245
246        let mls_group = MlsGroup {
247            mls_group_config: mls_group_create_config.join_config.clone(),
248            own_leaf_nodes: vec![],
249            aad: vec![],
250            #[cfg(feature = "extensions-draft")]
251            safe_aad: crate::framing::SafeAad::empty(),
252            group_state: MlsGroupState::Operational,
253            public_group,
254            group_epoch_secrets,
255            own_leaf_index: LeafNodeIndex::new(0),
256            message_secrets_store,
257            resumption_psk_store,
258            #[cfg(feature = "extensions-draft")]
259            application_export_tree: Some(application_export_tree),
260            #[cfg(feature = "virtual-clients-draft")]
261            emulation_group: mls_group_create_config.emulation_group,
262        };
263
264        mls_group
265            .store(provider.storage())
266            .map_err(NewGroupError::StorageError)?;
267        mls_group
268            .store_epoch_keypairs(provider.storage(), &[leaf_keypair])
269            .map_err(NewGroupError::StorageError)?;
270
271        Ok(mls_group)
272    }
273
274    // MlsGroupCreateConfigBuilder options
275
276    /// Sets the `wire_format` property of the MlsGroup.
277    pub fn with_wire_format_policy(mut self, wire_format_policy: WireFormatPolicy) -> Self {
278        self.mls_group_create_config_builder = self
279            .mls_group_create_config_builder
280            .wire_format_policy(wire_format_policy);
281        self
282    }
283
284    /// Sets the `padding_size` property of the MlsGroup.
285    pub fn padding_size(mut self, padding_size: usize) -> Self {
286        self.mls_group_create_config_builder = self
287            .mls_group_create_config_builder
288            .padding_size(padding_size);
289        self
290    }
291
292    /// Sets the `max_past_epochs` property of the MlsGroup.
293    /// This allows application messages from previous epochs to be decrypted.
294    ///
295    /// This method overrides the policy set by [`Self::set_past_epoch_deletion_policy()`],
296    /// and is equivalent to setting the past epoch deletion policy to
297    /// `PastEpochDeletionPolicy::MaxEpochs(max_past_epochs)`.
298    ///
299    /// **WARNING**
300    ///
301    ///
302    /// This feature enables the storage of message secrets from past epochs.
303    /// It is a trade-off between functionality and forward secrecy and should only be enabled
304    /// if the Delivery Service cannot guarantee that application messages will be sent in
305    /// the same epoch in which they were generated. The number for `max_epochs` should be
306    /// as low as possible.
307    ///
308    /// NOTE: This function will be deprecated in future releases.
309    pub fn max_past_epochs(mut self, max_past_epochs: usize) -> Self {
310        self.mls_group_create_config_builder = self
311            .mls_group_create_config_builder
312            .max_past_epochs(max_past_epochs);
313        self
314    }
315
316    /// Set the policy for deleting past epoch secrets.
317    ///
318    /// By default, storage of past epoch secrets is disabled.
319    ///
320    /// This method overrides the configuration set by [`Self::max_past_epochs()`].
321    ///
322    /// **WARNING**
323    ///
324    /// This feature enables the storage of message secrets from past epochs.
325    /// It is a trade-off between functionality and forward secrecy and should only be enabled
326    /// if the Delivery Service cannot guarantee that application messages will be sent in
327    /// the same epoch in which they were generated. The number for `max_epochs` should be
328    /// as low as possible.
329    pub fn set_past_epoch_deletion_policy(mut self, policy: PastEpochDeletionPolicy) -> Self {
330        self.mls_group_create_config_builder = self
331            .mls_group_create_config_builder
332            .set_past_epoch_deletion_policy(policy);
333        self
334    }
335
336    /// Sets the `number_of_resumption_psks` property of the MlsGroup.
337    pub fn number_of_resumption_psks(mut self, number_of_resumption_psks: usize) -> Self {
338        self.mls_group_create_config_builder = self
339            .mls_group_create_config_builder
340            .number_of_resumption_psks(number_of_resumption_psks);
341        self
342    }
343
344    /// Sets the `use_ratchet_tree_extension` property of the MlsGroup.
345    pub fn use_ratchet_tree_extension(mut self, use_ratchet_tree_extension: bool) -> Self {
346        self.mls_group_create_config_builder = self
347            .mls_group_create_config_builder
348            .use_ratchet_tree_extension(use_ratchet_tree_extension);
349        self
350    }
351
352    /// Sets the `sender_ratchet_configuration` property of the MlsGroup.
353    /// See [`SenderRatchetConfiguration`] for more information.
354    pub fn sender_ratchet_configuration(
355        mut self,
356        sender_ratchet_configuration: SenderRatchetConfiguration,
357    ) -> Self {
358        self.mls_group_create_config_builder = self
359            .mls_group_create_config_builder
360            .sender_ratchet_configuration(sender_ratchet_configuration);
361        self
362    }
363
364    /// Sets the `lifetime` of the group creator's leaf.
365    pub fn lifetime(mut self, lifetime: Lifetime) -> Self {
366        self.mls_group_create_config_builder =
367            self.mls_group_create_config_builder.lifetime(lifetime);
368        self
369    }
370
371    /// Sets the `ciphersuite` of the MlsGroup.
372    pub fn ciphersuite(mut self, ciphersuite: Ciphersuite) -> Self {
373        self.mls_group_create_config_builder = self
374            .mls_group_create_config_builder
375            .ciphersuite(ciphersuite);
376        self
377    }
378
379    /// Sets the initial group context extensions
380    pub fn with_group_context_extensions(mut self, extensions: Extensions<GroupContext>) -> Self {
381        self.mls_group_create_config_builder = self
382            .mls_group_create_config_builder
383            .with_group_context_extensions(extensions);
384        self
385    }
386
387    /// Sets the initial leaf node extensions
388    pub fn with_leaf_node_extensions(
389        mut self,
390        extensions: Extensions<LeafNode>,
391    ) -> Result<Self, LeafNodeValidationError> {
392        self.mls_group_create_config_builder = self
393            .mls_group_create_config_builder
394            .with_leaf_node_extensions(extensions)?;
395        Ok(self)
396    }
397
398    /// Sets the group creator's [`Capabilities`]
399    pub fn with_capabilities(mut self, capabilities: Capabilities) -> Self {
400        self.mls_group_create_config_builder = self
401            .mls_group_create_config_builder
402            .capabilities(capabilities);
403        self
404    }
405}
406
407/// Create a new group with the virtual client as the creator (epoch 0, single
408/// leaf).
409///
410/// The creator's leaf is `key_package`-sourced and its key material is derived
411/// from a fresh `key_package` operation secret of the derivation epoch
412/// `epoch_id` (batch index 0). The epoch-0 `epoch_secret` is derived from the
413/// same KeyPackage seed under the created group's ciphersuite. A sibling
414/// emulator client reconstructs this exact state with
415/// [`MlsGroup::vc_join_at_creation`]: it shares the operation secret tree, so it
416/// rederives the same seed and hence the same epoch secret without any secret
417/// travelling on the wire. Because the `epoch_secret` is derived rather than run
418/// through the joiner key schedule, this path bypasses it entirely.
419///
420/// [`MlsGroup::vc_join_at_creation`]: crate::group::MlsGroup::vc_join_at_creation
421#[cfg(feature = "virtual-clients-draft")]
422#[allow(clippy::too_many_arguments)]
423fn build_vc_internal<Provider: OpenMlsProvider>(
424    provider: &Provider,
425    signer: &impl Signer,
426    credential_with_key: CredentialWithKey,
427    mls_group_create_config: MlsGroupCreateConfig,
428    group_id: GroupId,
429    replace_old_group: bool,
430    epoch_id: crate::components::vc_derivation_info::EpochId,
431) -> Result<MlsGroup, NewGroupError<Provider::StorageError>> {
432    use openmls_traits::storage::StorageProvider as _;
433
434    use crate::{
435        components::vc_derivation_info::{
436            load_vc_epoch_state_and_tree, DerivationInfo, DerivationInfoTbe,
437            VirtualClientOperationType, VirtualClientsError,
438        },
439        schedule::EpochSecrets,
440        treesync::TreeSync,
441    };
442
443    if !replace_old_group
444        && MlsGroup::load(provider.storage(), &group_id)
445            .map_err(NewGroupError::StorageError)?
446            .is_some()
447    {
448        return Err(NewGroupError::GroupAlreadyExists);
449    }
450
451    let ciphersuite = mls_group_create_config.ciphersuite;
452    let capabilities = mls_group_create_config.capabilities.clone();
453
454    // Validate that the creator's leaf declares `AppDataDictionary` and lists
455    // `VC_COMPONENT_ID` before allocating a generation, so a deterministic
456    // precondition failure does not burn an operation secret.
457    let resolved_dictionary = crate::components::vc_derivation_info::resolve_vc_leaf_dictionary(
458        Some(&capabilities),
459        Some(&mls_group_create_config.leaf_node_extensions),
460        None,
461    )?;
462
463    // Load the derivation epoch state and operation tree, allocate a fresh
464    // `key_package` generation (empty operation context, matching the KeyPackage
465    // batch path), and persist the advanced tree right away. A retried creation
466    // consumes a fresh generation.
467    let (state, mut operation_tree) = load_vc_epoch_state_and_tree(provider, &epoch_id)?;
468    let (emulation_leaf_index, epoch_encryption_key, emulation_ciphersuite) = state.into_parts();
469    let (generation, operation_secret) = operation_tree.next_operation_secret(
470        provider.crypto(),
471        emulation_ciphersuite,
472        &epoch_id,
473        emulation_leaf_index,
474        VirtualClientOperationType::KeyPackage,
475        b"",
476    )?;
477    provider
478        .storage()
479        .write_vc_operation_tree(&epoch_id, &operation_tree)
480        .map_err(NewGroupError::StorageError)?;
481
482    // The creator batch consists of this single derivation and is closed
483    // immediately: no `KeyPackageUpload` is sent and no
484    // `RetainedKeyPackageMaterial` is written, because a creator leaf has no
485    // KeyPackage / KeyPackageRef. Import the per-KeyPackage seed (index 0) under
486    // the created group's ciphersuite, then derive the creator leaf's encryption
487    // keypair and the epoch-0 secret from that seed.
488    let key_package_index = 0;
489    let key_package_seed = operation_secret.derive_key_package_seed_secret(
490        provider.crypto(),
491        ciphersuite,
492        key_package_index,
493    )?;
494    let leaf_encryption_keypair = key_package_seed
495        .derive_encryption_key_secret(provider.crypto(), ciphersuite)?
496        .generate_encryption_key_pair(provider.crypto(), ciphersuite)?;
497    let epoch_secret =
498        key_package_seed.derive_group_creation_secret(provider.crypto(), ciphersuite)?;
499
500    // Wrap the derivation info under the per-epoch AEAD key, bound to the leaf
501    // via its serialized encryption key.
502    let leaf_encryption_key = leaf_encryption_keypair
503        .public_key()
504        .tls_serialize_detached()
505        .map_err(VirtualClientsError::from)?;
506    let tbe = DerivationInfoTbe::KeyPackage {
507        leaf_index: emulation_leaf_index,
508        generation,
509        key_package_index,
510    };
511    let derivation_info = DerivationInfo::encrypt(
512        provider.crypto(),
513        emulation_ciphersuite,
514        &epoch_encryption_key,
515        epoch_id.clone(),
516        &leaf_encryption_key,
517        &tbe,
518    )?;
519    let derivation_info_bytes = derivation_info
520        .tls_serialize_detached()
521        .map_err(VirtualClientsError::from)?;
522    let leaf_extensions = crate::components::vc_derivation_info::merge_vc_derivation_info(
523        Some(&mls_group_create_config.leaf_node_extensions),
524        resolved_dictionary,
525        derivation_info_bytes,
526    )?;
527
528    // Build the single-leaf tree with the derived key_package-sourced leaf.
529    let (treesync, leaf_keypair) = TreeSync::new_vc(
530        provider,
531        signer,
532        ciphersuite,
533        credential_with_key,
534        *mls_group_create_config.lifetime(),
535        capabilities,
536        leaf_extensions,
537        leaf_encryption_keypair,
538    )?;
539    let group_context = GroupContext::create_initial_group_context(
540        ciphersuite,
541        group_id.clone(),
542        treesync.tree_hash().to_vec(),
543        mls_group_create_config.group_context_extensions.clone(),
544    );
545    let serialized_group_context = group_context
546        .tls_serialize_detached()
547        .map_err(LibraryError::missing_bound_check)?;
548
549    // Derive epoch-0 secrets from the epoch secret derived above.
550    let epoch_secrets =
551        EpochSecrets::from_epoch_secret(provider.crypto(), ciphersuite, epoch_secret)
552            .map_err(LibraryError::unexpected_crypto_error)?;
553    let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
554        serialized_group_context,
555        TreeSize::new(1),
556        LeafNodeIndex::new(0),
557    );
558    let initial_confirmation_tag = message_secrets
559        .confirmation_key()
560        .tag(provider.crypto(), ciphersuite, &[])
561        .map_err(LibraryError::unexpected_crypto_error)?;
562    let message_secrets_store = MessageSecretsStore::new_with_secret(
563        mls_group_create_config
564            .join_config
565            .past_epoch_deletion_policy(),
566        message_secrets,
567    );
568    let public_group = PublicGroup::new(
569        provider.crypto(),
570        treesync,
571        group_context,
572        initial_confirmation_tag,
573    )?;
574
575    let mut resumption_psk_store = ResumptionPskStore::new(32);
576    let resumption_psk = group_epoch_secrets.resumption_psk();
577    resumption_psk_store.add(public_group.group_context().epoch(), resumption_psk.clone());
578
579    let mls_group = MlsGroup {
580        mls_group_config: mls_group_create_config.join_config.clone(),
581        own_leaf_nodes: vec![],
582        aad: vec![],
583        safe_aad: crate::framing::SafeAad::empty(),
584        group_state: MlsGroupState::Operational,
585        public_group,
586        group_epoch_secrets,
587        own_leaf_index: LeafNodeIndex::new(0),
588        message_secrets_store,
589        resumption_psk_store,
590        // Reconstructed VC groups do not populate the application export tree,
591        // matching the other VC group-entry paths.
592        application_export_tree: None,
593        // A group a virtual client creates is not itself an emulation group.
594        emulation_group: false,
595    };
596
597    // Bind epoch 0 of the new group to the derivation epoch so later VC
598    // operations in this group resolve the right derivation epoch state.
599    // Written before the group itself, so an error between the writes cannot
600    // leave a loadable group without a binding (a bound group is required for
601    // the reuse-guard MUST).
602    let mut bindings: crate::components::vc_derivation_info::VcEmulationBindings = provider
603        .storage()
604        .vc_emulation_bindings(&group_id)
605        .map_err(NewGroupError::StorageError)?
606        .unwrap_or_default();
607    let max_entries = mls_group.message_secrets_store.max_epochs.saturating_add(1);
608    bindings.insert(mls_group.epoch(), epoch_id, max_entries);
609    provider
610        .storage()
611        .write_vc_emulation_bindings(&group_id, &bindings)
612        .map_err(NewGroupError::StorageError)?;
613
614    mls_group
615        .store(provider.storage())
616        .map_err(NewGroupError::StorageError)?;
617    mls_group
618        .store_epoch_keypairs(provider.storage(), &[leaf_keypair])
619        .map_err(NewGroupError::StorageError)?;
620
621    Ok(mls_group)
622}