Skip to main content

openmls/group/mls_group/
creation.rs

1use errors::NewGroupError;
2use openmls_traits::{crypto::OpenMlsCrypto, storage::StorageProvider as StorageProviderTrait};
3
4use super::{builder::MlsGroupBuilder, *};
5use crate::{
6    credentials::CredentialWithKey,
7    extensions::Extensions,
8    group::{
9        commit_builder::external_commits::ExternalCommitBuilder,
10        errors::{ExportSecretError, ExternalCommitError, WelcomeError},
11    },
12    messages::{
13        group_info::{GroupInfo, VerifiableGroupInfo},
14        Welcome,
15    },
16    schedule::{
17        psk::{store::ResumptionPskStore, PreSharedKeyId},
18        EpochSecretsResult,
19    },
20    storage::OpenMlsProvider,
21    treesync::{
22        errors::{DerivePathError, PublicTreeError},
23        node::leaf_node::{Capabilities, LeafNodeParameters},
24        RatchetTreeIn,
25    },
26};
27
28#[cfg(doc)]
29use crate::key_packages::KeyPackage;
30
31impl MlsGroup {
32    // === Group creation ===
33
34    /// Creates a builder which can be used to configure and build
35    /// a new [`MlsGroup`].
36    pub fn builder() -> MlsGroupBuilder {
37        MlsGroupBuilder::new()
38    }
39
40    /// Creates a new group with the creator as the only member (and a random
41    /// group ID).
42    ///
43    /// This function removes the private key corresponding to the
44    /// `key_package` from the key store.
45    pub fn new<Provider: OpenMlsProvider>(
46        provider: &Provider,
47        signer: &impl Signer,
48        mls_group_create_config: &MlsGroupCreateConfig,
49        credential_with_key: CredentialWithKey,
50    ) -> Result<Self, NewGroupError<Provider::StorageError>> {
51        MlsGroupBuilder::new().build_internal(
52            provider,
53            signer,
54            credential_with_key,
55            Some(mls_group_create_config.clone()),
56        )
57    }
58
59    /// Creates a new group with a given group ID with the creator as the only
60    /// member.
61    pub fn new_with_group_id<Provider: OpenMlsProvider>(
62        provider: &Provider,
63        signer: &impl Signer,
64        mls_group_create_config: &MlsGroupCreateConfig,
65        group_id: GroupId,
66        credential_with_key: CredentialWithKey,
67    ) -> Result<Self, NewGroupError<Provider::StorageError>> {
68        MlsGroupBuilder::new()
69            .with_group_id(group_id)
70            .build_internal(
71                provider,
72                signer,
73                credential_with_key,
74                Some(mls_group_create_config.clone()),
75            )
76    }
77
78    /// Join an existing group through an External Commit.
79    /// The resulting [`MlsGroup`] instance starts off with a pending
80    /// commit (the external commit, which adds this client to the group).
81    /// Merging this commit is necessary for this [`MlsGroup`] instance to
82    /// function properly, as, for example, this client is not yet part of the
83    /// tree. As a result, it is not possible to clear the pending commit. If
84    /// the external commit was rejected due to an epoch change, the
85    /// [`MlsGroup`] instance has to be discarded and a new one has to be
86    /// created using this function based on the latest `ratchet_tree` and
87    /// group info. For more information on the external init process,
88    /// please see Section 11.2.1 in the MLS specification.
89    ///
90    /// Note: If there is a group member in the group with the same identity as
91    /// us, this will create a remove proposal.
92    #[allow(clippy::too_many_arguments)]
93    #[deprecated(
94        since = "0.7.1",
95        note = "Use the `MlsGroup::external_commit_builder` instead."
96    )]
97    pub fn join_by_external_commit<Provider: OpenMlsProvider>(
98        provider: &Provider,
99        signer: &impl Signer,
100        ratchet_tree: Option<RatchetTreeIn>,
101        verifiable_group_info: VerifiableGroupInfo,
102        mls_group_config: &MlsGroupJoinConfig,
103        capabilities: Option<Capabilities>,
104        extensions: Option<Extensions<LeafNode>>,
105        aad: &[u8],
106        credential_with_key: CredentialWithKey,
107    ) -> Result<(Self, MlsMessageOut, Option<GroupInfo>), ExternalCommitError<Provider::StorageError>>
108    {
109        let leaf_node_parameters = LeafNodeParameters::builder()
110            .with_capabilities(capabilities.unwrap_or_default())
111            .with_extensions(extensions.unwrap_or_default())
112            .build();
113
114        let mut external_commit_builder = ExternalCommitBuilder::new()
115            .with_aad(aad.to_vec())
116            .with_config(mls_group_config.clone());
117
118        if let Some(ratchet_tree) = ratchet_tree {
119            external_commit_builder = external_commit_builder.with_ratchet_tree(ratchet_tree)
120        }
121
122        let (mls_group, commit_message_bundle) = external_commit_builder
123            .build_group(provider, verifiable_group_info, credential_with_key)?
124            .leaf_node_parameters(leaf_node_parameters)
125            .load_psks(provider.storage())
126            .map_err(|e| {
127                log::error!("Error loading PSKs for external commit: {e:?}");
128                LibraryError::custom("Error loading PSKs for external commit")
129            })?
130            .build(provider.rand(), provider.crypto(), signer, |_| true)?
131            .finalize(provider)?;
132
133        let (commit, _, group_info) = commit_message_bundle.into_contents();
134
135        Ok((mls_group, commit, group_info))
136    }
137}
138
139impl ProcessedWelcome {
140    /// Creates a new processed [`Welcome`] message , which can be
141    /// inspected before creating a [`StagedWelcome`].
142    ///
143    /// This does not require a ratchet tree yet.
144    ///
145    /// [`Welcome`]: crate::messages::Welcome
146    pub fn new_from_welcome<Provider: OpenMlsProvider>(
147        provider: &Provider,
148        mls_group_config: &MlsGroupJoinConfig,
149        welcome: Welcome,
150    ) -> Result<Self, WelcomeError<Provider::StorageError>> {
151        let (resumption_psk_store, key_package_bundle) =
152            keys_for_welcome(mls_group_config, &welcome, provider)?;
153
154        let ciphersuite = welcome.ciphersuite();
155        let Some(egs) = welcome.find_encrypted_group_secret(
156            key_package_bundle
157                .key_package()
158                .hash_ref(provider.crypto())?,
159        ) else {
160            return Err(WelcomeError::JoinerSecretNotFound);
161        };
162
163        // This check seems to be superfluous from the perspective of the RFC, but still doesn't
164        // seem like a bad idea.
165        if welcome.ciphersuite() != key_package_bundle.key_package().ciphersuite() {
166            let e = WelcomeError::CiphersuiteMismatch;
167            log::debug!("new_from_welcome {e:?}");
168            return Err(e);
169        }
170
171        let group_secrets = GroupSecrets::try_from_ciphertext(
172            key_package_bundle.init_private_key(),
173            egs.encrypted_group_secrets(),
174            welcome.encrypted_group_info(),
175            ciphersuite,
176            provider.crypto(),
177        )?;
178
179        // Validate PSKs
180        PreSharedKeyId::validate_in_welcome(&group_secrets.psks, ciphersuite)?;
181
182        let psk_secret = {
183            let psks = load_psks(
184                provider.storage(),
185                &resumption_psk_store,
186                &group_secrets.psks,
187            )?;
188
189            PskSecret::new(provider.crypto(), ciphersuite, psks)?
190        };
191        let key_schedule = KeySchedule::init(
192            ciphersuite,
193            provider.crypto(),
194            &group_secrets.joiner_secret,
195            psk_secret,
196        )?;
197        let (welcome_key, welcome_nonce) = key_schedule
198            .welcome(provider.crypto(), ciphersuite)
199            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?
200            .derive_welcome_key_nonce(provider.crypto(), ciphersuite)
201            .map_err(LibraryError::unexpected_crypto_error)?;
202        let verifiable_group_info = VerifiableGroupInfo::try_from_ciphertext(
203            &welcome_key,
204            &welcome_nonce,
205            welcome.encrypted_group_info(),
206            &[],
207            provider.crypto(),
208        )?;
209
210        if let Some(required_capabilities) =
211            verifiable_group_info.extensions().required_capabilities()
212        {
213            // Also check that our key package actually supports the extensions.
214            // As per the spec, the sender must have checked this. But you never know.
215            key_package_bundle
216                .key_package()
217                .leaf_node()
218                .capabilities()
219                .supports_required_capabilities(required_capabilities)?;
220        }
221
222        // https://validation.openmls.tech/#valn1404
223        // Verify that the cipher_suite in the GroupInfo matches the cipher_suite in the
224        // KeyPackage.
225        if verifiable_group_info.ciphersuite() != key_package_bundle.key_package().ciphersuite() {
226            let e = WelcomeError::CiphersuiteMismatch;
227            log::debug!("new_from_welcome {e:?}");
228            return Err(e);
229        }
230
231        Ok(Self {
232            mls_group_config: mls_group_config.clone(),
233            ciphersuite,
234            group_secrets,
235            key_schedule,
236            verifiable_group_info,
237            resumption_psk_store,
238            key_package_bundle,
239        })
240    }
241
242    /// Get a reference to the GroupInfo in this Welcome message.
243    ///
244    /// **NOTE:** The group info contains **unverified** values. Use with caution.
245    pub fn unverified_group_info(&self) -> &VerifiableGroupInfo {
246        &self.verifiable_group_info
247    }
248
249    /// Get a reference to the PSKs in this Welcome message.
250    ///
251    /// **NOTE:** The group info contains **unverified** values. Use with caution.
252    pub fn psks(&self) -> &[PreSharedKeyId] {
253        &self.group_secrets.psks
254    }
255
256    /// Consume the `ProcessedWelcome` and combine it with the ratchet tree into
257    /// a `StagedWelcome`.
258    pub fn into_staged_welcome<Provider: OpenMlsProvider>(
259        self,
260        provider: &Provider,
261        ratchet_tree: Option<RatchetTreeIn>,
262    ) -> Result<StagedWelcome, WelcomeError<Provider::StorageError>> {
263        self.into_staged_welcome_inner(
264            provider,
265            ratchet_tree,
266            LeafNodeLifetimePolicy::Verify,
267            false,
268        )
269    }
270
271    /// Consume the `ProcessedWelcome` and combine it with the ratchet tree into
272    /// a `StagedWelcome`.
273    pub(crate) fn into_staged_welcome_inner<Provider: OpenMlsProvider>(
274        mut self,
275        provider: &Provider,
276        ratchet_tree: Option<RatchetTreeIn>,
277        validate_lifetimes: LeafNodeLifetimePolicy,
278        replace_old_group: bool,
279    ) -> Result<StagedWelcome, WelcomeError<Provider::StorageError>> {
280        // Check if we need to replace an old group
281        if !replace_old_group
282            && MlsGroup::load(provider.storage(), self.verifiable_group_info.group_id())
283                .map_err(WelcomeError::StorageError)?
284                .is_some()
285        {
286            return Err(WelcomeError::GroupAlreadyExists);
287        }
288
289        // Build the ratchet tree and group
290
291        // Set nodes either from the extension or from the `nodes_option`.
292        // If we got a ratchet tree extension in the welcome, we enable it for
293        // this group. Note that this is not strictly necessary. But there's
294        // currently no other mechanism to enable the extension.
295        let ratchet_tree = match self.verifiable_group_info.extensions().ratchet_tree() {
296            Some(extension) => extension.ratchet_tree().clone(),
297            None => match ratchet_tree {
298                Some(ratchet_tree) => ratchet_tree,
299                None => return Err(WelcomeError::MissingRatchetTree),
300            },
301        };
302
303        // Since there is currently only the external pub extension, there is no
304        // group info extension of interest here.
305        let (public_group, _group_info_extensions) = PublicGroup::from_ratchet_tree(
306            provider.crypto(),
307            ratchet_tree,
308            self.verifiable_group_info.clone(),
309            ProposalStore::new(),
310            validate_lifetimes,
311        )?;
312
313        // Check that the leaf node of the added key package supports all extensions in the group
314        // context.
315        // https://validation.openmls.tech/#valn1415
316        let added_leaf_supports_all_group_context_extensions = public_group
317            .group_context()
318            .extensions()
319            .iter()
320            .all(|extension| {
321                self.key_package_bundle
322                    .key_package
323                    .leaf_node()
324                    .supports_extension(&extension.extension_type())
325            });
326        if !added_leaf_supports_all_group_context_extensions {
327            return Err(WelcomeError::UnsupportedExtensions);
328        }
329
330        // Find our own leaf in the tree.
331        let own_leaf_index = public_group
332            .members()
333            .find_map(|m| {
334                if m.signature_key
335                    == self
336                        .key_package_bundle
337                        .key_package()
338                        .leaf_node()
339                        .signature_key()
340                        .as_slice()
341                {
342                    Some(m.index)
343                } else {
344                    None
345                }
346            })
347            .ok_or(WelcomeError::PublicTreeError(
348                PublicTreeError::MalformedTree,
349            ))?;
350
351        struct KeyScheduleResult {
352            group_epoch_secrets: GroupEpochSecrets,
353            message_secrets: MessageSecrets,
354            #[cfg(feature = "extensions-draft-08")]
355            application_exporter: ApplicationExportSecret,
356        }
357        let KeyScheduleResult {
358            group_epoch_secrets,
359            message_secrets,
360            #[cfg(feature = "extensions-draft-08")]
361                application_exporter: application_export_secret,
362        } = {
363            let serialized_group_context = public_group
364                .group_context()
365                .tls_serialize_detached()
366                .map_err(LibraryError::missing_bound_check)?;
367
368            // TODO #751: Implement PSK
369            self.key_schedule
370                .add_context(provider.crypto(), &serialized_group_context)
371                .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
372
373            let EpochSecretsResult {
374                epoch_secrets,
375                #[cfg(feature = "extensions-draft-08")]
376                application_exporter,
377            } = self
378                .key_schedule
379                .epoch_secrets(provider.crypto(), self.ciphersuite)
380                .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
381
382            let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
383                serialized_group_context,
384                public_group.tree_size(),
385                own_leaf_index,
386            );
387
388            KeyScheduleResult {
389                group_epoch_secrets,
390                message_secrets,
391                #[cfg(feature = "extensions-draft-08")]
392                application_exporter,
393            }
394        };
395
396        let confirmation_tag = message_secrets
397            .confirmation_key()
398            .tag(
399                provider.crypto(),
400                self.ciphersuite,
401                public_group.group_context().confirmed_transcript_hash(),
402            )
403            .map_err(LibraryError::unexpected_crypto_error)?;
404
405        // Verify confirmation tag
406        // https://validation.openmls.tech/#valn1411
407        if &confirmation_tag != public_group.confirmation_tag() {
408            log::error!("Confirmation tag mismatch");
409            log_crypto!(trace, "  Got:      {:x?}", confirmation_tag);
410            log_crypto!(trace, "  Expected: {:x?}", public_group.confirmation_tag());
411
412            // in some tests we need to be able to proceed despite the tag being wrong,
413            // e.g. to test whether a later validation check is performed correctly.
414            if !crate::skip_validation::is_disabled::confirmation_tag() {
415                return Err(WelcomeError::ConfirmationTagMismatch);
416            }
417        }
418
419        let message_secrets_store = MessageSecretsStore::new_with_secret(
420            &PastEpochDeletionPolicy::MaxEpochs(0),
421            message_secrets,
422        );
423
424        // Extract and store the resumption PSK for the current epoch.
425        let resumption_psk = group_epoch_secrets.resumption_psk();
426        self.resumption_psk_store
427            .add(public_group.group_context().epoch(), resumption_psk.clone());
428
429        let welcome_sender_index = self.verifiable_group_info.signer();
430        let path_keypairs = if let Some(path_secret) = self.group_secrets.path_secret {
431            let (path_keypairs, _commit_secret) = public_group
432                .derive_path_secrets(
433                    provider.crypto(),
434                    self.ciphersuite,
435                    path_secret,
436                    welcome_sender_index,
437                    own_leaf_index,
438                )
439                .map_err(|e| match e {
440                    DerivePathError::LibraryError(e) => e.into(),
441                    DerivePathError::PublicKeyMismatch => {
442                        WelcomeError::PublicTreeError(PublicTreeError::PublicKeyMismatch)
443                    }
444                })?;
445            Some(path_keypairs)
446        } else {
447            None
448        };
449
450        let staged_welcome = StagedWelcome {
451            mls_group_config: self.mls_group_config,
452            public_group,
453            group_epoch_secrets,
454            own_leaf_index,
455            message_secrets_store,
456            #[cfg(feature = "extensions-draft-08")]
457            application_export_secret,
458            resumption_psk_store: self.resumption_psk_store,
459            verifiable_group_info: self.verifiable_group_info,
460            key_package_bundle: self.key_package_bundle,
461            path_keypairs,
462        };
463
464        Ok(staged_welcome)
465    }
466}
467
468impl StagedWelcome {
469    /// Creates a new staged welcome from a [`Welcome`] message. Returns an error
470    /// ([`WelcomeError::NoMatchingKeyPackage`]) if no [`KeyPackage`]
471    /// can be found.
472    /// Note: calling this function will consume the key material for decrypting the [`Welcome`]
473    /// message, even if the caller does not turn the [`StagedWelcome`] into an [`MlsGroup`].
474    ///
475    /// [`Welcome`]: crate::messages::Welcome
476    pub fn new_from_welcome<Provider: OpenMlsProvider>(
477        provider: &Provider,
478        mls_group_config: &MlsGroupJoinConfig,
479        welcome: Welcome,
480        ratchet_tree: Option<RatchetTreeIn>,
481    ) -> Result<Self, WelcomeError<Provider::StorageError>> {
482        let processed_welcome =
483            ProcessedWelcome::new_from_welcome(provider, mls_group_config, welcome)?;
484
485        processed_welcome.into_staged_welcome(provider, ratchet_tree)
486    }
487
488    /// Similar to [`StagedWelcome::new_from_welcome`] but as a builder.
489    ///
490    /// The builder allows to set the ratchet tree, skip leaf node lifetime
491    /// validation, and get the [`ProcessedWelcome`] for inspection before staging.
492    pub fn build_from_welcome<'a, Provider: OpenMlsProvider>(
493        provider: &'a Provider,
494        mls_group_config: &MlsGroupJoinConfig,
495        welcome: Welcome,
496        // ratchet_tree: Option<RatchetTreeIn>,
497    ) -> Result<JoinBuilder<'a, Provider>, WelcomeError<Provider::StorageError>> {
498        let processed_welcome =
499            ProcessedWelcome::new_from_welcome(provider, mls_group_config, welcome)?;
500
501        // processed_welcome.into_staged_welcome(provider, ratchet_tree)
502        Ok(JoinBuilder::new(provider, processed_welcome))
503    }
504
505    /// Returns the [`LeafNodeIndex`] of the group member that authored the [`Welcome`] message.
506    ///
507    /// [`Welcome`]: crate::messages::Welcome
508    pub fn welcome_sender_index(&self) -> LeafNodeIndex {
509        self.verifiable_group_info.signer()
510    }
511
512    /// Returns the [`LeafNode`] of the group member that authored the [`Welcome`] message.
513    ///
514    /// [`Welcome`]: crate::messages::Welcome
515    pub fn welcome_sender(&self) -> Result<&LeafNode, LibraryError> {
516        let sender_index = self.welcome_sender_index();
517        self.public_group
518            .leaf(sender_index)
519            .ok_or(LibraryError::custom(
520                "no leaf with given welcome sender index exists",
521            ))
522    }
523
524    /// Get the [`GroupContext`] of this welcome's [`PublicGroup`].
525    pub fn group_context(&self) -> &GroupContext {
526        self.public_group.group_context()
527    }
528
529    /// Get an iterator over all [`Member`]s of this welcome's [`PublicGroup`].
530    pub fn members(&self) -> impl Iterator<Item = Member> + '_ {
531        self.public_group.members()
532    }
533
534    /// Get the [`ApplicationExportSecret`] of this welcome.
535    #[cfg(feature = "extensions-draft-08")]
536    pub fn application_export_secret(&self) -> &ApplicationExportSecret {
537        &self.application_export_secret
538    }
539
540    /// Consumes the [`StagedWelcome`] and returns the respective [`MlsGroup`].
541    pub fn into_group<Provider: OpenMlsProvider>(
542        self,
543        provider: &Provider,
544    ) -> Result<MlsGroup, WelcomeError<Provider::StorageError>> {
545        // If we got a path secret, derive the path (which also checks if the
546        // public keys match) and store the derived keys in the key store.
547        let group_keypairs = if let Some(path_keypairs) = self.path_keypairs {
548            let mut keypairs = vec![self.key_package_bundle.encryption_key_pair()];
549            keypairs.extend_from_slice(&path_keypairs);
550            keypairs
551        } else {
552            vec![self.key_package_bundle.encryption_key_pair()]
553        };
554
555        #[cfg(feature = "extensions-draft-08")]
556        let application_export_tree = ApplicationExportTree::new(self.application_export_secret);
557
558        let past_epoch_deletion_policy = self.mls_group_config.past_epoch_deletion_policy().clone();
559
560        let mut mls_group = MlsGroup {
561            mls_group_config: self.mls_group_config,
562            own_leaf_nodes: vec![],
563            aad: vec![],
564            group_state: MlsGroupState::Operational,
565            public_group: self.public_group,
566            group_epoch_secrets: self.group_epoch_secrets,
567            own_leaf_index: self.own_leaf_index,
568            message_secrets_store: self.message_secrets_store,
569            resumption_psk_store: self.resumption_psk_store,
570            #[cfg(feature = "extensions-draft-08")]
571            application_export_tree: Some(application_export_tree),
572        };
573
574        mls_group
575            .store_epoch_keypairs(provider.storage(), group_keypairs.as_slice())
576            .map_err(WelcomeError::StorageError)?;
577        // resize the store
578        mls_group.resize_message_secrets_store(&past_epoch_deletion_policy);
579
580        mls_group
581            .store(provider.storage())
582            .map_err(WelcomeError::StorageError)?;
583
584        Ok(mls_group)
585    }
586
587    /// Exports a secret from the epoch of the group that is joined
588    /// using this [`StagedWelcome`].
589    /// Returns [`ExportSecretError::KeyLengthTooLong`] if the requested
590    /// key length is too long.
591    pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
592        &self,
593        crypto: &CryptoProvider,
594        label: &str,
595        context: &[u8],
596        key_length: usize,
597    ) -> Result<Vec<u8>, ExportSecretError> {
598        if key_length > u16::MAX as usize {
599            log::error!("Got a key that is larger than u16::MAX");
600            return Err(ExportSecretError::KeyLengthTooLong);
601        }
602
603        Ok(self
604            .group_epoch_secrets
605            .exporter_secret()
606            .derive_exported_secret(
607                self.group_context().ciphersuite(),
608                crypto,
609                label,
610                context,
611                key_length,
612            )
613            .map_err(LibraryError::unexpected_crypto_error)?)
614    }
615}
616
617fn keys_for_welcome<Provider: OpenMlsProvider>(
618    mls_group_config: &MlsGroupJoinConfig,
619    welcome: &Welcome,
620    provider: &Provider,
621) -> Result<
622    (ResumptionPskStore, KeyPackageBundle),
623    WelcomeError<<Provider as OpenMlsProvider>::StorageError>,
624> {
625    let resumption_psk_store = ResumptionPskStore::new(mls_group_config.number_of_resumption_psks);
626    let key_package_bundle: KeyPackageBundle = welcome
627        .secrets()
628        .iter()
629        .find_map(|egs| {
630            let hash_ref = egs.new_member();
631
632            provider
633                .storage()
634                .key_package(&hash_ref)
635                .map_err(WelcomeError::StorageError)
636                .transpose()
637        })
638        .ok_or(WelcomeError::NoMatchingKeyPackage)??;
639    if !key_package_bundle.key_package().last_resort() {
640        provider
641            .storage()
642            .delete_key_package(&key_package_bundle.key_package.hash_ref(provider.crypto())?)
643            .map_err(WelcomeError::StorageError)?;
644    } else {
645        log::debug!("Key package has last resort extension, not deleting");
646    }
647    Ok((resumption_psk_store, key_package_bundle))
648}
649
650/// Verify or skip the validation of leaf node lifetimes in the ratchet tree
651/// when joining a group.
652#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
653pub enum LeafNodeLifetimePolicy {
654    /// Verify the lifetime of leaf nodes in the ratchet tree.
655    ///
656    /// **NOTE:** Only leaf nodes that have never been updated have a lifetime.
657    #[default]
658    Verify,
659
660    /// Skip the verification of the lifeimte in leaf nodes in the ratchet tree.
661    Skip,
662}
663
664/// Builder for joining a group.
665///
666/// Create this with [`StagedWelcome::build_from_welcome`].
667pub struct JoinBuilder<'a, Provider: OpenMlsProvider> {
668    provider: &'a Provider,
669    processed_welcome: ProcessedWelcome,
670    ratchet_tree: Option<RatchetTreeIn>,
671    validate_lifetimes: LeafNodeLifetimePolicy,
672    replace_old_group: bool,
673}
674
675impl<'a, Provider: OpenMlsProvider> JoinBuilder<'a, Provider> {
676    /// Create a new builder for the [`JoinBuilder`].
677    pub fn new(provider: &'a Provider, processed_welcome: ProcessedWelcome) -> Self {
678        Self {
679            provider,
680            processed_welcome,
681            ratchet_tree: None,
682            replace_old_group: false,
683            validate_lifetimes: LeafNodeLifetimePolicy::Verify,
684        }
685    }
686
687    /// The ratchet tree to use for the new group.
688    pub fn with_ratchet_tree(mut self, ratchet_tree: RatchetTreeIn) -> Self {
689        self.ratchet_tree = Some(ratchet_tree);
690        self
691    }
692
693    /// Instruct the builder to replace any existing group with the same ID.
694    pub fn replace_old_group(mut self) -> Self {
695        self.replace_old_group = true;
696        self
697    }
698
699    /// Skip the validation of lifetimes in leaf nodes in the ratchet tree.
700    /// Note that only the leaf nodes are checked that were never updated.
701    ///
702    /// By default they are validated.
703    pub fn skip_lifetime_validation(mut self) -> Self {
704        self.validate_lifetimes = LeafNodeLifetimePolicy::Skip;
705        self
706    }
707
708    /// Get a reference to the [`ProcessedWelcome`].
709    ///
710    /// Use this to inspect the [`Welcome`] message before validation.
711    pub fn processed_welcome(&self) -> &ProcessedWelcome {
712        &self.processed_welcome
713    }
714
715    /// Build the [`StagedWelcome`].
716    pub fn build(self) -> Result<StagedWelcome, WelcomeError<Provider::StorageError>> {
717        self.processed_welcome.into_staged_welcome_inner(
718            self.provider,
719            self.ratchet_tree,
720            self.validate_lifetimes,
721            self.replace_old_group,
722        )
723    }
724}