Skip to main content

openmls/group/mls_group/
creation.rs

1use errors::NewGroupError;
2use openmls_traits::{crypto::OpenMlsCrypto, storage::StorageProvider as StorageProviderTrait};
3
4use super::{builder::MlsGroupBuilder, *};
5use crate::{
6    credentials::CredentialWithKey,
7    extensions::Extensions,
8    group::{
9        commit_builder::external_commits::ExternalCommitBuilder,
10        errors::{ExportSecretError, ExternalCommitError, WelcomeError},
11    },
12    key_packages::KeyPackage,
13    messages::{
14        group_info::{GroupInfo, VerifiableGroupInfo},
15        Welcome,
16    },
17    schedule::{
18        psk::{store::ResumptionPskStore, PreSharedKeyId, Psk, ResumptionPskUsage},
19        EpochSecretsResult,
20    },
21    storage::OpenMlsProvider,
22    treesync::{
23        errors::{DerivePathError, PublicTreeError},
24        node::leaf_node::{Capabilities, LeafNodeParameters},
25        RatchetTreeIn,
26    },
27};
28
29#[cfg(feature = "virtual-clients-draft")]
30use crate::{
31    ciphersuite::hash_ref::KeyPackageRef,
32    component::ComponentId,
33    components::vc_derivation_info::{
34        load_vc_epoch_state_and_tree, register_vc_derivation_epoch,
35        write_vc_emulation_binding_with_pruning, DerivationInfoTbe, EpochId,
36        RetainedKeyPackageMaterial, VcDerivationEpochParams, VcDerivationEpochState,
37        VcWelcomeMaterial, VirtualClientOperationType, VirtualClientsError,
38    },
39    framing::{mls_auth_content::AuthenticatedContent, ProtocolMessage, SafeAad, Sender},
40    group::{
41        config::PastEpochDeletionPolicy,
42        errors::{
43            ProcessMessageError, StageCommitError, VcExternalCommitJoinError,
44            VcGroupCreationJoinError,
45        },
46        mls_group::processing::{
47            committed_app_data_update_proposals, AppDataDictionaryUpdater, AppDataUpdates,
48        },
49        public_group::{errors::ApplyAppDataUpdateError, PublicGroup},
50    },
51    messages::proposals::{AppDataUpdateProposal, AppEphemeralProposal, Proposal, ProposalOrRef},
52    prelude::mls_content::FramedContentBody,
53    schedule::{EpochSecrets, InitSecret},
54    treesync::node::leaf_node::LeafNodeSource,
55};
56
57impl MlsGroup {
58    // === Group creation ===
59
60    /// Creates a builder which can be used to configure and build
61    /// a new [`MlsGroup`].
62    pub fn builder() -> MlsGroupBuilder {
63        MlsGroupBuilder::new()
64    }
65
66    /// Creates a new group with the creator as the only member (and a random
67    /// group ID).
68    pub fn new<Provider: OpenMlsProvider>(
69        provider: &Provider,
70        signer: &impl Signer,
71        mls_group_create_config: &MlsGroupCreateConfig,
72        credential_with_key: CredentialWithKey,
73    ) -> Result<Self, NewGroupError<Provider::StorageError>> {
74        MlsGroupBuilder::new().build_internal(
75            provider,
76            signer,
77            credential_with_key,
78            Some(mls_group_create_config.clone()),
79        )
80    }
81
82    /// Creates a new group with a given group ID with the creator as the only
83    /// member.
84    pub fn new_with_group_id<Provider: OpenMlsProvider>(
85        provider: &Provider,
86        signer: &impl Signer,
87        mls_group_create_config: &MlsGroupCreateConfig,
88        group_id: GroupId,
89        credential_with_key: CredentialWithKey,
90    ) -> Result<Self, NewGroupError<Provider::StorageError>> {
91        MlsGroupBuilder::new()
92            .with_group_id(group_id)
93            .build_internal(
94                provider,
95                signer,
96                credential_with_key,
97                Some(mls_group_create_config.clone()),
98            )
99    }
100
101    /// Join an existing group through an External Commit.
102    /// The resulting [`MlsGroup`] instance starts off with a pending
103    /// commit (the external commit, which adds this client to the group).
104    /// Merging this commit is necessary for this [`MlsGroup`] instance to
105    /// function properly, as, for example, this client is not yet part of the
106    /// tree. As a result, it is not possible to clear the pending commit. If
107    /// the external commit was rejected due to an epoch change, the
108    /// [`MlsGroup`] instance has to be discarded and a new one has to be
109    /// created using this function based on the latest `ratchet_tree` and
110    /// group info. For more information on the external init process,
111    /// please see Section 11.2.1 in the MLS specification.
112    ///
113    /// Note: If there is a group member in the group with the same identity as
114    /// us, this will create a remove proposal.
115    #[allow(clippy::too_many_arguments)]
116    #[deprecated(
117        since = "0.7.1",
118        note = "Use the `MlsGroup::external_commit_builder` instead."
119    )]
120    pub fn join_by_external_commit<Provider: OpenMlsProvider>(
121        provider: &Provider,
122        signer: &impl Signer,
123        ratchet_tree: Option<RatchetTreeIn>,
124        verifiable_group_info: VerifiableGroupInfo,
125        mls_group_config: &MlsGroupJoinConfig,
126        capabilities: Option<Capabilities>,
127        extensions: Option<Extensions<LeafNode>>,
128        aad: &[u8],
129        credential_with_key: CredentialWithKey,
130    ) -> Result<(Self, MlsMessageOut, Option<GroupInfo>), ExternalCommitError<Provider::StorageError>>
131    {
132        let leaf_node_parameters = LeafNodeParameters::builder()
133            .with_capabilities(capabilities.unwrap_or_default())
134            .with_extensions(extensions.unwrap_or_default())
135            .build();
136
137        let mut external_commit_builder = ExternalCommitBuilder::new()
138            .with_aad(aad.to_vec())
139            .with_config(mls_group_config.clone());
140
141        if let Some(ratchet_tree) = ratchet_tree {
142            external_commit_builder = external_commit_builder.with_ratchet_tree(ratchet_tree)
143        }
144
145        let (mls_group, commit_message_bundle) = external_commit_builder
146            .build_group(provider, verifiable_group_info, credential_with_key)?
147            .leaf_node_parameters(leaf_node_parameters)
148            .load_psks(provider.storage())
149            .map_err(|e| {
150                log::error!("Error loading PSKs for external commit: {e:?}");
151                LibraryError::custom("Error loading PSKs for external commit")
152            })?
153            .build(provider.rand(), provider.crypto(), signer, |_| true)?
154            .finalize(provider)?;
155
156        let (commit, _, group_info) = commit_message_bundle.into_contents();
157
158        Ok((mls_group, commit, group_info))
159    }
160}
161
162impl ProcessedWelcome {
163    /// Creates a new processed [`Welcome`] message , which can be
164    /// inspected before creating a [`StagedWelcome`].
165    ///
166    /// This does not require a ratchet tree yet.
167    ///
168    /// [`Welcome`]: crate::messages::Welcome
169    pub fn new_from_welcome<Provider: OpenMlsProvider>(
170        provider: &Provider,
171        mls_group_config: &MlsGroupJoinConfig,
172        welcome: Welcome,
173    ) -> Result<Self, WelcomeError<Provider::StorageError>> {
174        Self::new_from_welcome_inner(provider, mls_group_config, welcome, None)
175    }
176
177    /// Like [`ProcessedWelcome::new_from_welcome`], but allows injecting a
178    /// resumption PSK secret that is not held in storage.
179    ///
180    /// This is used for subgroup branching (RFC 9420 §11.3): the branch PSK
181    /// secret comes from the parent group and is injected at the sentinel epoch
182    /// 0, where [`load_psks`](crate::schedule::psk::load_psks) looks it up for
183    /// branch usage. See [`StagedWelcome::build_from_branch`].
184    pub(crate) fn new_from_welcome_inner<Provider: OpenMlsProvider>(
185        provider: &Provider,
186        mls_group_config: &MlsGroupJoinConfig,
187        welcome: Welcome,
188        branch_info: Option<&BranchInfo>,
189    ) -> Result<Self, WelcomeError<Provider::StorageError>> {
190        let (resumption_psk_store, key_material, group_secrets) =
191            decrypt_group_secrets(provider, mls_group_config, &welcome)?;
192
193        finish_processed_welcome(
194            provider,
195            mls_group_config,
196            welcome.ciphersuite(),
197            resumption_psk_store,
198            key_material,
199            group_secrets,
200            &welcome,
201            branch_info,
202        )
203    }
204
205    /// Get a reference to the GroupInfo in this Welcome message.
206    ///
207    /// **NOTE:** The group info contains **unverified** values. Use with caution.
208    pub fn unverified_group_info(&self) -> &VerifiableGroupInfo {
209        &self.verifiable_group_info
210    }
211
212    /// Get a reference to the PSKs in this Welcome message.
213    ///
214    /// **NOTE:** The group info contains **unverified** values. Use with caution.
215    pub fn psks(&self) -> &[PreSharedKeyId] {
216        &self.group_secrets.psks
217    }
218
219    /// Consume the `ProcessedWelcome` and combine it with the ratchet tree into
220    /// a `StagedWelcome`.
221    pub fn into_staged_welcome<Provider: OpenMlsProvider>(
222        self,
223        provider: &Provider,
224        ratchet_tree: Option<RatchetTreeIn>,
225    ) -> Result<StagedWelcome, WelcomeError<Provider::StorageError>> {
226        self.into_staged_welcome_inner(
227            provider,
228            ratchet_tree,
229            LeafNodeLifetimePolicy::Verify,
230            false,
231        )
232    }
233
234    /// Consume the `ProcessedWelcome` and combine it with the ratchet tree into
235    /// a `StagedWelcome`.
236    pub(crate) fn into_staged_welcome_inner<Provider: OpenMlsProvider>(
237        mut self,
238        provider: &Provider,
239        ratchet_tree: Option<RatchetTreeIn>,
240        validate_lifetimes: LeafNodeLifetimePolicy,
241        replace_old_group: bool,
242    ) -> Result<StagedWelcome, WelcomeError<Provider::StorageError>> {
243        // Check if we need to replace an old group
244        if !replace_old_group
245            && MlsGroup::load(provider.storage(), self.verifiable_group_info.group_id())
246                .map_err(WelcomeError::StorageError)?
247                .is_some()
248        {
249            return Err(WelcomeError::GroupAlreadyExists);
250        }
251
252        // Build the ratchet tree and group
253
254        // Set nodes either from the extension or from the `nodes_option`.
255        // If we got a ratchet tree extension in the welcome, we enable it for
256        // this group. Note that this is not strictly necessary. But there's
257        // currently no other mechanism to enable the extension.
258        let ratchet_tree = match self.verifiable_group_info.extensions().ratchet_tree() {
259            Some(extension) => extension.ratchet_tree().clone(),
260            None => match ratchet_tree {
261                Some(ratchet_tree) => ratchet_tree,
262                None => return Err(WelcomeError::MissingRatchetTree),
263            },
264        };
265
266        // Since there is currently only the external pub extension, there is no
267        // group info extension of interest here.
268        let (public_group, _group_info_extensions) = PublicGroup::from_ratchet_tree(
269            provider.crypto(),
270            ratchet_tree,
271            self.verifiable_group_info.clone(),
272            ProposalStore::new(),
273            validate_lifetimes,
274        )?;
275
276        // Find our own leaf in the tree. On the bundle path this is the leaf
277        // whose signature key matches the local KeyPackage. On the
278        // virtual-client path there is no local signature key, so the leaf is
279        // located by its derived encryption key and validated against the
280        // derivation epoch's derivation info.
281        let own_leaf_index = match &self.key_material.inner() {
282            WelcomeKeyMaterialInner::KeyPackage(key_package_bundle) => {
283                // Check that the leaf node of the added key package supports all extensions in
284                // the group context.
285                // https://validation.openmls.tech/#valn1415
286                let added_leaf_supports_all_group_context_extensions = public_group
287                    .group_context()
288                    .extensions()
289                    .iter()
290                    .all(|extension| {
291                        key_package_bundle
292                            .key_package
293                            .leaf_node()
294                            .supports_extension(&extension.extension_type())
295                    });
296                if !added_leaf_supports_all_group_context_extensions {
297                    return Err(WelcomeError::UnsupportedExtensions);
298                }
299
300                public_group
301                    .members()
302                    .find_map(|m| {
303                        if m.signature_key
304                            == key_package_bundle
305                                .key_package()
306                                .leaf_node()
307                                .signature_key()
308                                .as_slice()
309                        {
310                            Some(m.index)
311                        } else {
312                            None
313                        }
314                    })
315                    .ok_or(WelcomeError::PublicTreeError(
316                        PublicTreeError::MalformedTree,
317                    ))?
318            }
319            #[cfg(feature = "virtual-clients-draft")]
320            WelcomeKeyMaterialInner::VirtualClient(material) => {
321                find_and_validate_vc_own_leaf(provider, &public_group, material)?
322            }
323        };
324
325        struct KeyScheduleResult {
326            group_epoch_secrets: GroupEpochSecrets,
327            message_secrets: MessageSecrets,
328            #[cfg(feature = "extensions-draft")]
329            application_exporter: ApplicationExportSecret,
330        }
331        let KeyScheduleResult {
332            group_epoch_secrets,
333            message_secrets,
334            #[cfg(feature = "extensions-draft")]
335                application_exporter: application_export_secret,
336        } = {
337            let serialized_group_context = public_group
338                .group_context()
339                .tls_serialize_detached()
340                .map_err(LibraryError::missing_bound_check)?;
341
342            let EpochSecretsResult {
343                epoch_secrets,
344                #[cfg(feature = "extensions-draft")]
345                application_exporter,
346            } = self.epoch_secrets;
347
348            let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
349                serialized_group_context,
350                public_group.tree_size(),
351                own_leaf_index,
352            );
353
354            KeyScheduleResult {
355                group_epoch_secrets,
356                message_secrets,
357                #[cfg(feature = "extensions-draft")]
358                application_exporter,
359            }
360        };
361
362        let confirmation_tag = message_secrets
363            .confirmation_key()
364            .tag(
365                provider.crypto(),
366                self.ciphersuite,
367                public_group.group_context().confirmed_transcript_hash(),
368            )
369            .map_err(LibraryError::unexpected_crypto_error)?;
370
371        // Verify confirmation tag
372        // https://validation.openmls.tech/#valn1411
373        if &confirmation_tag != public_group.confirmation_tag() {
374            log::error!("Confirmation tag mismatch");
375            log_crypto!(trace, "  Got:      {:x?}", confirmation_tag);
376            log_crypto!(trace, "  Expected: {:x?}", public_group.confirmation_tag());
377
378            // in some tests we need to be able to proceed despite the tag being wrong,
379            // e.g. to test whether a later validation check is performed correctly.
380            if !crate::skip_validation::is_disabled::confirmation_tag() {
381                return Err(WelcomeError::ConfirmationTagMismatch);
382            }
383        }
384
385        let message_secrets_store = MessageSecretsStore::new_with_secret(
386            &PastEpochDeletionPolicy::MaxEpochs(0),
387            message_secrets,
388        );
389
390        // Extract and store the resumption PSK for the current epoch.
391        let resumption_psk = group_epoch_secrets.resumption_psk();
392        self.resumption_psk_store
393            .add(public_group.group_context().epoch(), resumption_psk.clone());
394
395        let welcome_sender_index = self.verifiable_group_info.signer();
396        let path_keypairs = if let Some(path_secret) = self.group_secrets.path_secret {
397            let (path_keypairs, _commit_secret) = public_group
398                .derive_path_secrets(
399                    provider.crypto(),
400                    self.ciphersuite,
401                    path_secret,
402                    welcome_sender_index,
403                    own_leaf_index,
404                )
405                .map_err(|e| match e {
406                    DerivePathError::LibraryError(e) => e.into(),
407                    DerivePathError::PublicKeyMismatch => {
408                        WelcomeError::PublicTreeError(PublicTreeError::PublicKeyMismatch)
409                    }
410                })?;
411            Some(path_keypairs)
412        } else {
413            None
414        };
415
416        let staged_welcome = StagedWelcome {
417            mls_group_config: self.mls_group_config,
418            public_group,
419            group_epoch_secrets,
420            own_leaf_index,
421            message_secrets_store,
422            #[cfg(feature = "extensions-draft")]
423            application_export_secret,
424            resumption_psk_store: self.resumption_psk_store,
425            verifiable_group_info: self.verifiable_group_info,
426            key_material: self.key_material,
427            path_keypairs,
428            #[cfg(feature = "virtual-clients-draft")]
429            emulation_group: false,
430        };
431
432        Ok(staged_welcome)
433    }
434
435    /// Exports a secret from the epoch of the group that is joined
436    /// using this [`ProcessedWelcome`].
437    /// Returns [`ExportSecretError::KeyLengthTooLong`] if the requested
438    /// key length is too long.
439    pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
440        &self,
441        crypto: &CryptoProvider,
442        label: &str,
443        context: &[u8],
444        key_length: usize,
445    ) -> Result<ExportedSecret<ProcessedWelcomeExport>, ExportSecretError> {
446        if key_length > u16::MAX as usize {
447            log::error!("Got a key that is larger than u16::MAX");
448            return Err(ExportSecretError::KeyLengthTooLong);
449        }
450
451        Ok(ExportedSecret::new(
452            self.epoch_secrets
453                .epoch_secrets
454                .exporter_secret()
455                .derive_exported_secret(self.ciphersuite, crypto, label, context, key_length)
456                .map_err(LibraryError::unexpected_crypto_error)?,
457        ))
458    }
459
460    /// Retrieve a reference to the own [`KeyPackage`] that was retrieved from local storage as
461    /// part of [`Welcome`] processing, and is used to build the group
462    pub fn own_key_package(&self) -> Option<&KeyPackage> {
463        self.key_material
464            .key_package_bundle()
465            .map(|bundle| bundle.key_package())
466    }
467}
468
469impl StagedWelcome {
470    /// Creates a new staged welcome from a [`Welcome`] message. Returns an error
471    /// ([`WelcomeError::NoMatchingKeyPackage`]) if no [`KeyPackage`]
472    /// can be found.
473    /// Note: calling this function will consume the key material for decrypting the [`Welcome`]
474    /// message, even if the caller does not turn the [`StagedWelcome`] into an [`MlsGroup`].
475    ///
476    /// [`Welcome`]: crate::messages::Welcome
477    pub fn new_from_welcome<Provider: OpenMlsProvider>(
478        provider: &Provider,
479        mls_group_config: &MlsGroupJoinConfig,
480        welcome: Welcome,
481        ratchet_tree: Option<RatchetTreeIn>,
482    ) -> Result<Self, WelcomeError<Provider::StorageError>> {
483        let processed_welcome =
484            ProcessedWelcome::new_from_welcome(provider, mls_group_config, welcome)?;
485
486        processed_welcome.into_staged_welcome(provider, ratchet_tree)
487    }
488
489    /// Similar to [`StagedWelcome::new_from_welcome`] but as a builder.
490    ///
491    /// The builder allows to set the ratchet tree, skip leaf node lifetime
492    /// validation, and get the [`ProcessedWelcome`] for inspection before staging.
493    pub fn build_from_welcome<'a, Provider: OpenMlsProvider>(
494        provider: &'a Provider,
495        mls_group_config: &MlsGroupJoinConfig,
496        welcome: Welcome,
497    ) -> Result<JoinBuilder<'a, Provider>, WelcomeError<Provider::StorageError>> {
498        let processed_welcome =
499            ProcessedWelcome::new_from_welcome(provider, mls_group_config, welcome)?;
500
501        // processed_welcome.into_staged_welcome(provider, ratchet_tree)
502        Ok(JoinBuilder::new(provider, processed_welcome))
503    }
504
505    /// Builder to create a [`StagedWelcome`] for a subgroup branched from a
506    /// parent group, as described in [RFC 9420 §11.3].
507    ///
508    /// The parent group's parameters are provided via `branch_info`, which the
509    /// parent exports with
510    /// [`MlsGroup::branch_info`](crate::group::MlsGroup::branch_info).
511    ///
512    /// In addition to the regular [`StagedWelcome::build_from_welcome`]
513    /// processing, this injects the parent's resumption PSK secret (which is
514    /// required to derive the subgroup's key schedule from the branch PSK) and
515    /// enforces the receiver-side checks the RFC mandates when joining a branched
516    /// subgroup.
517    ///
518    /// The branch PSK carried in the `Welcome` must reference the same parent
519    /// group and epoch as the supplied `branch_info`; otherwise the injected
520    /// resumption PSK secret would be for the wrong epoch. This is checked here,
521    /// before the secret is mixed into the key schedule, and fails with
522    /// [`WelcomeError::SubgroupParentMismatch`].
523    ///
524    /// The remaining checks run when [`JoinBuilder::build`] is called:
525    ///
526    /// * the protocol version and ciphersuite match the parent group,
527    /// * the subgroup is at epoch 1, and
528    /// * every member of the subgroup matches a member of the parent group
529    ///   (unless disabled via [`JoinBuilder::check_members`]).
530    ///
531    /// Matching members is left to the application by the RFC; here we use
532    /// credential equality for equivalent identifiers.
533    ///
534    /// If the receiver does not yet know which parent epoch the branch was taken
535    /// from (its own view of the parent group may have advanced), use
536    /// [`StagedWelcome::process_branch_welcome`] to read the parent reference
537    /// from the `Welcome` first and then pick the matching `branch_info`. That
538    /// path decrypts the `Welcome` only once. This one-shot method is a
539    /// convenience for callers that already know the parent epoch; it also
540    /// decrypts only once, via the same carrier.
541    ///
542    /// [RFC 9420 §11.3]: https://www.rfc-editor.org/rfc/rfc9420.html#name-subgroup-branching
543    pub fn build_from_branch<'a, Provider: OpenMlsProvider>(
544        provider: &'a Provider,
545        mls_group_config: &MlsGroupJoinConfig,
546        welcome: Welcome,
547        branch_info: BranchInfo,
548    ) -> Result<JoinBuilder<'a, Provider>, WelcomeError<Provider::StorageError>> {
549        Self::process_branch_welcome(provider, mls_group_config, welcome)?
550            .build_from_branch(provider, branch_info)
551    }
552
553    /// Decrypt a subgroup-branch `Welcome` once so its parent reference can be
554    /// inspected before selecting the matching [`BranchInfo`].
555    ///
556    /// The branch PSK's parent group and epoch live inside the `Welcome`'s
557    /// encrypted `GroupSecrets`, so reading them requires decryption. This
558    /// returns a [`PendingBranchWelcome`] holding the decrypted state: call
559    /// [`PendingBranchWelcome::parent`] to read the parent `(group_id, epoch)`
560    /// the branch derives from (RFC 9420 §8.4), select the `BranchInfo` for that
561    /// parent epoch (e.g. from a sliding window of recent epochs), then finish
562    /// with [`PendingBranchWelcome::build_from_branch`] — without decrypting the
563    /// `Welcome` a second time.
564    ///
565    /// **Note:** like [`StagedWelcome::build_from_branch`], this path assumes a
566    /// subgroup-branch `Welcome`. It consumes the matching key package from
567    /// storage. If [`PendingBranchWelcome::parent`] returns `None` the `Welcome`
568    /// is not a branch welcome and the carrier cannot complete a regular join
569    /// (the key package is already consumed); use the normal
570    /// [`ProcessedWelcome`] flow when the message may not be a branch welcome.
571    pub fn process_branch_welcome<Provider: OpenMlsProvider>(
572        provider: &Provider,
573        mls_group_config: &MlsGroupJoinConfig,
574        welcome: Welcome,
575    ) -> Result<PendingBranchWelcome, WelcomeError<Provider::StorageError>> {
576        let (resumption_psk_store, key_material, group_secrets) =
577            decrypt_group_secrets(provider, mls_group_config, &welcome)?;
578
579        Ok(PendingBranchWelcome {
580            mls_group_config: mls_group_config.clone(),
581            ciphersuite: welcome.ciphersuite(),
582            welcome,
583            resumption_psk_store,
584            key_material,
585            group_secrets,
586        })
587    }
588
589    /// Returns the [`LeafNodeIndex`] of the group member that authored the [`Welcome`] message.
590    ///
591    /// [`Welcome`]: crate::messages::Welcome
592    pub fn welcome_sender_index(&self) -> LeafNodeIndex {
593        self.verifiable_group_info.signer()
594    }
595
596    /// Returns the [`LeafNode`] of the group member that authored the [`Welcome`] message.
597    ///
598    /// [`Welcome`]: crate::messages::Welcome
599    pub fn welcome_sender(&self) -> Result<&LeafNode, LibraryError> {
600        let sender_index = self.welcome_sender_index();
601        self.public_group
602            .leaf(sender_index)
603            .ok_or_else(|| LibraryError::custom("no leaf with given welcome sender index exists"))
604    }
605
606    /// Returns the leaf index of the client in this welcome's [`PublicGroup`].
607    pub fn own_leaf_index(&self) -> LeafNodeIndex {
608        self.own_leaf_index
609    }
610
611    /// Returns the leaf node of the client in this welcome's [`PublicGroup`].
612    pub fn own_leaf_node(&self) -> Option<&LeafNode> {
613        self.public_group.leaf(self.own_leaf_index())
614    }
615
616    /// Get the [`GroupContext`] of this welcome's [`PublicGroup`].
617    pub fn group_context(&self) -> &GroupContext {
618        self.public_group.group_context()
619    }
620
621    /// Get an iterator over all [`Member`]s of this welcome's [`PublicGroup`].
622    pub fn members(&self) -> impl Iterator<Item = Member> + '_ {
623        self.public_group.members()
624    }
625
626    /// Get the [`ApplicationExportSecret`] of this welcome.
627    #[cfg(feature = "extensions-draft")]
628    pub fn application_export_secret(&self) -> &ApplicationExportSecret {
629        &self.application_export_secret
630    }
631
632    /// Join the group as an emulation group of a virtual client. See
633    /// [`MlsGroupCreateConfigBuilder::emulation_group`] for what an emulation
634    /// group is.
635    ///
636    /// Nothing on the wire marks a group as an emulation group, so a joiner has
637    /// to set this itself. Joining an emulation group without it leaves the
638    /// virtual client's secrets underived.
639    ///
640    /// [`MlsGroupCreateConfigBuilder::emulation_group`]: crate::group::MlsGroupCreateConfigBuilder::emulation_group
641    #[cfg(feature = "virtual-clients-draft")]
642    pub fn emulation_group(mut self, emulation_group: bool) -> Self {
643        self.emulation_group = emulation_group;
644        self
645    }
646
647    /// Consumes the [`StagedWelcome`] and returns the respective [`MlsGroup`].
648    pub fn into_group<Provider: OpenMlsProvider>(
649        self,
650        provider: &Provider,
651    ) -> Result<MlsGroup, WelcomeError<Provider::StorageError>> {
652        // If we got a path secret, derive the path (which also checks if the
653        // public keys match) and store the derived keys in the key store.
654        let group_keypairs = if let Some(path_keypairs) = self.path_keypairs {
655            let mut keypairs = vec![self.key_material.encryption_key_pair()];
656            keypairs.extend_from_slice(&path_keypairs);
657            keypairs
658        } else {
659            vec![self.key_material.encryption_key_pair()]
660        };
661
662        #[cfg(feature = "extensions-draft")]
663        #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
664        let mut application_export_tree =
665            ApplicationExportTree::new(self.application_export_secret);
666
667        // The epoch the Welcome hands us is a derivation epoch of the emulation
668        // group: the commit that created it added us, so it changed membership.
669        #[cfg(feature = "virtual-clients-draft")]
670        if self.emulation_group {
671            register_vc_derivation_epoch(
672                provider.crypto(),
673                provider.storage(),
674                Some(&mut application_export_tree),
675                VcDerivationEpochParams::for_public_group(
676                    &self.public_group,
677                    self.own_leaf_index,
678                    self.mls_group_config
679                        .vc_derivation_epoch_retention_policy()
680                        .clone(),
681                ),
682            )?;
683        }
684
685        // A join via a virtual-client KeyPackage lands on a leaf shared with
686        // sibling emulator clients. Bind the joined epoch to the leaf's
687        // derivation epoch, as an external-commit join does, so that messages
688        // siblings send from the shared leaf can be deprotected instead of
689        // being mistaken for our own echo.
690        #[cfg(feature = "virtual-clients-draft")]
691        if let Some(derivation_info) = self
692            .public_group
693            .leaf(self.own_leaf_index)
694            .map(LeafNode::vc_derivation_info)
695            .transpose()?
696            .flatten()
697        {
698            let epoch_id = derivation_info.epoch_id().clone();
699            let group_id = self.public_group.group_id();
700            provider
701                .storage()
702                .vc_derivation_epoch_state::<_, VcDerivationEpochState>(&epoch_id)
703                .map_err(WelcomeError::StorageError)?
704                .ok_or(WelcomeError::VirtualClientsError(
705                    VirtualClientsError::MissingDerivationEpochState,
706                ))?;
707            // Keep one binding per retained message-secrets epoch plus the
708            // current one, matching the other VC group-entry paths.
709            let max_entries = self.message_secrets_store.max_epochs.saturating_add(1);
710            write_vc_emulation_binding_with_pruning(
711                provider.storage(),
712                group_id,
713                self.public_group.group_context().epoch(),
714                epoch_id,
715                max_entries,
716            )
717            .map_err(WelcomeError::StorageError)?;
718        }
719
720        let past_epoch_deletion_policy = self.mls_group_config.past_epoch_deletion_policy().clone();
721
722        let mut mls_group = MlsGroup {
723            mls_group_config: self.mls_group_config,
724            own_leaf_nodes: vec![],
725            aad: vec![],
726            #[cfg(feature = "extensions-draft")]
727            safe_aad: SafeAad::empty(),
728            group_state: MlsGroupState::Operational,
729            public_group: self.public_group,
730            group_epoch_secrets: self.group_epoch_secrets,
731            own_leaf_index: self.own_leaf_index,
732            message_secrets_store: self.message_secrets_store,
733            resumption_psk_store: self.resumption_psk_store,
734            #[cfg(feature = "extensions-draft")]
735            application_export_tree: Some(application_export_tree),
736            #[cfg(feature = "virtual-clients-draft")]
737            emulation_group: self.emulation_group,
738        };
739
740        mls_group
741            .store_epoch_keypairs(provider.storage(), group_keypairs.as_slice())
742            .map_err(WelcomeError::StorageError)?;
743        // resize the store
744        mls_group.resize_message_secrets_store(&past_epoch_deletion_policy);
745
746        // A join through a virtual client's KeyPackage binds the joined epoch
747        // to the KeyPackage's derivation epoch. The binding takes over the
748        // epoch reference from the retained KeyPackage material, which
749        // `keys_for_welcome` left in storage for that purpose. (a bound group
750        // is required for the reuse-guard MUST).
751        #[cfg(feature = "virtual-clients-draft")]
752        if let Some(material) = self.key_material.vc_welcome_material() {
753            let max_entries = mls_group.message_secrets_store.max_epochs.saturating_add(1);
754            write_vc_emulation_binding_with_pruning(
755                provider.storage(),
756                mls_group.group_id(),
757                mls_group.epoch(),
758                material.epoch_id.clone(),
759                max_entries,
760            )
761            .map_err(WelcomeError::StorageError)?;
762            provider
763                .storage()
764                .delete_retained_key_package_material(&material.key_package_ref)
765                .map_err(WelcomeError::StorageError)?;
766        }
767
768        mls_group
769            .store(provider.storage())
770            .map_err(WelcomeError::StorageError)?;
771
772        Ok(mls_group)
773    }
774
775    /// Exports a secret from the epoch of the group that is joined
776    /// using this [`StagedWelcome`].
777    /// Returns [`ExportSecretError::KeyLengthTooLong`] if the requested
778    /// key length is too long.
779    pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
780        &self,
781        crypto: &CryptoProvider,
782        label: &str,
783        context: &[u8],
784        key_length: usize,
785    ) -> Result<ExportedSecret<StagedWelcomeExport>, ExportSecretError> {
786        if key_length > u16::MAX as usize {
787            log::error!("Got a key that is larger than u16::MAX");
788            return Err(ExportSecretError::KeyLengthTooLong);
789        }
790
791        Ok(ExportedSecret::new(
792            self.group_epoch_secrets
793                .exporter_secret()
794                .derive_exported_secret(
795                    self.group_context().ciphersuite(),
796                    crypto,
797                    label,
798                    context,
799                    key_length,
800                )
801                .map_err(LibraryError::unexpected_crypto_error)?,
802        ))
803    }
804}
805
806/// A subgroup-branch `Welcome` whose `GroupSecrets` have been decrypted but not
807/// yet staged.
808///
809/// This lets a receiver read which parent group and epoch the branch derives
810/// from before committing to a [`BranchInfo`].
811/// The decrypted state is carried here and reused when finishing the join.
812/// Create it with [`StagedWelcome::process_branch_welcome`], read the parent
813/// reference with [`Self::parent`], then finish with [`Self::build_from_branch`].
814pub struct PendingBranchWelcome {
815    mls_group_config: MlsGroupJoinConfig,
816    ciphersuite: Ciphersuite,
817    welcome: Welcome,
818    resumption_psk_store: ResumptionPskStore,
819    key_material: WelcomeKeyMaterial,
820    group_secrets: GroupSecrets,
821}
822
823impl PendingBranchWelcome {
824    /// The parent group and epoch this branch derives from, read from the branch
825    /// resumption PSK carried in the `Welcome`.
826    ///
827    /// Use this to select the [`BranchInfo`] exported from the matching parent
828    /// epoch before calling [`Self::build_from_branch`].
829    ///
830    /// Returns `None` if the `Welcome` carries no branch resumption PSK, i.e. it
831    /// is not a subgroup-branch welcome.
832    pub fn parent(&self) -> Option<(GroupId, GroupEpoch)> {
833        self.group_secrets
834            .psks
835            .iter()
836            .find_map(|id| match id.psk() {
837                Psk::Resumption(r) if r.usage() == ResumptionPskUsage::Branch => {
838                    Some((r.psk_group_id().clone(), r.psk_epoch()))
839                }
840                _ => None,
841            })
842    }
843
844    /// Finish the subgroup-branch join with the selected [`BranchInfo`], reusing
845    /// the already-decrypted state (no second decrypt of the `Welcome`).
846    ///
847    /// The branch PSK's parent reference is checked against `branch_info` (see
848    /// [`StagedWelcome::build_from_branch`]); a `branch_info` from the wrong
849    /// parent epoch fails with [`WelcomeError::SubgroupParentMismatch`]. The
850    /// remaining receiver checks run when [`JoinBuilder::build`] is called.
851    pub fn build_from_branch<'a, Provider: OpenMlsProvider>(
852        self,
853        provider: &'a Provider,
854        branch_info: BranchInfo,
855    ) -> Result<JoinBuilder<'a, Provider>, WelcomeError<Provider::StorageError>> {
856        let processed_welcome = finish_processed_welcome(
857            provider,
858            &self.mls_group_config,
859            self.ciphersuite,
860            self.resumption_psk_store,
861            self.key_material,
862            self.group_secrets,
863            &self.welcome,
864            Some(&branch_info),
865        )?;
866
867        Ok(JoinBuilder::new(provider, processed_welcome).with_branch_info(branch_info))
868    }
869}
870
871/// Decrypt a `Welcome`'s `GroupSecrets`.
872///
873/// This is the first half of welcome processing, shared between the regular
874/// join and the subgroup-branch peek (see [`PendingBranchWelcome`]). It consumes
875/// the matching (non-last-resort) key package from storage via
876/// [`keys_for_welcome`] and decrypts the encrypted group secrets addressed to
877/// it. Retained virtual-client material is read but not consumed, see
878/// [`keys_for_welcome`]. The branch resumption PSK secret is not injected
879/// here: injection and the parent-reference check happen in
880/// [`finish_processed_welcome`], so this step is identical on both paths.
881fn decrypt_group_secrets<Provider: OpenMlsProvider>(
882    provider: &Provider,
883    mls_group_config: &MlsGroupJoinConfig,
884    welcome: &Welcome,
885) -> Result<
886    (ResumptionPskStore, WelcomeKeyMaterial, GroupSecrets),
887    WelcomeError<<Provider as OpenMlsProvider>::StorageError>,
888> {
889    let ciphersuite = welcome.ciphersuite();
890    // Check this before touching any stored key material: `keys_for_welcome`
891    // consumes a matching (non-last-resort) key package.
892    provider
893        .crypto()
894        .supports(ciphersuite)
895        .map_err(|_| WelcomeError::UnsupportedCiphersuite(ciphersuite))?;
896
897    let (resumption_psk_store, key_material) =
898        keys_for_welcome(mls_group_config, welcome, provider)?;
899
900    let Some(egs) =
901        welcome.find_encrypted_group_secret(key_material.key_package_ref(provider.crypto())?)
902    else {
903        return Err(WelcomeError::JoinerSecretNotFound);
904    };
905
906    // This check seems to be superfluous from the perspective of the RFC, but still doesn't
907    // seem like a bad idea. There is no local KeyPackage to compare against on the
908    // virtual-client path, where the derived material is implicitly the welcome's ciphersuite.
909    if let Some(key_package_bundle) = key_material.key_package_bundle() {
910        if welcome.ciphersuite() != key_package_bundle.key_package().ciphersuite() {
911            let e = WelcomeError::CiphersuiteMismatch;
912            log::debug!("new_from_welcome {e:?}");
913            return Err(e);
914        }
915    }
916
917    let group_secrets = GroupSecrets::try_from_ciphertext(
918        key_material.init_private_key(),
919        egs.encrypted_group_secrets(),
920        welcome.encrypted_group_info(),
921        ciphersuite,
922        provider.crypto(),
923    )?;
924
925    // Validate PSKs
926    PreSharedKeyId::validate_in_welcome(&group_secrets.psks, ciphersuite)?;
927
928    Ok((resumption_psk_store, key_material, group_secrets))
929}
930
931/// Finish processing a `Welcome` from its already-decrypted `GroupSecrets`.
932///
933/// This is the second half of welcome processing (the tail of
934/// [`ProcessedWelcome::new_from_welcome_inner`]): it derives the key schedule,
935/// decrypts the group info, and runs the capability/ciphersuite checks,
936/// producing a [`ProcessedWelcome`]. It still needs the `welcome` for its
937/// encrypted group info.
938///
939/// For subgroup branching (`branch_info` set), the parent group's resumption
940/// PSK secret is injected at the sentinel epoch 0 and the branch PSK carried in
941/// the `Welcome` is checked to reference the same parent group and epoch as
942/// `branch_info` — both before the secret is mixed into the key schedule, so a
943/// wrong-epoch secret fails cleanly with [`WelcomeError::SubgroupParentMismatch`]
944/// rather than as an opaque group-info decryption failure further down.
945#[allow(clippy::too_many_arguments)]
946fn finish_processed_welcome<Provider: OpenMlsProvider>(
947    provider: &Provider,
948    mls_group_config: &MlsGroupJoinConfig,
949    ciphersuite: Ciphersuite,
950    mut resumption_psk_store: ResumptionPskStore,
951    key_material: WelcomeKeyMaterial,
952    group_secrets: GroupSecrets,
953    welcome: &Welcome,
954    branch_info: Option<&BranchInfo>,
955) -> Result<ProcessedWelcome, WelcomeError<<Provider as OpenMlsProvider>::StorageError>> {
956    // For subgroup branching, inject the parent group's resumption PSK at
957    // the sentinel epoch 0 before the PSKs are loaded.
958    if let Some(branch_info) = branch_info {
959        resumption_psk_store.add(0.into(), branch_info.resumption_psk_secret().clone());
960    }
961
962    // When joining a subgroup branch, the branch resumption
963    // PSK carried in the Welcome must reference the same parent group and
964    // epoch the receiver is branching from. This must be checked here, before
965    // the injected PSK secret is mixed into the key schedule: a wrong-epoch
966    // secret would otherwise only surface as an opaque group-info decryption
967    // failure further down.
968    if let Some(branch_info) = branch_info {
969        let parent_matches = group_secrets
970            .psks
971            .iter()
972            .filter_map(|id| match id.psk() {
973                Psk::Resumption(r) if r.usage() == ResumptionPskUsage::Branch => Some(r),
974                _ => None,
975            })
976            .any(|r| {
977                r.psk_group_id() == branch_info.group_id() && r.psk_epoch() == branch_info.epoch()
978            });
979        if !parent_matches {
980            return Err(WelcomeError::SubgroupParentMismatch);
981        }
982    }
983
984    let psk_secret = {
985        let psks = load_psks(
986            provider.storage(),
987            &resumption_psk_store,
988            &group_secrets.psks,
989        )?;
990
991        PskSecret::new(provider.crypto(), ciphersuite, psks)?
992    };
993
994    // prepare the key schedule
995    let mut key_schedule = KeySchedule::init(
996        ciphersuite,
997        provider.crypto(),
998        &group_secrets.joiner_secret,
999        psk_secret,
1000    )?;
1001
1002    // derive the keys for decrypting the group info
1003    let (welcome_key, welcome_nonce) = key_schedule
1004        .welcome(provider.crypto(), ciphersuite)
1005        .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?
1006        .derive_welcome_key_nonce(provider.crypto(), ciphersuite)
1007        .map_err(LibraryError::unexpected_crypto_error)?;
1008
1009    let verifiable_group_info = VerifiableGroupInfo::try_from_ciphertext(
1010        &welcome_key,
1011        &welcome_nonce,
1012        welcome.encrypted_group_info(),
1013        &[],
1014        provider.crypto(),
1015    )?;
1016
1017    let serialized_group_context = verifiable_group_info
1018        .group_context()
1019        .tls_serialize_detached()
1020        .map_err(LibraryError::missing_bound_check)?;
1021
1022    key_schedule.add_context(provider.crypto(), &serialized_group_context)?;
1023
1024    let epoch_secrets = key_schedule.epoch_secrets(provider.crypto(), ciphersuite)?;
1025
1026    // On the bundle path, check the required capabilities and the
1027    // ciphersuite against the local KeyPackage. On the virtual-client path
1028    // there is no local KeyPackage: these are checked in staging against
1029    // the own tree leaf instead.
1030    if let Some(key_package_bundle) = key_material.key_package_bundle() {
1031        if let Some(required_capabilities) =
1032            verifiable_group_info.extensions().required_capabilities()
1033        {
1034            // Also check that our key package actually supports the extensions.
1035            // As per the spec, the sender must have checked this. But you never know.
1036            key_package_bundle
1037                .key_package()
1038                .leaf_node()
1039                .capabilities()
1040                .supports_required_capabilities(required_capabilities)?;
1041        }
1042
1043        // https://validation.openmls.tech/#valn1404
1044        // Verify that the cipher_suite in the GroupInfo matches the cipher_suite in the
1045        // KeyPackage.
1046        if verifiable_group_info.ciphersuite() != key_package_bundle.key_package().ciphersuite() {
1047            let e = WelcomeError::CiphersuiteMismatch;
1048            log::debug!("new_from_welcome {e:?}");
1049            return Err(e);
1050        }
1051    }
1052
1053    Ok(ProcessedWelcome {
1054        mls_group_config: mls_group_config.clone(),
1055        ciphersuite,
1056        group_secrets,
1057        epoch_secrets,
1058        verifiable_group_info,
1059        resumption_psk_store,
1060        key_material,
1061    })
1062}
1063
1064/// Read keys for decrypting the welcome message.
1065fn keys_for_welcome<Provider: OpenMlsProvider>(
1066    mls_group_config: &MlsGroupJoinConfig,
1067    welcome: &Welcome,
1068    provider: &Provider,
1069) -> Result<
1070    (ResumptionPskStore, WelcomeKeyMaterial),
1071    WelcomeError<<Provider as OpenMlsProvider>::StorageError>,
1072> {
1073    let resumption_psk_store = ResumptionPskStore::new(mls_group_config.number_of_resumption_psks);
1074
1075    for egs in welcome.secrets() {
1076        let hash_ref = egs.new_member();
1077        if let Some(key_package_bundle) = provider
1078            .storage()
1079            .key_package(&hash_ref)
1080            .map_err(WelcomeError::StorageError)?
1081        {
1082            let key_package_bundle: KeyPackageBundle = key_package_bundle;
1083            if !key_package_bundle.key_package().last_resort() {
1084                provider
1085                    .storage()
1086                    .delete_key_package(
1087                        &key_package_bundle.key_package.hash_ref(provider.crypto())?,
1088                    )
1089                    .map_err(WelcomeError::StorageError)?;
1090            } else {
1091                log::debug!("Key package has last resort extension, not deleting");
1092            }
1093            return Ok((
1094                resumption_psk_store,
1095                WelcomeKeyMaterial::with_key_package_bundle(key_package_bundle),
1096            ));
1097        }
1098
1099        #[cfg(feature = "virtual-clients-draft")]
1100        if let Some(material) =
1101            resolve_vc_welcome_material(provider, welcome.ciphersuite(), &hash_ref)?
1102        {
1103            // The retained material stays in storage for now.
1104            return Ok((
1105                resumption_psk_store,
1106                WelcomeKeyMaterial::with_vc_welcome_material(material),
1107            ));
1108        }
1109    }
1110
1111    Err(WelcomeError::NoMatchingKeyPackage)
1112}
1113
1114/// Try to derive virtual-client welcome material for `hash_ref`. Returns
1115/// `None` if no [`RetainedKeyPackageMaterial`] is stored for the ref, so the
1116/// caller can keep scanning the welcome's encrypted group secrets.
1117///
1118/// On a match this reconstructs the per-KeyPackage seed pinned in the retained
1119/// material at upload-processing time, and
1120/// derives the init and leaf-encryption keypairs from the seed under the
1121/// Welcome's ciphersuite. It does not touch the operation tree: the batch
1122/// generation was already consumed once when the upload was processed, and the
1123/// seed is enough to reproduce the keys.
1124///
1125/// [`RetainedKeyPackageMaterial`]: RetainedKeyPackageMaterial
1126#[cfg(feature = "virtual-clients-draft")]
1127pub(crate) fn resolve_vc_welcome_material<Provider: OpenMlsProvider>(
1128    provider: &Provider,
1129    ciphersuite: Ciphersuite,
1130    hash_ref: &KeyPackageRef,
1131) -> Result<Option<VcWelcomeMaterial>, WelcomeError<<Provider as OpenMlsProvider>::StorageError>> {
1132    let storage = provider.storage();
1133    let Some(material) = storage
1134        .retained_key_package_material::<_, RetainedKeyPackageMaterial>(hash_ref)
1135        .map_err(WelcomeError::StorageError)?
1136    else {
1137        return Ok(None);
1138    };
1139
1140    if material.key_package_ciphersuite != ciphersuite {
1141        return Err(WelcomeError::CiphersuiteMismatch);
1142    }
1143
1144    // The seed is imported into the KeyPackage's ciphersuite at
1145    // upload-processing time. The init and leaf-encryption keys are derived
1146    // from it under the KeyPackage's own (the welcome's) ciphersuite.
1147    let crypto = provider.crypto();
1148    let init_key_pair = material
1149        .key_package_seed_secret
1150        .derive_init_key_secret(crypto, ciphersuite)?
1151        .generate_init_key_pair(crypto, ciphersuite)?;
1152    let encryption_keypair = material
1153        .key_package_seed_secret
1154        .derive_encryption_key_secret(crypto, ciphersuite)?
1155        .generate_encryption_key_pair(crypto, ciphersuite)?;
1156
1157    Ok(Some(VcWelcomeMaterial {
1158        key_package_ref: hash_ref.clone(),
1159        epoch_id: material.epoch_id,
1160        leaf_index: material.leaf_index,
1161        generation: material.generation,
1162        key_package_index: material.key_package_index,
1163        init_private_key: init_key_pair.private,
1164        init_key: init_key_pair.public.into(),
1165        encryption_keypair,
1166    }))
1167}
1168
1169/// Find and validate the virtual client's own leaf in the ratchet tree.
1170///
1171/// A virtual client has no signature key of its own, so the leaf is located by
1172/// the derivation info the VC sender embedded under [`VC_COMPONENT_ID`]: the
1173/// leaf whose cleartext `epoch_id` equals the material's and whose
1174/// `encryption_key` equals the key derived in the first welcome stage. That
1175/// leaf's encrypted [`DerivationInfoTbe`] is then decrypted with the derivation
1176/// epoch state and its `leaf_index`, `generation`, and `key_package_index`
1177/// must equal the material's.
1178///
1179/// [`VC_COMPONENT_ID`]: crate::components::vc_derivation_info::VC_COMPONENT_ID
1180/// [`DerivationInfoTbe`]: crate::components::vc_derivation_info::DerivationInfoTbe
1181#[cfg(feature = "virtual-clients-draft")]
1182fn find_and_validate_vc_own_leaf<Provider: OpenMlsProvider>(
1183    provider: &Provider,
1184    public_group: &PublicGroup,
1185    material: &VcWelcomeMaterial,
1186) -> Result<LeafNodeIndex, WelcomeError<<Provider as OpenMlsProvider>::StorageError>> {
1187    use tls_codec::Serialize as _;
1188
1189    let crypto = provider.crypto();
1190    let derived_encryption_key = material.encryption_keypair.public_key().as_slice().to_vec();
1191
1192    let own_index = public_group
1193        .members()
1194        .find(|m| m.encryption_key == derived_encryption_key)
1195        .map(|m| m.index)
1196        .ok_or(WelcomeError::PublicTreeError(
1197            PublicTreeError::MalformedTree,
1198        ))?;
1199
1200    let own_leaf = public_group
1201        .leaf(own_index)
1202        .ok_or(WelcomeError::PublicTreeError(
1203            PublicTreeError::MalformedTree,
1204        ))?;
1205
1206    let derivation_info = own_leaf
1207        .vc_derivation_info()?
1208        .ok_or(VirtualClientsError::VcComponentNotListed)?;
1209    if derivation_info.epoch_id() != &material.epoch_id {
1210        log::error!("vc: welcome leaf epoch id does not match the retained material");
1211        return Err(VirtualClientsError::DerivationInfoMalformed.into());
1212    }
1213
1214    let state: VcDerivationEpochState = provider
1215        .storage()
1216        .vc_derivation_epoch_state(&material.epoch_id)
1217        .map_err(|e| {
1218            log::error!("vc: load derivation epoch state in welcome staging failed: {e:?}");
1219            VirtualClientsError::StorageError
1220        })?
1221        .ok_or(VirtualClientsError::MissingDerivationEpochState)?;
1222    let (_state_leaf_index, epoch_encryption_key, emulation_ciphersuite) = state.into_parts();
1223
1224    let leaf_encryption_key = own_leaf
1225        .encryption_key()
1226        .tls_serialize_detached()
1227        .map_err(VirtualClientsError::from)?;
1228    // A KeyPackage leaf carries the `KeyPackage` variant of the tagless
1229    // select, so it decodes with `key_package_index` present.
1230    let tbe = derivation_info.decrypt(
1231        crypto,
1232        emulation_ciphersuite,
1233        &epoch_encryption_key,
1234        &leaf_encryption_key,
1235        VirtualClientOperationType::KeyPackage,
1236    )?;
1237
1238    let DerivationInfoTbe::KeyPackage {
1239        leaf_index,
1240        generation,
1241        key_package_index,
1242    } = tbe
1243    else {
1244        log::error!("vc: welcome leaf derivation info is not a key-package variant");
1245        return Err(VirtualClientsError::DerivationInfoMalformed.into());
1246    };
1247    if leaf_index != material.leaf_index
1248        || generation != material.generation
1249        || key_package_index != material.key_package_index
1250    {
1251        log::error!("vc: welcome leaf derivation info does not match the retained material");
1252        return Err(VirtualClientsError::DerivationInfoMalformed.into());
1253    }
1254
1255    Ok(own_index)
1256}
1257
1258#[cfg(feature = "virtual-clients-draft")]
1259impl MlsGroup {
1260    /// Returns a new [`VcExternalCommitJoinBuilder`] for joining a
1261    /// higher-level group as a virtual client's sibling emulator client, by
1262    /// processing another sibling's external commit.
1263    pub fn vc_external_commit_join_builder() -> VcExternalCommitJoinBuilder {
1264        VcExternalCommitJoinBuilder::new()
1265    }
1266
1267    /// Bootstrap a virtual client's sibling emulator client into a higher-level
1268    /// group the virtual client created, when this client is not yet a member.
1269    ///
1270    /// The creator emulator client built the group with
1271    /// [`MlsGroupBuilder::vc_emulation`], its `key_package`-sourced leaf key
1272    /// material derived from a `key_package` operation secret. Sharing the same
1273    /// derivation epoch (`epoch_id`), this client reconstructs the epoch-0 state:
1274    /// it verifies the GroupInfo and single-leaf ratchet tree (which may instead
1275    /// travel in the GroupInfo's `ratchet_tree` extension), rederives the creator
1276    /// leaf's key material from the shared operation secret tree, and derives the
1277    /// same epoch-0 `epoch_secret` from the creator's KeyPackage seed. No secret
1278    /// travels on the wire. On success the returned group sits at epoch 0 with
1279    /// this client on the shared virtual-client leaf (index 0).
1280    ///
1281    /// [`MlsGroupBuilder::vc_emulation`]: crate::group::MlsGroupBuilder::vc_emulation
1282    pub fn vc_join_at_creation<Provider: OpenMlsProvider>(
1283        provider: &Provider,
1284        join_config: &MlsGroupJoinConfig,
1285        verifiable_group_info: VerifiableGroupInfo,
1286        ratchet_tree: Option<RatchetTreeIn>,
1287        epoch_id: EpochId,
1288    ) -> Result<MlsGroup, VcGroupCreationJoinError<Provider::StorageError>> {
1289        use tls_codec::Serialize as _;
1290
1291        type Error<S> = VcGroupCreationJoinError<S>;
1292
1293        // Resolve the ratchet tree (from the GroupInfo extension or the
1294        // argument) and verify the GroupInfo and tree.
1295        let ratchet_tree = match verifiable_group_info.extensions().ratchet_tree() {
1296            Some(extension) => extension.ratchet_tree().clone(),
1297            None => ratchet_tree.ok_or(Error::MissingRatchetTree)?,
1298        };
1299        let (public_group, group_info) = PublicGroup::from_ratchet_tree(
1300            provider.crypto(),
1301            ratchet_tree,
1302            verifiable_group_info,
1303            ProposalStore::new(),
1304            LeafNodeLifetimePolicy::default(),
1305        )?;
1306        let ciphersuite = public_group.ciphersuite();
1307
1308        // The created group consists of exactly the creator's leaf at index 0.
1309        if public_group.members().count() != 1 {
1310            return Err(Error::NotASingleLeafTree);
1311        }
1312        let creator_index = LeafNodeIndex::new(0);
1313        let creator_leaf = public_group
1314            .leaf(creator_index)
1315            .ok_or(Error::NotASingleLeafTree)?;
1316
1317        // A virtual client's group-creation leaf is key_package-sourced.
1318        let LeafNodeSource::KeyPackage(_) = creator_leaf.leaf_node_source() else {
1319            return Err(Error::CreatorLeafNotKeyPackageSourced);
1320        };
1321
1322        // Read the creator leaf's derivation info and check the derivation epoch.
1323        let derivation_info = creator_leaf
1324            .vc_derivation_info()?
1325            .ok_or(Error::MissingDerivationInfo)?;
1326        if derivation_info.epoch_id() != &epoch_id {
1327            return Err(Error::EpochIdMismatch);
1328        }
1329
1330        // Load the derivation epoch state and operation tree.
1331        let (state, mut operation_tree) = load_vc_epoch_state_and_tree(provider, &epoch_id)?;
1332        let (_leaf_index, epoch_encryption_key, emulation_ciphersuite) = state.into_parts();
1333
1334        // Decrypt the derivation info (KeyPackage case) and read the batch index
1335        // the creator's seed was derived under.
1336        let leaf_encryption_key = creator_leaf
1337            .encryption_key()
1338            .tls_serialize_detached()
1339            .map_err(VirtualClientsError::from)?;
1340        let tbe = derivation_info.decrypt(
1341            provider.crypto(),
1342            emulation_ciphersuite,
1343            &epoch_encryption_key,
1344            &leaf_encryption_key,
1345            VirtualClientOperationType::KeyPackage,
1346        )?;
1347        let DerivationInfoTbe::KeyPackage {
1348            leaf_index,
1349            generation,
1350            key_package_index,
1351        } = tbe
1352        else {
1353            // Decrypting with the KeyPackage operation type always yields the
1354            // KeyPackage variant.
1355            return Err(LibraryError::custom("unexpected derivation info variant").into());
1356        };
1357
1358        // Rederive the creator's KeyPackage seed positionally from the shared
1359        // operation tree, then the creator leaf's encryption keypair from that
1360        // seed. The `key_package` operation context is empty. The advanced tree
1361        // is persisted only after the confirmation tag verifies, so a corrupted
1362        // GroupInfo naming the genuine creator leaf does not burn this sibling's
1363        // generation.
1364        let operation_secret = operation_tree.derive_operation_secret(
1365            provider.crypto(),
1366            emulation_ciphersuite,
1367            &epoch_id,
1368            leaf_index,
1369            VirtualClientOperationType::KeyPackage,
1370            generation,
1371            b"",
1372        )?;
1373        let key_package_seed = operation_secret.derive_key_package_seed_secret(
1374            provider.crypto(),
1375            ciphersuite,
1376            key_package_index,
1377        )?;
1378        let leaf_keypair = key_package_seed
1379            .derive_encryption_key_secret(provider.crypto(), ciphersuite)?
1380            .generate_encryption_key_pair(provider.crypto(), ciphersuite)?;
1381        if leaf_keypair.public_key() != creator_leaf.encryption_key() {
1382            return Err(Error::LeafKeyMismatch);
1383        }
1384
1385        // Derive epoch-0 secrets from the same KeyPackage seed and verify the
1386        // GroupInfo's confirmation tag against them. A mismatch means the
1387        // reconstruction did not reproduce the creator's epoch secrets.
1388        let serialized_group_context = public_group
1389            .group_context()
1390            .tls_serialize_detached()
1391            .map_err(LibraryError::missing_bound_check)?;
1392        let epoch_secret =
1393            key_package_seed.derive_group_creation_secret(provider.crypto(), ciphersuite)?;
1394        let epoch_secrets =
1395            EpochSecrets::from_epoch_secret(provider.crypto(), ciphersuite, epoch_secret)
1396                .map_err(LibraryError::unexpected_crypto_error)?;
1397        let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
1398            serialized_group_context,
1399            public_group.tree_size(),
1400            creator_index,
1401        );
1402        let expected_confirmation_tag = message_secrets
1403            .confirmation_key()
1404            .tag(
1405                provider.crypto(),
1406                ciphersuite,
1407                public_group.group_context().confirmed_transcript_hash(),
1408            )
1409            .map_err(LibraryError::unexpected_crypto_error)?;
1410        if &expected_confirmation_tag != group_info.confirmation_tag() {
1411            return Err(Error::ConfirmationTagMismatch);
1412        }
1413
1414        // The reconstruction reproduced the creator's secrets, so this is a
1415        // genuine sibling-created group: persist the advanced operation tree.
1416        provider
1417            .storage()
1418            .write_vc_operation_tree(&epoch_id, &operation_tree)
1419            .map_err(Error::StorageError)?;
1420
1421        let message_secrets_store = MessageSecretsStore::new_with_secret(
1422            join_config.past_epoch_deletion_policy(),
1423            message_secrets,
1424        );
1425        let mut resumption_psk_store =
1426            ResumptionPskStore::new(join_config.number_of_resumption_psks);
1427        resumption_psk_store.add(
1428            public_group.group_context().epoch(),
1429            group_epoch_secrets.resumption_psk().clone(),
1430        );
1431
1432        // Bind epoch 0 of the created group to the derivation epoch so later
1433        // VC operations in this group resolve the right derivation epoch state.
1434        // Written before the group itself, so an error between the writes
1435        // cannot leave a loadable group without a binding (a bound group is
1436        // required for the reuse-guard MUST).
1437        let max_entries = message_secrets_store.max_epochs.saturating_add(1);
1438        write_vc_emulation_binding_with_pruning(
1439            provider.storage(),
1440            public_group.group_id(),
1441            public_group.group_context().epoch(),
1442            epoch_id,
1443            max_entries,
1444        )
1445        .map_err(Error::StorageError)?;
1446
1447        let mls_group = MlsGroup {
1448            mls_group_config: join_config.clone(),
1449            own_leaf_nodes: vec![],
1450            aad: vec![],
1451            #[cfg(feature = "extensions-draft")]
1452            safe_aad: SafeAad::empty(),
1453            group_state: MlsGroupState::Operational,
1454            public_group,
1455            group_epoch_secrets,
1456            own_leaf_index: creator_index,
1457            message_secrets_store,
1458            resumption_psk_store,
1459            #[cfg(feature = "extensions-draft")]
1460            application_export_tree: None,
1461            #[cfg(feature = "virtual-clients-draft")]
1462            emulation_group: false,
1463        };
1464        mls_group
1465            .store(provider.storage())
1466            .map_err(Error::StorageError)?;
1467        mls_group
1468            .store_epoch_keypairs(provider.storage(), &[leaf_keypair])
1469            .map_err(Error::StorageError)?;
1470
1471        Ok(mls_group)
1472    }
1473}
1474
1475/// Builder for bootstrapping a virtual client's sibling emulator client into
1476/// a higher-level group by processing another sibling's external commit,
1477/// when this client is not yet a member of that group.
1478///
1479/// The first emulator client joined the higher-level group via an external
1480/// commit. A second emulator client (this one), sharing the same emulation
1481/// epoch, reconstructs the resulting group state from that commit.
1482///
1483/// The join happens in two steps. [`Self::process_commit`] rebuilds the
1484/// prior-epoch group and verifies the commit, returning a
1485/// [`StagedVcExternalCommitJoin`]. The application inspects the verified
1486/// proposals it exposes, resolves any AppDataUpdate proposals, and completes
1487/// the join with [`StagedVcExternalCommitJoin::into_group`].
1488#[cfg(feature = "virtual-clients-draft")]
1489#[derive(Debug, Default)]
1490pub struct VcExternalCommitJoinBuilder {
1491    join_config: MlsGroupJoinConfig,
1492    ratchet_tree: Option<RatchetTreeIn>,
1493    lifetime_policy: LeafNodeLifetimePolicy,
1494}
1495
1496#[cfg(feature = "virtual-clients-draft")]
1497impl VcExternalCommitJoinBuilder {
1498    /// Creates a new [`VcExternalCommitJoinBuilder`] with default values.
1499    pub fn new() -> Self {
1500        Self::default()
1501    }
1502
1503    /// Specifies the configuration to use for the joined group.
1504    pub fn with_config(mut self, join_config: MlsGroupJoinConfig) -> Self {
1505        self.join_config = join_config;
1506        self
1507    }
1508
1509    /// Specifies the prior-epoch ratchet tree. This is only used if the
1510    /// ratchet tree is not provided in the [`VerifiableGroupInfo`]
1511    /// extensions. A ratchet tree must be provided, either in the
1512    /// [`VerifiableGroupInfo`] extensions or via this method.
1513    pub fn with_ratchet_tree(mut self, ratchet_tree: RatchetTreeIn) -> Self {
1514        self.ratchet_tree = Some(ratchet_tree);
1515        self
1516    }
1517
1518    /// Skip the validation of lifetimes in leaf nodes in the ratchet tree.
1519    /// Note that only the leaf nodes are checked that were never updated.
1520    ///
1521    /// By default they are validated.
1522    pub fn skip_lifetime_validation(mut self) -> Self {
1523        self.lifetime_policy = LeafNodeLifetimePolicy::Skip;
1524        self
1525    }
1526
1527    /// Rebuilds the higher-level group at the epoch *before* the external
1528    /// commit from `verifiable_group_info` (and the ratchet tree, taken from
1529    /// the GroupInfo's `ratchet_tree` extension or
1530    /// [`Self::with_ratchet_tree`]), and verifies `external_commit` against
1531    /// it: the GroupInfo signature and tree, the commit's signature and
1532    /// external-commit shape, and that the commit's derivation info
1533    /// references the shared emulation epoch `epoch_id`.
1534    ///
1535    /// Nothing is consumed or persisted at this point. Dropping the returned
1536    /// [`StagedVcExternalCommitJoin`] discards the join without advancing
1537    /// the shared operation secret tree.
1538    pub fn process_commit<Provider: OpenMlsProvider>(
1539        self,
1540        provider: &Provider,
1541        verifiable_group_info: VerifiableGroupInfo,
1542        external_commit: impl Into<ProtocolMessage>,
1543        epoch_id: EpochId,
1544    ) -> Result<StagedVcExternalCommitJoin, VcExternalCommitJoinError<Provider::StorageError>> {
1545        type Error<S> = VcExternalCommitJoinError<S>;
1546
1547        let Self {
1548            join_config,
1549            ratchet_tree,
1550            lifetime_policy,
1551        } = self;
1552
1553        // Resolve the prior-epoch ratchet tree (from the GroupInfo extension
1554        // or the builder) and rebuild the prior-epoch public group.
1555        let ratchet_tree = match verifiable_group_info.extensions().ratchet_tree() {
1556            Some(extension) => extension.ratchet_tree().clone(),
1557            None => ratchet_tree.ok_or(Error::MissingRatchetTree)?,
1558        };
1559        let (public_group, _group_info) = PublicGroup::from_ratchet_tree(
1560            provider.crypto(),
1561            ratchet_tree,
1562            verifiable_group_info,
1563            ProposalStore::new(),
1564            lifetime_policy,
1565        )?;
1566
1567        // Assemble a transient group at the prior epoch. Its epoch secrets are
1568        // never used cryptographically here: the external commit carries the
1569        // external init secret and the operation secret tree supplies the path,
1570        // so a random init-secret stub is sufficient. The own leaf index is the
1571        // leftmost free index, where the committing sibling installs the shared
1572        // virtual-client leaf.
1573        let ciphersuite = public_group.ciphersuite();
1574        let serialized_group_context = public_group
1575            .group_context()
1576            .tls_serialize_detached()
1577            .map_err(LibraryError::missing_bound_check)?;
1578        let own_leaf_index = public_group.leftmost_free_index(std::iter::empty())?;
1579        let init_secret = InitSecret::random(ciphersuite, provider.rand())
1580            .map_err(LibraryError::unexpected_crypto_error)?;
1581        let epoch_secrets =
1582            EpochSecrets::with_init_secret(provider.crypto(), ciphersuite, init_secret)
1583                .map_err(LibraryError::unexpected_crypto_error)?;
1584        let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
1585            serialized_group_context,
1586            public_group.tree_size(),
1587            LeafNodeIndex::new(0u32),
1588        );
1589        // Do not retain the synthetic prior-epoch secrets used only to stage
1590        // this external commit.
1591        let message_secrets_store = MessageSecretsStore::new_with_secret(
1592            &PastEpochDeletionPolicy::MaxEpochs(0),
1593            message_secrets,
1594        );
1595        let resumption_psk_store = ResumptionPskStore::new(join_config.number_of_resumption_psks);
1596        let mut group = MlsGroup {
1597            mls_group_config: join_config,
1598            own_leaf_nodes: vec![],
1599            aad: vec![],
1600            #[cfg(feature = "extensions-draft")]
1601            safe_aad: SafeAad::empty(),
1602            group_state: MlsGroupState::Operational,
1603            public_group,
1604            group_epoch_secrets,
1605            own_leaf_index,
1606            message_secrets_store,
1607            resumption_psk_store,
1608            #[cfg(feature = "extensions-draft")]
1609            application_export_tree: None,
1610            // A virtual client joins a higher-level group here, never its own
1611            // emulation group.
1612            emulation_group: false,
1613        };
1614
1615        // Parse and verify the external commit against the prior-epoch group.
1616        // A PrivateMessage claiming our own leaf cannot be an external commit.
1617        let processing::UnprotectedMessage::Unverified(unverified) =
1618            group.unprotect_message(provider, external_commit)?
1619        else {
1620            return Err(Error::NotAnExternalCommit);
1621        };
1622        let verified = unverified
1623            .verify(group.ciphersuite(), provider.crypto(), group.version())
1624            .map_err(ProcessMessageError::from)?;
1625        if !matches!(verified.content.sender(), Sender::NewMemberCommit) {
1626            return Err(Error::NotAnExternalCommit);
1627        }
1628        let content = verified.content;
1629        let FramedContentBody::Commit(commit) = content.content() else {
1630            return Err(Error::NotAnExternalCommit);
1631        };
1632
1633        // Check the commit's derivation info without consuming an operation
1634        // secret generation: presence and the emulation epoch binding are
1635        // validated here, decryption and the consume-once secret derivation
1636        // happen in `StagedVcExternalCommitJoin::into_group`.
1637        let derivation_info = commit
1638            .path
1639            .as_ref()
1640            .map(|path| path.leaf_node().vc_derivation_info())
1641            .transpose()?
1642            .flatten()
1643            .ok_or(Error::MissingDerivationInfo)?;
1644        if derivation_info.epoch_id() != &epoch_id {
1645            return Err(Error::EpochIdMismatch);
1646        }
1647
1648        // The AppDataUpdate proposals covered by the commit, for the
1649        // application to resolve before completing the join. The transient
1650        // group's proposal store is empty, so only by-value proposals
1651        // resolve. An external commit cannot reference proposals a
1652        // non-member could hold.
1653        let app_data_update_proposals =
1654            committed_app_data_update_proposals(commit, group.proposal_store());
1655
1656        Ok(StagedVcExternalCommitJoin {
1657            group,
1658            content,
1659            app_data_update_proposals,
1660            app_data_updates: None,
1661        })
1662    }
1663}
1664
1665/// A verified sibling external commit, ready to be joined. Returned by
1666/// [`VcExternalCommitJoinBuilder::process_commit`].
1667///
1668/// The commit's signature has been verified against the prior-epoch group,
1669/// so the proposals exposed here are authenticated. If the commit covers
1670/// AppDataUpdate proposals, the application must interpret them (with the
1671/// help of [`Self::app_data_dictionary_updater`]) and supply the resulting
1672/// [`AppDataUpdates`] via [`Self::with_app_data_dictionary_updates`] before
1673/// calling [`Self::into_group`], exactly as it would for
1674/// [`MlsGroup::resolve_app_data_commit`] when processing the same commit as
1675/// a member.
1676///
1677/// Dropping this value discards the join. No operation secret generation is
1678/// consumed before [`Self::into_group`], so a discarded join can be started
1679/// over by processing the same commit again.
1680#[cfg(feature = "virtual-clients-draft")]
1681pub struct StagedVcExternalCommitJoin {
1682    /// Transient reconstruction of the group at the prior epoch.
1683    group: MlsGroup,
1684    /// The verified commit content.
1685    content: AuthenticatedContent,
1686    /// The by-value AppDataUpdate proposals covered by the commit, sorted by
1687    /// component id.
1688    app_data_update_proposals: Vec<AppDataUpdateProposal>,
1689    /// The application-resolved updates for the commit's AppDataUpdate
1690    /// proposals, if any.
1691    app_data_updates: Option<AppDataUpdates>,
1692}
1693
1694#[cfg(feature = "virtual-clients-draft")]
1695impl StagedVcExternalCommitJoin {
1696    /// Returns the [`GroupContext`] of the group at the epoch *before* the
1697    /// external commit. The commit's changes (including any AppDataUpdate
1698    /// proposals) are not applied to it. The joined group's context is
1699    /// available on the [`MlsGroup`] returned by [`Self::into_group`].
1700    pub fn prior_group_context(&self) -> &GroupContext {
1701        self.group.context()
1702    }
1703
1704    /// Returns an iterator over the [`Member`]s of the group at the epoch
1705    /// *before* the external commit. The commit's changes are not applied:
1706    /// the committing sibling's virtual-client leaf is absent and members
1707    /// the commit inline-removes are still present.
1708    pub fn prior_members(&self) -> impl Iterator<Item = Member> + '_ {
1709        self.group.members()
1710    }
1711
1712    /// Returns the AppDataUpdate proposals covered by the commit, sorted by
1713    /// component id. The application interprets them to compute the
1714    /// [`AppDataUpdates`] that [`Self::into_group`] requires.
1715    pub fn app_data_update_proposals(&self) -> impl Iterator<Item = &AppDataUpdateProposal> {
1716        self.app_data_update_proposals.iter()
1717    }
1718
1719    /// Returns the AppEphemeral proposals for `component_id` that the commit
1720    /// carries by value, in the order they appear in the commit, for example
1721    /// to decide how to follow the join. Unlike
1722    /// [`PublicMessageIn::unverified_app_ephemeral_proposals`], the returned
1723    /// data is authenticated: the commit's signature has been verified.
1724    ///
1725    /// [`PublicMessageIn::unverified_app_ephemeral_proposals`]:
1726    ///     crate::framing::PublicMessageIn::unverified_app_ephemeral_proposals
1727    pub fn app_ephemeral_proposals_for_component_id(
1728        &self,
1729        component_id: ComponentId,
1730    ) -> impl Iterator<Item = &AppEphemeralProposal> {
1731        let proposals = match self.content.content() {
1732            FramedContentBody::Commit(commit) => commit.proposals.as_slice(),
1733            // `process_commit` only constructs staged joins from commits.
1734            _ => &[],
1735        };
1736        proposals
1737            .iter()
1738            .filter_map(move |proposal_or_ref| match proposal_or_ref {
1739                ProposalOrRef::Proposal(proposal) => match proposal.as_ref() {
1740                    Proposal::AppEphemeral(app_ephemeral)
1741                        if app_ephemeral.component_id() == component_id =>
1742                    {
1743                        Some(app_ephemeral.as_ref())
1744                    }
1745                    _ => None,
1746                },
1747                ProposalOrRef::Reference(_) => None,
1748            })
1749    }
1750
1751    /// Returns a helper for computing the [`AppDataUpdates`], seeded with
1752    /// the app data dictionary of the prior-epoch group context.
1753    pub fn app_data_dictionary_updater(&self) -> AppDataDictionaryUpdater<'_> {
1754        AppDataDictionaryUpdater::new(self.group.context().app_data_dict())
1755    }
1756
1757    /// Sets the [`AppDataUpdates`] that contain the changes made by the
1758    /// commit's AppDataUpdate proposals. Updates must be set exactly when
1759    /// the commit covers AppDataUpdate proposals.
1760    pub fn with_app_data_dictionary_updates(&mut self, app_data_updates: Option<AppDataUpdates>) {
1761        self.app_data_updates = app_data_updates;
1762    }
1763
1764    /// Completes the join and returns the joined [`MlsGroup`]. If the commit
1765    /// covers AppDataUpdate proposals, the resolved updates must have been
1766    /// set via [`Self::with_app_data_dictionary_updates`]. Absent updates
1767    /// are rejected before the consume-once operation secret generation is
1768    /// spent, so the join can be started over. Updates that do not reproduce
1769    /// the committing sibling's dictionary fail with a confirmation tag
1770    /// mismatch after the generation is consumed, so they should be computed
1771    /// deterministically from the proposals rather than guessed.
1772    pub fn into_group<Provider: OpenMlsProvider>(
1773        self,
1774        provider: &Provider,
1775    ) -> Result<MlsGroup, VcExternalCommitJoinError<Provider::StorageError>> {
1776        type Error<S> = VcExternalCommitJoinError<S>;
1777
1778        let Self {
1779            mut group,
1780            content,
1781            app_data_update_proposals,
1782            app_data_updates,
1783        } = self;
1784
1785        // An application that has not resolved the commit's AppDataUpdate
1786        // proposals yet is rejected before the operation secret generation
1787        // is consumed, so it can compute the updates and process the commit
1788        // again. Staging repeats this check. Superfluous updates are only
1789        // rejected there.
1790        if !app_data_update_proposals.is_empty() && app_data_updates.is_none() {
1791            return Err(StageCommitError::ApplyAppDataUpdateError(
1792                ApplyAppDataUpdateError::MissingAppDataUpdates,
1793            )
1794            .into());
1795        }
1796
1797        let FramedContentBody::Commit(commit) = content.content() else {
1798            // `process_commit` only constructs staged joins from commits.
1799            return Err(LibraryError::custom("staged join without commit content").into());
1800        };
1801
1802        // Recover the sibling-VC commit material (operation secret + emulation
1803        // epoch id + carried external init secret) from the leaf's derivation
1804        // info, then stage and merge the commit through the sibling-VC path.
1805        let material = group
1806            .load_vc_commit_material(provider, commit)?
1807            .ok_or(Error::MissingDerivationInfo)?;
1808        let staged = group.stage_commit_with_app_data_updates(
1809            &content,
1810            vec![],
1811            vec![],
1812            app_data_updates,
1813            provider,
1814            Some(material),
1815        )?;
1816        group.merge_staged_commit(provider, staged)?;
1817        let deletion_policy = group.mls_group_config.past_epoch_deletion_policy().clone();
1818        group.resize_message_secrets_store(&deletion_policy);
1819        group
1820            .store(provider.storage())
1821            .map_err(Error::StorageError)?;
1822        Ok(group)
1823    }
1824}
1825
1826/// Verify or skip the validation of leaf node lifetimes in the ratchet tree
1827/// when joining a group.
1828#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1829pub enum LeafNodeLifetimePolicy {
1830    /// Verify the lifetime of leaf nodes in the ratchet tree.
1831    ///
1832    /// **NOTE:** Only leaf nodes that have never been updated have a lifetime.
1833    #[default]
1834    Verify,
1835
1836    /// Skip the verification of the lifeimte in leaf nodes in the ratchet tree.
1837    Skip,
1838}
1839
1840/// Builder for joining a group.
1841///
1842/// Create this with [`StagedWelcome::build_from_welcome`].
1843pub struct JoinBuilder<'a, Provider: OpenMlsProvider> {
1844    provider: &'a Provider,
1845    processed_welcome: ProcessedWelcome,
1846    ratchet_tree: Option<RatchetTreeIn>,
1847    validate_lifetimes: LeafNodeLifetimePolicy,
1848    replace_old_group: bool,
1849    /// Set when joining a subgroup branch (see [`StagedWelcome::build_from_branch`]).
1850    /// Triggers the receiver checks in [`Self::build`].
1851    branch: Option<BranchInfo>,
1852    /// Whether to check that every subgroup member is also a parent-group
1853    /// member. Only relevant when [`Self::branch`] is set. Defaults to `true`.
1854    check_members: bool,
1855}
1856
1857impl<'a, Provider: OpenMlsProvider> JoinBuilder<'a, Provider> {
1858    /// Create a new builder for the [`JoinBuilder`].
1859    pub fn new(provider: &'a Provider, processed_welcome: ProcessedWelcome) -> Self {
1860        Self {
1861            provider,
1862            processed_welcome,
1863            ratchet_tree: None,
1864            replace_old_group: false,
1865            validate_lifetimes: LeafNodeLifetimePolicy::Verify,
1866            branch: None,
1867            check_members: true,
1868        }
1869    }
1870
1871    /// Seed this builder with the parent group's [`BranchInfo`], turning it into
1872    /// a subgroup-branch join (see [`StagedWelcome::build_from_branch`]).
1873    fn with_branch_info(mut self, branch_info: BranchInfo) -> Self {
1874        self.branch = Some(branch_info);
1875        self
1876    }
1877
1878    /// When joining a subgroup branch, controls whether [`Self::build`] checks
1879    /// that every subgroup member is also a member of the parent group receiver
1880    /// check (c)). Defaults to `true`.
1881    ///
1882    /// This has no effect on a regular (non-branch) join.
1883    pub fn check_members(mut self, check_members: bool) -> Self {
1884        self.check_members = check_members;
1885        self
1886    }
1887
1888    /// The ratchet tree to use for the new group.
1889    pub fn with_ratchet_tree(mut self, ratchet_tree: RatchetTreeIn) -> Self {
1890        self.ratchet_tree = Some(ratchet_tree);
1891        self
1892    }
1893
1894    /// Instruct the builder to replace any existing group with the same ID.
1895    pub fn replace_old_group(mut self) -> Self {
1896        self.replace_old_group = true;
1897        self
1898    }
1899
1900    /// Skip the validation of lifetimes in leaf nodes in the ratchet tree.
1901    /// Note that only the leaf nodes are checked that were never updated.
1902    ///
1903    /// By default they are validated.
1904    pub fn skip_lifetime_validation(mut self) -> Self {
1905        self.validate_lifetimes = LeafNodeLifetimePolicy::Skip;
1906        self
1907    }
1908
1909    /// Get a reference to the [`ProcessedWelcome`].
1910    ///
1911    /// Use this to inspect the [`Welcome`] message before validation.
1912    pub fn processed_welcome(&self) -> &ProcessedWelcome {
1913        &self.processed_welcome
1914    }
1915
1916    /// Build the [`StagedWelcome`].
1917    pub fn build(self) -> Result<StagedWelcome, WelcomeError<Provider::StorageError>> {
1918        // Receiver checks (a) and (b): when joining a subgroup branch, the
1919        // version and ciphersuite must match the parent group, and the subgroup
1920        // must be at epoch 1.
1921        if let Some(branch_info) = &self.branch {
1922            let group_info = self.processed_welcome.unverified_group_info();
1923            if group_info.group_context().protocol_version() != branch_info.version()
1924                || group_info.ciphersuite() != branch_info.ciphersuite()
1925            {
1926                return Err(WelcomeError::SubgroupParameterMismatch);
1927            }
1928            if group_info.group_context().epoch().as_u64() != 1 {
1929                return Err(WelcomeError::SubgroupEpochInvalid);
1930            }
1931        }
1932
1933        let staged_welcome = self.processed_welcome.into_staged_welcome_inner(
1934            self.provider,
1935            self.ratchet_tree,
1936            self.validate_lifetimes,
1937            self.replace_old_group,
1938        )?;
1939
1940        // Receiver check (c): every LeafNode in the subgroup must
1941        // match a LeafNode in the parent group.
1942        if let Some(branch_info) = &self.branch {
1943            if self.check_members {
1944                for member in staged_welcome.members() {
1945                    if !branch_info
1946                        .member_credentials()
1947                        .contains(&member.credential)
1948                    {
1949                        return Err(WelcomeError::SubgroupLeafMismatch);
1950                    }
1951                }
1952            }
1953        }
1954
1955        Ok(staged_welcome)
1956    }
1957}