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