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