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, BranchInfo, CommitBuilderStageError,
14        CommitMessageBundle, CreateCommitError, GroupContext, GroupId, MlsGroup,
15        MlsGroupCreateConfig, MlsGroupCreateConfigBuilder, MlsGroupState, NewGroupError,
16        PublicGroup, ReInitInfo, WireFormatPolicy,
17    },
18    key_packages::{KeyPackage, Lifetime},
19    schedule::{
20        psk::{load_psks, store::ResumptionPskStore, PskSecret},
21        EpochSecretsResult, InitSecret, JoinerSecret, KeySchedule, PreSharedKeyId,
22    },
23    storage::OpenMlsProvider,
24    tree::sender_ratchet::SenderRatchetConfiguration,
25    treesync::{
26        errors::LeafNodeValidationError,
27        node::leaf_node::{Capabilities, CapabilitiesPolicy, LeafNode},
28    },
29};
30
31#[cfg(feature = "virtual-clients-draft")]
32use crate::treesync::node::leaf_node::{resolve_capabilities, LeafNodeConstraints};
33
34/// Builder struct for an [`MlsGroup`].
35#[derive(Default, Debug)]
36pub struct MlsGroupBuilder {
37    group_id: Option<GroupId>,
38    mls_group_create_config_builder: MlsGroupCreateConfigBuilder,
39    replace_old_group: bool,
40    psk_ids: Vec<PreSharedKeyId>,
41    /// The emulation group to create this group as a virtual client of. The
42    /// derivation epoch is resolved from its state when [`Self::build`] runs.
43    #[cfg(feature = "virtual-clients-draft")]
44    vc_emulation_group_id: Option<GroupId>,
45}
46
47impl MlsGroupBuilder {
48    pub(super) fn new() -> Self {
49        Self::default()
50    }
51
52    /// Sets the group ID of the [`MlsGroup`].
53    pub fn with_group_id(mut self, group_id: GroupId) -> Self {
54        self.group_id = Some(group_id);
55        self
56    }
57
58    /// Create the group as a virtual client of the emulation group named by
59    /// `emulation_group_id`.
60    ///
61    /// The group is created from the newest derivation epoch of the emulation
62    /// group, which is what the draft requires of every new virtual-client
63    /// operation. The epoch is resolved when [`Self::build`] runs, against the
64    /// emulation group's state at that point.
65    ///
66    /// The creator's leaf is `key_package`-sourced and its key material is
67    /// derived from a fresh `key_package` operation secret of that epoch (so
68    /// sibling emulator clients can reconstruct it), and the epoch-0
69    /// `epoch_secret` is derived from the same KeyPackage seed rather than
70    /// transmitted. Siblings bootstrap into the group with
71    /// [`MlsGroup::vc_join_at_creation`].
72    ///
73    /// [`MlsGroup::vc_join_at_creation`]: crate::group::MlsGroup::vc_join_at_creation
74    #[cfg(feature = "virtual-clients-draft")]
75    pub fn vc_emulation(mut self, emulation_group_id: &GroupId) -> Self {
76        self.vc_emulation_group_id = Some(emulation_group_id.clone());
77        self
78    }
79
80    /// Instruct the builder to replace any existing group with the same ID.
81    pub fn replace_old_group(mut self) -> Self {
82        self.replace_old_group = true;
83        self
84    }
85
86    /// Turn this builder into a [`BranchGroupBuilder`] to build a sub-group
87    /// [RFC 9420 §11.3].
88    ///
89    /// The parent group's parameters are provided via `branch_info`, which the
90    /// parent exports with
91    /// [`MlsGroup::branch_info`](crate::group::MlsGroup::branch_info). The
92    /// sub-group is created with the parent's ciphersuite. Set any other group
93    /// configuration on this builder before calling `branch`, then create the
94    /// sub-group and its branch commit with
95    /// [`BranchGroupBuilder::build_branch`].
96    ///
97    /// [RFC 9420 §11.3]: https://www.rfc-editor.org/rfc/rfc9420.html#name-subgroup-branching
98    pub fn branch(self, branch_info: BranchInfo) -> BranchGroupBuilder {
99        BranchGroupBuilder {
100            group_builder: self,
101            branch_info,
102            extensions: None,
103            force_self_update: false,
104        }
105    }
106
107    /// Turn this builder into a [`ReInitGroupBuilder`] to build a reinitialized successor
108    /// group [RFC 9420 §11.2].
109    ///
110    /// The reinit parameters are provided via `reinit_info`, which the predecessor
111    /// exports with [`MlsGroup::reinit_info`](crate::group::MlsGroup::reinit_info).
112    /// The successor is created with the ReInit proposal's protocol version, group_id,
113    /// ciphersuite, group_context extensions. Set any other group
114    /// configuration on this builder before calling `reinit`, then create the
115    /// new group and its reinit commit with
116    /// [`ReInitGroupBuilder::build_reinit`].
117    ///
118    /// [RFC 9420 §11.2]: https://www.rfc-editor.org/rfc/rfc9420.html#name-reinitialization
119    pub fn reinit(self, reinit_info: ReInitInfo) -> ReInitGroupBuilder {
120        ReInitGroupBuilder {
121            group_builder: self,
122            reinit_info,
123            force_self_update: false,
124        }
125    }
126
127    /// Build a new group as configured by this builder.
128    #[openmls_traits::maybe_async]
129    pub async fn build<Provider: OpenMlsProvider>(
130        self,
131        provider: &Provider,
132        signer: &impl Signer,
133        credential_with_key: CredentialWithKey,
134    ) -> Result<MlsGroup, NewGroupError<Provider::StorageError>> {
135        self.build_internal(provider, signer, credential_with_key, None)
136            .await
137    }
138
139    /// Build a new group with the given group ID.
140    ///
141    /// If an [`MlsGroupCreateConfig`] is provided, it will be used to configure the
142    /// group. Otherwise, the internal builder is used to build one with the
143    /// parameters set on this builder.
144    ///
145    /// If a group with the same ID already exists in storage and
146    /// `replace_old_group` was not set, an error will be returned.
147    #[openmls_traits::maybe_async]
148    pub(super) async fn build_internal<Provider: OpenMlsProvider>(
149        self,
150        provider: &Provider,
151        signer: &impl Signer,
152        credential_with_key: CredentialWithKey,
153        mls_group_create_config_option: Option<MlsGroupCreateConfig>,
154    ) -> Result<MlsGroup, NewGroupError<Provider::StorageError>> {
155        let mls_group_create_config = mls_group_create_config_option
156            .unwrap_or_else(|| self.mls_group_create_config_builder.build());
157        let group_id = self
158            .group_id
159            .unwrap_or_else(|| GroupId::random(provider.rand()));
160        let ciphersuite = mls_group_create_config.ciphersuite;
161
162        provider
163            .crypto()
164            .supports(ciphersuite)
165            .map_err(|_| NewGroupError::UnsupportedCiphersuite(ciphersuite))?;
166
167        #[cfg(feature = "virtual-clients-draft")]
168        if let Some(emulation_group_id) = &self.vc_emulation_group_id {
169            let epoch_id =
170                crate::components::vc_derivation_info::require_newest_vc_derivation_epoch(
171                    provider.storage(),
172                    emulation_group_id,
173                )
174                .await?;
175            return build_vc_internal(
176                provider,
177                signer,
178                credential_with_key,
179                mls_group_create_config,
180                group_id,
181                self.replace_old_group,
182                epoch_id,
183            )
184            .await;
185        }
186
187        if !self.replace_old_group
188            && MlsGroup::load(provider.storage(), &group_id)
189                .await
190                .map_err(NewGroupError::StorageError)?
191                .is_some()
192        {
193            return Err(NewGroupError::GroupAlreadyExists);
194        }
195
196        let (public_group_builder, commit_secret, leaf_keypair) =
197            PublicGroup::builder(group_id, ciphersuite, credential_with_key)
198                .with_group_context_extensions(
199                    mls_group_create_config.group_context_extensions.clone(),
200                )
201                .with_leaf_node_extensions(mls_group_create_config.leaf_node_extensions.clone())
202                .with_lifetime(*mls_group_create_config.lifetime())
203                .with_capabilities(mls_group_create_config.capabilities.clone())
204                .with_capabilities_policy(mls_group_create_config.capabilities_policy)
205                .get_secrets(provider, signer)
206                .map_err(|e| match e {
207                    PublicGroupBuildError::LibraryError(e) => NewGroupError::LibraryError(e),
208                    PublicGroupBuildError::InvalidExtensions(e) => e.into(),
209                    PublicGroupBuildError::LeafNodeBuild(e) => e.into(),
210                })?;
211
212        let serialized_group_context = public_group_builder
213            .group_context()
214            .tls_serialize_detached()
215            .map_err(LibraryError::missing_bound_check)?;
216
217        // Derive an initial joiner secret based on the commit secret.
218        // Derive an epoch secret from the joiner secret.
219        // We use a random `InitSecret` for initialization.
220        let joiner_secret = JoinerSecret::new(
221            provider.crypto(),
222            ciphersuite,
223            commit_secret,
224            &InitSecret::random(ciphersuite, provider.rand())
225                .map_err(LibraryError::unexpected_crypto_error)?,
226            &serialized_group_context,
227        )
228        .map_err(LibraryError::unexpected_crypto_error)?;
229
230        // TODO(#1357)
231        let mut resumption_psk_store = ResumptionPskStore::new(32);
232
233        // Prepare the PskSecret
234        let psk_secret = load_psks(provider.storage(), &resumption_psk_store, &self.psk_ids)
235            .await
236            .and_then(|psks| PskSecret::new(provider.crypto(), ciphersuite, psks))
237            .map_err(|e| {
238                log::debug!("Unexpected PSK error: {e:?}");
239                LibraryError::custom("Unexpected PSK error")
240            })?;
241
242        let mut key_schedule =
243            KeySchedule::init(ciphersuite, provider.crypto(), &joiner_secret, psk_secret)?;
244        key_schedule
245            .add_context(provider.crypto(), &serialized_group_context)
246            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
247
248        let EpochSecretsResult {
249            epoch_secrets,
250            #[cfg(feature = "extensions-draft")]
251            application_exporter,
252        } = key_schedule
253            .epoch_secrets(provider.crypto(), ciphersuite)
254            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
255
256        let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
257            serialized_group_context,
258            TreeSize::new(1),
259            LeafNodeIndex::new(0u32),
260        );
261
262        let initial_confirmation_tag = message_secrets
263            .confirmation_key()
264            .tag(provider.crypto(), ciphersuite, &[])
265            .map_err(LibraryError::unexpected_crypto_error)?;
266
267        let message_secrets_store = MessageSecretsStore::new_with_secret(
268            mls_group_create_config
269                .join_config
270                .past_epoch_deletion_policy(),
271            message_secrets,
272        );
273
274        let public_group = public_group_builder
275            .with_confirmation_tag(initial_confirmation_tag)
276            .build(provider.crypto())?;
277
278        // We already add a resumption PSK for epoch 0 to make things more unified.
279        let resumption_psk = group_epoch_secrets.resumption_psk();
280        resumption_psk_store.add(public_group.group_context().epoch(), resumption_psk.clone());
281
282        #[cfg(feature = "extensions-draft")]
283        #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
284        let mut application_export_tree = ApplicationExportTree::new(application_exporter);
285
286        // The initial epoch of an emulation group is a derivation epoch.
287        #[cfg(feature = "virtual-clients-draft")]
288        if mls_group_create_config.emulation_group {
289            crate::components::vc_derivation_info::register_vc_derivation_epoch(
290                provider.crypto(),
291                provider.storage(),
292                Some(&mut application_export_tree),
293                crate::components::vc_derivation_info::VcDerivationEpochParams::for_public_group(
294                    &public_group,
295                    LeafNodeIndex::new(0),
296                    mls_group_create_config
297                        .join_config
298                        .vc_derivation_epoch_retention_policy()
299                        .clone(),
300                ),
301            )
302            .await?;
303        }
304
305        let mls_group = MlsGroup {
306            mls_group_config: mls_group_create_config.join_config.clone(),
307            own_leaf_nodes: vec![],
308            aad: vec![],
309            #[cfg(feature = "extensions-draft")]
310            safe_aad: crate::framing::SafeAad::empty(),
311            group_state: MlsGroupState::Operational,
312            public_group,
313            group_epoch_secrets,
314            own_leaf_index: LeafNodeIndex::new(0),
315            message_secrets_store,
316            resumption_psk_store,
317            #[cfg(feature = "extensions-draft")]
318            application_export_tree: Some(application_export_tree),
319            #[cfg(feature = "virtual-clients-draft")]
320            emulation_group: mls_group_create_config.emulation_group,
321        };
322
323        mls_group
324            .store(provider.storage())
325            .await
326            .map_err(NewGroupError::StorageError)?;
327        mls_group
328            .store_epoch_keypairs(provider.storage(), &[leaf_keypair])
329            .await
330            .map_err(NewGroupError::StorageError)?;
331
332        Ok(mls_group)
333    }
334
335    // MlsGroupCreateConfigBuilder options
336
337    /// Sets the `wire_format` property of the MlsGroup.
338    pub fn with_wire_format_policy(mut self, wire_format_policy: WireFormatPolicy) -> Self {
339        self.mls_group_create_config_builder = self
340            .mls_group_create_config_builder
341            .wire_format_policy(wire_format_policy);
342        self
343    }
344
345    /// Sets the `padding_size` property of the MlsGroup.
346    pub fn padding_size(mut self, padding_size: usize) -> Self {
347        self.mls_group_create_config_builder = self
348            .mls_group_create_config_builder
349            .padding_size(padding_size);
350        self
351    }
352
353    /// Sets the `max_past_epochs` property of the MlsGroup.
354    /// This allows application messages from previous epochs to be decrypted.
355    ///
356    /// This method overrides the policy set by [`Self::set_past_epoch_deletion_policy()`],
357    /// and is equivalent to setting the past epoch deletion policy to
358    /// `PastEpochDeletionPolicy::MaxEpochs(max_past_epochs)`.
359    ///
360    /// **WARNING**
361    ///
362    ///
363    /// This feature enables the storage of message secrets from past epochs.
364    /// It is a trade-off between functionality and forward secrecy and should only be enabled
365    /// if the Delivery Service cannot guarantee that application messages will be sent in
366    /// the same epoch in which they were generated. The number for `max_epochs` should be
367    /// as low as possible.
368    ///
369    /// NOTE: This function will be deprecated in future releases.
370    pub fn max_past_epochs(mut self, max_past_epochs: usize) -> Self {
371        self.mls_group_create_config_builder = self
372            .mls_group_create_config_builder
373            .max_past_epochs(max_past_epochs);
374        self
375    }
376
377    /// Set the policy for deleting past epoch secrets.
378    ///
379    /// By default, storage of past epoch secrets is disabled.
380    ///
381    /// This method overrides the configuration set by [`Self::max_past_epochs()`].
382    ///
383    /// **WARNING**
384    ///
385    /// This feature enables the storage of message secrets from past epochs.
386    /// It is a trade-off between functionality and forward secrecy and should only be enabled
387    /// if the Delivery Service cannot guarantee that application messages will be sent in
388    /// the same epoch in which they were generated. The number for `max_epochs` should be
389    /// as low as possible.
390    pub fn set_past_epoch_deletion_policy(mut self, policy: PastEpochDeletionPolicy) -> Self {
391        self.mls_group_create_config_builder = self
392            .mls_group_create_config_builder
393            .set_past_epoch_deletion_policy(policy);
394        self
395    }
396
397    /// Sets the derivation-epoch retention policy. See
398    /// [`VcDerivationEpochRetentionPolicy`](crate::group::VcDerivationEpochRetentionPolicy).
399    #[cfg(feature = "virtual-clients-draft")]
400    pub fn set_vc_derivation_epoch_retention_policy(
401        mut self,
402        policy: crate::group::VcDerivationEpochRetentionPolicy,
403    ) -> Self {
404        self.mls_group_create_config_builder = self
405            .mls_group_create_config_builder
406            .set_vc_derivation_epoch_retention_policy(policy);
407        self
408    }
409
410    /// Sets the `number_of_resumption_psks` property of the MlsGroup.
411    pub fn number_of_resumption_psks(mut self, number_of_resumption_psks: usize) -> Self {
412        self.mls_group_create_config_builder = self
413            .mls_group_create_config_builder
414            .number_of_resumption_psks(number_of_resumption_psks);
415        self
416    }
417
418    /// Sets the `use_ratchet_tree_extension` property of the MlsGroup.
419    pub fn use_ratchet_tree_extension(mut self, use_ratchet_tree_extension: bool) -> Self {
420        self.mls_group_create_config_builder = self
421            .mls_group_create_config_builder
422            .use_ratchet_tree_extension(use_ratchet_tree_extension);
423        self
424    }
425
426    /// Sets the `sender_ratchet_configuration` property of the MlsGroup.
427    /// See [`SenderRatchetConfiguration`] for more information.
428    pub fn sender_ratchet_configuration(
429        mut self,
430        sender_ratchet_configuration: SenderRatchetConfiguration,
431    ) -> Self {
432        self.mls_group_create_config_builder = self
433            .mls_group_create_config_builder
434            .sender_ratchet_configuration(sender_ratchet_configuration);
435        self
436    }
437
438    /// Sets the `lifetime` of the group creator's leaf.
439    pub fn lifetime(mut self, lifetime: Lifetime) -> Self {
440        self.mls_group_create_config_builder =
441            self.mls_group_create_config_builder.lifetime(lifetime);
442        self
443    }
444
445    /// Sets the `ciphersuite` of the MlsGroup.
446    pub fn ciphersuite(mut self, ciphersuite: Ciphersuite) -> Self {
447        self.mls_group_create_config_builder = self
448            .mls_group_create_config_builder
449            .ciphersuite(ciphersuite);
450        self
451    }
452
453    /// Sets the initial group context extensions
454    pub fn with_group_context_extensions(mut self, extensions: Extensions<GroupContext>) -> Self {
455        self.mls_group_create_config_builder = self
456            .mls_group_create_config_builder
457            .with_group_context_extensions(extensions);
458        self
459    }
460
461    /// Sets the initial leaf node extensions
462    pub fn with_leaf_node_extensions(
463        mut self,
464        extensions: Extensions<LeafNode>,
465    ) -> Result<Self, LeafNodeValidationError> {
466        self.mls_group_create_config_builder = self
467            .mls_group_create_config_builder
468            .with_leaf_node_extensions(extensions)?;
469        Ok(self)
470    }
471
472    /// Sets the group creator's [`Capabilities`]
473    ///
474    /// See [`MlsGroupCreateConfigBuilder::capabilities`] for what setting them
475    /// explicitly implies.
476    pub fn with_capabilities(mut self, capabilities: Capabilities) -> Self {
477        self.mls_group_create_config_builder = self
478            .mls_group_create_config_builder
479            .capabilities(capabilities);
480        self
481    }
482
483    /// Sets how the group creator's [`Capabilities`] are treated when they
484    /// don't cover what the leaf needs.
485    ///
486    /// See [`MlsGroupCreateConfigBuilder::capabilities_policy`].
487    pub fn with_capabilities_policy(mut self, policy: CapabilitiesPolicy) -> Self {
488        self.mls_group_create_config_builder = self
489            .mls_group_create_config_builder
490            .capabilities_policy(policy);
491        self
492    }
493}
494
495/// Builder that creates a fresh sub-group and its branch commit in a single
496/// step, as described in [RFC 9420 §11.3].
497///
498/// Create this with [`MlsGroupBuilder::branch`].
499///
500/// [RFC 9420 §11.3]: https://www.rfc-editor.org/rfc/rfc9420.html#name-subgroup-branching
501pub struct BranchGroupBuilder {
502    group_builder: MlsGroupBuilder,
503    branch_info: BranchInfo,
504    extensions: Option<Extensions<GroupContext>>,
505    force_self_update: bool,
506}
507
508impl BranchGroupBuilder {
509    /// Add a [`GroupContextExtensions`](crate::extensions::Extensions) proposal
510    /// to the branch commit. See
511    /// [`CommitBuilder::propose_group_context_extensions`](crate::group::CommitBuilder::propose_group_context_extensions).
512    pub fn propose_group_context_extensions(
513        mut self,
514        extensions: Extensions<GroupContext>,
515    ) -> Self {
516        self.extensions = Some(extensions);
517        self
518    }
519
520    /// Force a self-update (path) in the branch commit. See
521    /// [`CommitBuilder::force_self_update`](crate::group::CommitBuilder::force_self_update).
522    pub fn force_self_update(mut self, force_self_update: bool) -> Self {
523        self.force_self_update = force_self_update;
524        self
525    }
526
527    /// Create the sub-group and the branch commit that adds `new_members` to it.
528    ///
529    /// This creates a fresh group with the parent's ciphersuite, adds the branch
530    /// resumption PSK (mixing in the parent's resumption PSK secret), and commits
531    /// the additions (plus any extra proposals set via
532    /// [`Self::propose_group_context_extensions`] / [`Self::force_self_update`]).
533    /// It returns the new (epoch-0) sub-group and the [`CommitMessageBundle`]
534    /// carrying the branch commit and `Welcome`.
535    ///
536    /// The commit is staged but **not** merged: merge it with
537    /// [`MlsGroup::merge_pending_commit`](crate::group::MlsGroup::merge_pending_commit)
538    /// only once the delivery service has confirmed it.
539    #[openmls_traits::maybe_async]
540    pub async fn build_branch<Provider: OpenMlsProvider>(
541        self,
542        provider: &Provider,
543        signer: &impl Signer,
544        credential_with_key: CredentialWithKey,
545        new_members: Vec<KeyPackage>,
546    ) -> Result<(MlsGroup, CommitMessageBundle), BranchError<Provider::StorageError>> {
547        // The sub-group must use the same ciphersuite as the parent group.
548        let group_builder = self
549            .group_builder
550            .ciphersuite(self.branch_info.ciphersuite());
551        let mut group = group_builder
552            .build(provider, signer, credential_with_key)
553            .await?;
554
555        let mut builder = group
556            .commit_builder()
557            .branch(provider.rand(), &self.branch_info)?
558            .propose_adds(new_members);
559        if let Some(extensions) = self.extensions {
560            builder = builder.propose_group_context_extensions(extensions)?;
561        }
562        if self.force_self_update {
563            builder = builder.force_self_update(true);
564        }
565        let bundle = builder
566            .load_psks(provider.storage())
567            .await?
568            .build(provider.rand(), provider.crypto(), signer, |_| true)?
569            .stage_commit(provider)
570            .await?;
571
572        Ok((group, bundle))
573    }
574}
575
576/// Indicates an error occurred while creating a sub-group branch with
577/// [`BranchGroupBuilder::build_branch`].
578#[derive(Debug, thiserror::Error)]
579pub enum BranchError<StorageError> {
580    /// An error occurred while creating the sub-group.
581    #[error(transparent)]
582    NewGroup(#[from] NewGroupError<StorageError>),
583    /// An error occurred while creating the branch commit.
584    #[error(transparent)]
585    CreateCommit(#[from] CreateCommitError),
586    /// An error occurred while staging the branch commit.
587    #[error(transparent)]
588    CommitBuilderStage(#[from] CommitBuilderStageError<StorageError>),
589}
590
591/// Builder that creates a freshly reinitialized group and its reinit commit in a single
592/// step, as described in [RFC 9420 §11.2].
593///
594/// This overrides the group_id, ciphersuite and group_context.extensions of the group_builder
595/// with value from the ReInit proposal.
596///
597/// Create this with [`MlsGroupBuilder::reinit`].
598///
599/// [RFC 9420 §11.2]: https://www.rfc-editor.org/rfc/rfc9420.html#name-reinitialization
600pub struct ReInitGroupBuilder {
601    group_builder: MlsGroupBuilder,
602    reinit_info: ReInitInfo,
603    force_self_update: bool,
604}
605
606impl ReInitGroupBuilder {
607    /// Force a self-update (path) in the reinit commit. See
608    /// [`CommitBuilder::force_self_update`](crate::group::CommitBuilder::force_self_update).
609    pub fn force_self_update(mut self, force_self_update: bool) -> Self {
610        self.force_self_update = force_self_update;
611        self
612    }
613
614    /// Create the reinitialized group and a reinit commit that adds `members` to it.
615    /// The application must ensure that `members` match the members of the predecessor group,
616    /// otherwise joiners will reject the Welcome, see also [`JoinBuilder::check_members`](crate::group::mls_group::creation::JoinBuilder::check_members).
617    ///
618    /// This creates a fresh group with the ReInit proposal's group_id, ciphersuite and
619    /// group_context.extensions, adds the reinit
620    /// resumption PSK (mixing in the old group's resumption PSK secret), and commits
621    /// the additions (plus [`Self::force_self_update`]).
622    /// It returns the new (epoch-0) group and the [`CommitMessageBundle`]
623    /// carrying the first commit and `Welcome`.
624    ///
625    /// The commit is staged but **not** merged: merge it with
626    /// [`MlsGroup::merge_pending_commit`](crate::group::MlsGroup::merge_pending_commit)
627    /// only once the delivery service has confirmed it.
628    #[openmls_traits::maybe_async]
629    pub async fn build_reinit<Provider: OpenMlsProvider>(
630        self,
631        provider: &Provider,
632        signer: &impl Signer,
633        credential_with_key: CredentialWithKey,
634        members: impl IntoIterator<Item = KeyPackage>,
635    ) -> Result<(MlsGroup, CommitMessageBundle), ReInitError<Provider::StorageError>> {
636        // The group must match the proposal.
637        let ReInitInfo {
638            proposal,
639            old_group_id,
640            old_group_epoch,
641            resumption_psk_secret,
642            member_credentials: _,
643        } = self.reinit_info;
644        let group_builder = self
645            .group_builder
646            .ciphersuite(proposal.ciphersuite)
647            .with_group_id(proposal.group_id)
648            .with_group_context_extensions(proposal.extensions);
649        let mut group = group_builder
650            .build(provider, signer, credential_with_key)
651            .await?;
652
653        let mut commit_builder = group
654            .commit_builder()
655            .reinit(
656                provider.rand(),
657                old_group_id,
658                old_group_epoch,
659                resumption_psk_secret,
660            )?
661            .propose_adds(members);
662        if self.force_self_update {
663            commit_builder = commit_builder.force_self_update(true);
664        }
665        let bundle = commit_builder
666            .load_psks(provider.storage())
667            .await?
668            .build(provider.rand(), provider.crypto(), signer, |_| true)?
669            .stage_commit(provider)
670            .await?;
671
672        Ok((group, bundle))
673    }
674}
675
676/// Indicates an error occurred while creating a reinitialized group with
677/// [`ReInitGroupBuilder::build_reinit`].
678#[derive(Debug, thiserror::Error)]
679pub enum ReInitError<StorageError> {
680    /// An error occurred while creating the sub-group.
681    #[error(transparent)]
682    NewGroup(#[from] NewGroupError<StorageError>),
683    /// An error occurred while creating the reinit commit.
684    #[error(transparent)]
685    CreateCommit(#[from] CreateCommitError),
686    /// An error occurred while staging the reinit commit.
687    #[error(transparent)]
688    CommitBuilderStage(#[from] CommitBuilderStageError<StorageError>),
689}
690
691/// Create a new group with the virtual client as the creator (epoch 0, single
692/// leaf).
693///
694/// The creator's leaf is `key_package`-sourced and its key material is derived
695/// from a fresh `key_package` operation secret of the derivation epoch
696/// `epoch_id` (batch index 0). The epoch-0 `epoch_secret` is derived from the
697/// same KeyPackage seed under the created group's ciphersuite. A sibling
698/// emulator client reconstructs this exact state with
699/// [`MlsGroup::vc_join_at_creation`]: it shares the operation secret tree, so it
700/// rederives the same seed and hence the same epoch secret without any secret
701/// travelling on the wire. Because the `epoch_secret` is derived rather than run
702/// through the joiner key schedule, this path bypasses it entirely.
703///
704/// [`MlsGroup::vc_join_at_creation`]: crate::group::MlsGroup::vc_join_at_creation
705#[cfg(feature = "virtual-clients-draft")]
706#[allow(clippy::too_many_arguments)]
707#[openmls_traits::maybe_async]
708async fn build_vc_internal<Provider: OpenMlsProvider>(
709    provider: &Provider,
710    signer: &impl Signer,
711    credential_with_key: CredentialWithKey,
712    mls_group_create_config: MlsGroupCreateConfig,
713    group_id: GroupId,
714    replace_old_group: bool,
715    epoch_id: crate::components::vc_derivation_info::EpochId,
716) -> Result<MlsGroup, NewGroupError<Provider::StorageError>> {
717    use openmls_traits::storage::StorageProvider as _;
718
719    use crate::{
720        components::vc_derivation_info::{
721            load_vc_epoch_state_and_tree, DerivationInfo, DerivationInfoTbe,
722            VirtualClientOperationType, VirtualClientsError,
723        },
724        schedule::EpochSecrets,
725        treesync::TreeSync,
726    };
727
728    if !replace_old_group
729        && MlsGroup::load(provider.storage(), &group_id)
730            .await
731            .map_err(NewGroupError::StorageError)?
732            .is_some()
733    {
734        return Err(NewGroupError::GroupAlreadyExists);
735    }
736
737    let ciphersuite = mls_group_create_config.ciphersuite;
738    let (capabilities, capabilities_policy) = resolve_capabilities(
739        mls_group_create_config.capabilities.clone(),
740        mls_group_create_config.capabilities_policy,
741    );
742
743    // Validate that the creator's leaf declares `AppDataDictionary` and lists
744    // `VC_COMPONENT_ID` before allocating a generation, so a deterministic
745    // precondition failure does not burn an operation secret.
746    let resolved_dictionary = crate::components::vc_derivation_info::resolve_vc_leaf_dictionary(
747        Some(&capabilities),
748        Some(&mls_group_create_config.leaf_node_extensions),
749        None,
750    )?;
751
752    // Load the derivation epoch state and operation tree, allocate a fresh
753    // `key_package` generation (empty operation context, matching the KeyPackage
754    // batch path), and persist the advanced tree right away. A retried creation
755    // consumes a fresh generation.
756    let (state, mut operation_tree) = load_vc_epoch_state_and_tree(provider, &epoch_id).await?;
757    let (emulation_leaf_index, epoch_encryption_key, emulation_ciphersuite) = state.into_parts();
758    let (generation, operation_secret) = operation_tree.next_operation_secret(
759        provider.crypto(),
760        emulation_ciphersuite,
761        &epoch_id,
762        emulation_leaf_index,
763        VirtualClientOperationType::KeyPackage,
764        b"",
765    )?;
766    provider
767        .storage()
768        .write_vc_operation_tree(&epoch_id, &operation_tree)
769        .await
770        .map_err(NewGroupError::StorageError)?;
771
772    // The creator batch consists of this single derivation and is closed
773    // immediately: no `KeyPackageUpload` is sent and no
774    // `RetainedKeyPackageMaterial` is written, because a creator leaf has no
775    // KeyPackage / KeyPackageRef. Import the per-KeyPackage seed (index 0) under
776    // the created group's ciphersuite, then derive the creator leaf's encryption
777    // keypair and the epoch-0 secret from that seed.
778    let key_package_index = 0;
779    let key_package_seed = operation_secret.derive_key_package_seed_secret(
780        provider.crypto(),
781        ciphersuite,
782        key_package_index,
783    )?;
784    let leaf_encryption_keypair = key_package_seed
785        .derive_encryption_key_secret(provider.crypto(), ciphersuite)?
786        .generate_encryption_key_pair(provider.crypto(), ciphersuite)?;
787    let epoch_secret =
788        key_package_seed.derive_group_creation_secret(provider.crypto(), ciphersuite)?;
789
790    // Wrap the derivation info under the per-epoch AEAD key, bound to the leaf
791    // via its serialized encryption key.
792    let leaf_encryption_key = leaf_encryption_keypair
793        .public_key()
794        .tls_serialize_detached()
795        .map_err(VirtualClientsError::from)?;
796    let tbe = DerivationInfoTbe::KeyPackage {
797        leaf_index: emulation_leaf_index,
798        generation,
799        key_package_index,
800    };
801    let derivation_info = DerivationInfo::encrypt(
802        provider.crypto(),
803        emulation_ciphersuite,
804        &epoch_encryption_key,
805        epoch_id.clone(),
806        &leaf_encryption_key,
807        &tbe,
808    )?;
809    let derivation_info_bytes = derivation_info
810        .tls_serialize_detached()
811        .map_err(VirtualClientsError::from)?;
812    let leaf_extensions = crate::components::vc_derivation_info::merge_vc_derivation_info(
813        Some(&mls_group_create_config.leaf_node_extensions),
814        resolved_dictionary,
815        derivation_info_bytes,
816    )?;
817
818    // Build the single-leaf tree with the derived key_package-sourced leaf.
819    let (treesync, leaf_keypair) = TreeSync::new_vc(
820        provider,
821        signer,
822        ciphersuite,
823        credential_with_key,
824        *mls_group_create_config.lifetime(),
825        capabilities,
826        leaf_extensions,
827        leaf_encryption_keypair,
828        LeafNodeConstraints::from_group_context_extensions(
829            &mls_group_create_config.group_context_extensions,
830        ),
831        capabilities_policy,
832    )?;
833    let group_context = GroupContext::create_initial_group_context(
834        ciphersuite,
835        group_id.clone(),
836        treesync.tree_hash().to_vec(),
837        mls_group_create_config.group_context_extensions.clone(),
838    );
839    let serialized_group_context = group_context
840        .tls_serialize_detached()
841        .map_err(LibraryError::missing_bound_check)?;
842
843    // Derive epoch-0 secrets from the epoch secret derived above.
844    let epoch_secrets =
845        EpochSecrets::from_epoch_secret(provider.crypto(), ciphersuite, epoch_secret)
846            .map_err(LibraryError::unexpected_crypto_error)?;
847    let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
848        serialized_group_context,
849        TreeSize::new(1),
850        LeafNodeIndex::new(0),
851    );
852    let initial_confirmation_tag = message_secrets
853        .confirmation_key()
854        .tag(provider.crypto(), ciphersuite, &[])
855        .map_err(LibraryError::unexpected_crypto_error)?;
856    let message_secrets_store = MessageSecretsStore::new_with_secret(
857        mls_group_create_config
858            .join_config
859            .past_epoch_deletion_policy(),
860        message_secrets,
861    );
862    let public_group = PublicGroup::new(
863        provider.crypto(),
864        treesync,
865        group_context,
866        initial_confirmation_tag,
867    )?;
868
869    let mut resumption_psk_store = ResumptionPskStore::new(32);
870    let resumption_psk = group_epoch_secrets.resumption_psk();
871    resumption_psk_store.add(public_group.group_context().epoch(), resumption_psk.clone());
872
873    let mls_group = MlsGroup {
874        mls_group_config: mls_group_create_config.join_config.clone(),
875        own_leaf_nodes: vec![],
876        aad: vec![],
877        safe_aad: crate::framing::SafeAad::empty(),
878        group_state: MlsGroupState::Operational,
879        public_group,
880        group_epoch_secrets,
881        own_leaf_index: LeafNodeIndex::new(0),
882        message_secrets_store,
883        resumption_psk_store,
884        // Reconstructed VC groups do not populate the application export tree,
885        // matching the other VC group-entry paths.
886        application_export_tree: None,
887        // A group a virtual client creates is not itself an emulation group.
888        emulation_group: false,
889    };
890
891    // Bind epoch 0 of the new group to the derivation epoch so later VC
892    // operations in this group resolve the right derivation epoch state.
893    // Written before the group itself, so an error between the writes cannot
894    // leave a loadable group without a binding (a bound group is required for
895    // the reuse-guard MUST).
896    let max_entries = mls_group.message_secrets_store.max_epochs.saturating_add(1);
897    crate::components::vc_derivation_info::write_vc_emulation_binding_with_pruning(
898        provider.storage(),
899        &group_id,
900        mls_group.epoch(),
901        epoch_id,
902        max_entries,
903    )
904    .await
905    .map_err(NewGroupError::StorageError)?;
906
907    mls_group
908        .store(provider.storage())
909        .await
910        .map_err(NewGroupError::StorageError)?;
911    mls_group
912        .store_epoch_keypairs(provider.storage(), &[leaf_keypair])
913        .await
914        .map_err(NewGroupError::StorageError)?;
915
916    Ok(mls_group)
917}