openmls/group/mls_group/
commit_builder.rs

1//! This module contains the commit builder types, which can be used to build regular (i.e.
2//! non-external) commits. See the documentation of [`CommitBuilder`] for more information.
3
4use std::{borrow::BorrowMut, marker::PhantomData};
5
6use openmls_traits::{
7    crypto::OpenMlsCrypto, random::OpenMlsRand, signatures::Signer, storage::StorageProvider as _,
8};
9use tls_codec::Serialize as _;
10
11use crate::{
12    binary_tree::LeafNodeIndex,
13    ciphersuite::{signable::Signable as _, Secret},
14    framing::{FramingParameters, WireFormat},
15    group::{
16        diff::compute_path::{CommitType, PathComputationResult},
17        CommitBuilderStageError, CreateCommitError, Extension, Extensions, ExternalPubExtension,
18        ProposalQueue, ProposalQueueError, QueuedProposal, RatchetTreeExtension, StagedCommit,
19        WireFormatPolicy,
20    },
21    key_packages::KeyPackage,
22    messages::{
23        group_info::{GroupInfo, GroupInfoTBS},
24        Commit, Welcome,
25    },
26    prelude::{CredentialWithKey, LeafNodeParameters, LibraryError, NewSignerBundle},
27    schedule::{
28        psk::{load_psks, PskSecret},
29        JoinerSecret, KeySchedule, PreSharedKeyId,
30    },
31    storage::{OpenMlsProvider, StorageProvider},
32    versions::ProtocolVersion,
33};
34
35pub(crate) mod external_commits;
36
37pub use external_commits::{ExternalCommitBuilder, ExternalCommitBuilderError};
38
39#[cfg(doc)]
40use super::MlsGroupJoinConfig;
41
42use super::{
43    mls_auth_content::AuthenticatedContent,
44    staged_commit::{MemberStagedCommitState, StagedCommitState},
45    AddProposal, CreateCommitResult, GroupContextExtensionProposal, MlsGroup, MlsGroupState,
46    MlsMessageOut, PendingCommitState, Proposal, RemoveProposal, Sender,
47};
48
49#[derive(Debug)]
50struct ExternalCommitInfo {
51    aad: Vec<u8>,
52    credential: CredentialWithKey,
53    wire_format_policy: WireFormatPolicy,
54}
55
56/// This stage is for populating the builder.
57#[derive(Debug)]
58pub struct Initial {
59    own_proposals: Vec<Proposal>,
60    force_self_update: bool,
61    leaf_node_parameters: LeafNodeParameters,
62    create_group_info: bool,
63    external_commit_info: Option<ExternalCommitInfo>,
64
65    /// Whether or not to clear the proposal queue of the group when staging the commit. Needs to
66    /// be done when we include the commits that have already been queued.
67    consume_proposal_store: bool,
68}
69
70impl Default for Initial {
71    fn default() -> Self {
72        Initial {
73            consume_proposal_store: true,
74            force_self_update: false,
75            leaf_node_parameters: LeafNodeParameters::default(),
76            create_group_info: false,
77            own_proposals: vec![],
78            external_commit_info: None,
79        }
80    }
81}
82
83/// This stage is after the PSKs were loaded, ready for validation
84pub struct LoadedPsks {
85    own_proposals: Vec<Proposal>,
86    force_self_update: bool,
87    leaf_node_parameters: LeafNodeParameters,
88    create_group_info: bool,
89    external_commit_info: Option<ExternalCommitInfo>,
90
91    /// Whether or not to clear the proposal queue of the group when staging the commit. Needs to
92    /// be done when we include the commits that have already been queued.
93    consume_proposal_store: bool,
94    psks: Vec<(PreSharedKeyId, Secret)>,
95}
96
97/// This stage is after we validated the data, ready for staging and exporting the messages
98pub struct Complete {
99    result: CreateCommitResult,
100    // Only for external commits
101    original_wire_format_policy: Option<WireFormatPolicy>,
102}
103
104/// The [`CommitBuilder`] is used to easily and dynamically build commit messages.
105/// It operates in a series of stages:
106///
107/// The [`Initial`] stage is used to populate the builder with proposals and other data using
108/// method calls on the builder that let the builder stay in the same stage.
109///
110/// The next stage is [`LoadedPsks`], and it signifies the stage after the builder loaded the the
111/// pre-shared keys for the PreSharedKey proposals in this commit.
112///
113/// Then comes the [`Complete`] stage, which denotes that all data has been validated. From this
114/// stage, the commit can be staged in the group, and the outgoing messages returned.
115///
116/// For example, to create a commit to a new Add proposal with a KeyPackage `key_package_to_add`
117/// that does not commit to the proposals in the proposal store, one could build the commit as
118/// follows:
119///
120/// ```rust,ignore
121/// let message_bundle: CommitMessageBundle = mls_group
122///   .commit_builder()
123///   .consume_proposal_store(false)
124///   .add_proposal(key_package_to_add)
125///   .load_psks(provider.storage())?
126///   .build(provider.rand(), provider.crypto(), signer, app_policy_proposals)?
127///   .stage_commit(provider)?;
128///
129/// let commit = message_bundle.commit();
130/// let welcome = message_bundle.welcome().expect("expected a welcome since there was an add");
131/// let group_info = message_bundle.welcome().expect("expected a group info since there was an add");
132/// ```
133///
134/// In this example `signer` is a reference to a [`Signer`] and `app_policy_proposals` is the
135/// application-defined policy for which proposals to accept, implemented by an
136/// `FnMut(&QueuedProposal) -> bool`.
137///
138/// See the [book] for another example.
139///
140/// [book]: https://book.openmls.tech/user_manual/add_members.html
141#[derive(Debug)]
142pub struct CommitBuilder<'a, T, G: BorrowMut<MlsGroup> = &'a mut MlsGroup> {
143    /// A mutable reference to the MlsGroup. This means that we hold an exclusive lock on the group
144    /// for the lifetime of this builder.
145    group: G,
146
147    /// The current stage
148    stage: T,
149
150    pd: PhantomData<&'a ()>,
151}
152
153impl<'a, T, G: BorrowMut<MlsGroup>> CommitBuilder<'a, T, G> {
154    pub(crate) fn replace_stage<NextStage>(
155        self,
156        next_stage: NextStage,
157    ) -> (T, CommitBuilder<'a, NextStage, G>) {
158        self.map_stage(|prev_stage| (prev_stage, next_stage))
159    }
160
161    pub(crate) fn into_stage<NextStage>(
162        self,
163        next_stage: NextStage,
164    ) -> CommitBuilder<'a, NextStage, G> {
165        self.replace_stage(next_stage).1
166    }
167
168    fn take_stage(self) -> (T, CommitBuilder<'a, (), G>) {
169        self.replace_stage(())
170    }
171
172    fn map_stage<NextStage, Aux, F: FnOnce(T) -> (Aux, NextStage)>(
173        self,
174        f: F,
175    ) -> (Aux, CommitBuilder<'a, NextStage, G>) {
176        let Self {
177            group,
178            stage,
179            pd: PhantomData,
180        } = self;
181
182        let (aux, stage) = f(stage);
183
184        (
185            aux,
186            CommitBuilder {
187                group,
188                stage,
189                pd: PhantomData,
190            },
191        )
192    }
193
194    #[cfg(feature = "fork-resolution")]
195    pub(crate) fn stage(&self) -> &T {
196        &self.stage
197    }
198}
199
200impl MlsGroup {
201    /// Returns a builder for commits.
202    pub fn commit_builder(&mut self) -> CommitBuilder<'_, Initial> {
203        CommitBuilder::<'_, Initial, &mut MlsGroup>::new(self)
204    }
205}
206
207// Impls that only apply to non-external commits.
208impl<'a> CommitBuilder<'a, Initial, &mut MlsGroup> {
209    /// Sets whether or not the proposals in the proposal store of the group should be included in
210    /// the commit. Defaults to `true`.
211    pub fn consume_proposal_store(mut self, consume_proposal_store: bool) -> Self {
212        self.stage.consume_proposal_store = consume_proposal_store;
213        self
214    }
215
216    /// Sets whether or not the commit should force a self-update. Defaults to `false`.
217    pub fn force_self_update(mut self, force_self_update: bool) -> Self {
218        self.stage.force_self_update = force_self_update;
219        self
220    }
221
222    /// Adds an Add proposal to the provided [`KeyPackage`] to the list of proposals to be
223    /// committed.
224    pub fn propose_adds(mut self, key_packages: impl IntoIterator<Item = KeyPackage>) -> Self {
225        self.stage.own_proposals.extend(
226            key_packages
227                .into_iter()
228                .map(|key_package| Proposal::Add(AddProposal { key_package })),
229        );
230        self
231    }
232
233    /// Adds a Remove proposal for the provided [`LeafNodeIndex`]es to the list of proposals to be
234    /// committed.
235    pub fn propose_removals(mut self, removed: impl IntoIterator<Item = LeafNodeIndex>) -> Self {
236        self.stage.own_proposals.extend(
237            removed
238                .into_iter()
239                .map(|removed| Proposal::Remove(RemoveProposal { removed })),
240        );
241        self
242    }
243
244    /// Adds a GroupContextExtensions proposal for the provided [`Extensions`] to the list of
245    /// proposals to be committed.
246    pub fn propose_group_context_extensions(mut self, extensions: Extensions) -> Self {
247        self.stage
248            .own_proposals
249            .push(Proposal::GroupContextExtensions(
250                GroupContextExtensionProposal::new(extensions),
251            ));
252        self
253    }
254
255    /// Adds a proposal to the proposals to be committed. To add multiple
256    /// proposals, use [`Self::add_proposals`].
257    pub fn add_proposal(mut self, proposal: Proposal) -> Self {
258        self.stage.own_proposals.push(proposal);
259        self
260    }
261
262    /// Adds the proposals in the iterator to the proposals to be committed.
263    pub fn add_proposals(mut self, proposals: impl IntoIterator<Item = Proposal>) -> Self {
264        self.stage.own_proposals.extend(proposals);
265        self
266    }
267}
268
269// Impls that apply to regular and external commits.
270impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, Initial, G> {
271    /// returns a new [`CommitBuilder`] for the given [`MlsGroup`].
272    pub fn new(group: G) -> CommitBuilder<'a, Initial, G> {
273        let stage = Initial {
274            create_group_info: group.borrow().configuration().use_ratchet_tree_extension,
275            ..Default::default()
276        };
277        CommitBuilder {
278            group,
279            stage,
280            pd: PhantomData,
281        }
282    }
283
284    /// Sets whether or not a [`GroupInfo`] should be created when the commit is staged. Defaults to
285    /// the value of the [`MlsGroup`]s [`MlsGroupJoinConfig`].
286    pub fn create_group_info(mut self, create_group_info: bool) -> Self {
287        self.stage.create_group_info = create_group_info;
288        self
289    }
290
291    /// Sets the leaf node parameters for the new leaf node in a self-update. Implies that a
292    /// self-update takes place.
293    pub fn leaf_node_parameters(mut self, leaf_node_parameters: LeafNodeParameters) -> Self {
294        self.stage.leaf_node_parameters = leaf_node_parameters;
295        self
296    }
297
298    /// Loads the PSKs for the PskProposals marked for inclusion and moves on to the next phase.
299    pub fn load_psks<Storage: StorageProvider>(
300        self,
301        storage: &'a Storage,
302    ) -> Result<CommitBuilder<'a, LoadedPsks, G>, CreateCommitError> {
303        let psk_ids: Vec<_> = self
304            .stage
305            .own_proposals
306            .iter()
307            .chain(
308                self.group
309                    .borrow()
310                    .proposal_store()
311                    .proposals()
312                    .map(|queued_proposal| queued_proposal.proposal()),
313            )
314            .filter_map(|proposal| match proposal {
315                Proposal::PreSharedKey(psk_proposal) => Some(psk_proposal.clone().into_psk_id()),
316                _ => None,
317            })
318            .collect();
319
320        // Load the PSKs and make the PskIds owned.
321        let psks = load_psks(storage, &self.group.borrow().resumption_psk_store, &psk_ids)?
322            .into_iter()
323            .map(|(psk_id_ref, key)| (psk_id_ref.clone(), key))
324            .collect();
325
326        Ok(self
327            .map_stage(|stage| {
328                (
329                    (),
330                    LoadedPsks {
331                        own_proposals: stage.own_proposals,
332                        psks,
333                        force_self_update: stage.force_self_update,
334                        leaf_node_parameters: stage.leaf_node_parameters,
335                        consume_proposal_store: stage.consume_proposal_store,
336                        create_group_info: stage.create_group_info,
337                        external_commit_info: stage.external_commit_info,
338                    },
339                )
340            })
341            .1)
342    }
343}
344
345impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, LoadedPsks, G> {
346    /// Validates the inputs and builds the commit. The last argument `f` is a function that lets
347    /// the caller filter the proposals that are considered for inclusion. This provides a way for
348    /// the application to enforce custom policies in the creation of commits.
349    pub fn build<S: Signer>(
350        self,
351        rand: &impl OpenMlsRand,
352        crypto: &impl OpenMlsCrypto,
353        signer: &S,
354        f: impl FnMut(&QueuedProposal) -> bool,
355    ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
356        self.build_internal(rand, crypto, signer, None::<NewSignerBundle<'_, S>>, f)
357    }
358
359    /// Just like `build`, this function validates the inputs and builds the
360    /// commit. The last argument `f` is a function that lets the caller filter
361    /// the proposals that are considered for inclusion. This provides a way for
362    /// the application to enforce custom policies in the creation of commits.
363    ///
364    /// In contrast to `build`, this function can be used to create commits that
365    /// rotate the own leaf node's signature key.
366    pub fn build_with_new_signer<S: Signer>(
367        self,
368        rand: &impl OpenMlsRand,
369        crypto: &impl OpenMlsCrypto,
370        old_signer: &impl Signer,
371        new_signer: NewSignerBundle<'_, S>,
372        f: impl FnMut(&QueuedProposal) -> bool,
373    ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
374        self.build_internal(rand, crypto, old_signer, Some(new_signer), f)
375    }
376
377    fn build_internal<S: Signer>(
378        self,
379        rand: &impl OpenMlsRand,
380        crypto: &impl OpenMlsCrypto,
381        old_signer: &impl Signer,
382        new_signer: Option<NewSignerBundle<'_, S>>,
383        f: impl FnMut(&QueuedProposal) -> bool,
384    ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
385        let (mut cur_stage, builder) = self.take_stage();
386        let group = builder.group.borrow();
387        let ciphersuite = group.ciphersuite();
388        let own_leaf_index = group.own_leaf_index();
389        let (sender, is_external_commit) = match cur_stage.external_commit_info {
390            None => (Sender::build_member(own_leaf_index), false),
391            Some(_) => (Sender::NewMemberCommit, true),
392        };
393        let psks = cur_stage.psks;
394
395        // put the pending and uniform proposals into a uniform shape,
396        // i.e. produce queued proposals from the own proposals
397        let own_proposals: Vec<_> = cur_stage
398            .own_proposals
399            .into_iter()
400            .map(|proposal| {
401                QueuedProposal::from_proposal_and_sender(ciphersuite, crypto, proposal, &sender)
402            })
403            .collect::<Result<_, _>>()?;
404
405        // prepare an iterator for the proposals in the group's proposal store, but only if the
406        // flag is set.
407        let group_proposal_store_queue = group
408            .pending_proposals()
409            .filter(|_| cur_stage.consume_proposal_store)
410            .cloned();
411
412        // prepare the iterator for the proposal validation and seletion function. That function
413        // assumes that "earlier in the list" means "older", so since our own proposals are
414        // newest, we have to put them last.
415        let proposal_queue = group_proposal_store_queue.chain(own_proposals).filter(f);
416
417        let (proposal_queue, contains_own_updates) =
418            ProposalQueue::filter_proposals(proposal_queue, group.own_leaf_index).map_err(|e| {
419                match e {
420                    ProposalQueueError::LibraryError(e) => e.into(),
421                    ProposalQueueError::ProposalNotFound => CreateCommitError::MissingProposal,
422                    ProposalQueueError::UpdateFromExternalSender
423                    | ProposalQueueError::SelfRemoveFromNonMember => {
424                        CreateCommitError::WrongProposalSenderType
425                    }
426                }
427            })?;
428
429        // Validate the proposals by doing the following checks:
430
431        // ValSem113: All Proposals: The proposal type must be supported by all
432        // members of the group
433        group
434            .public_group
435            .validate_proposal_type_support(&proposal_queue)?;
436        // ValSem101
437        // ValSem102
438        // ValSem103
439        // ValSem104
440        group
441            .public_group
442            .validate_key_uniqueness(&proposal_queue, None)?;
443        // ValSem105
444        group.public_group.validate_add_proposals(&proposal_queue)?;
445        // ValSem106
446        // ValSem109
447        group.public_group.validate_capabilities(&proposal_queue)?;
448        // ValSem107
449        // ValSem108
450        group
451            .public_group
452            .validate_remove_proposals(&proposal_queue)?;
453        group
454            .public_group
455            .validate_pre_shared_key_proposals(&proposal_queue)?;
456        // Validate update proposals for member commits
457        // ValSem110
458        // ValSem111
459        // ValSem112
460        group
461            .public_group
462            .validate_update_proposals(&proposal_queue, own_leaf_index)?;
463
464        // ValSem208
465        // ValSem209
466        group
467            .public_group
468            .validate_group_context_extensions_proposal(&proposal_queue)?;
469
470        if is_external_commit {
471            group
472                .public_group
473                .validate_external_commit(&proposal_queue)?;
474        }
475
476        let proposal_reference_list = proposal_queue.commit_list();
477
478        // Make a copy of the public group to apply proposals safely
479        let mut diff = group.public_group.empty_diff();
480
481        // Apply proposals to tree
482        let apply_proposals_values = diff.apply_proposals(&proposal_queue, own_leaf_index)?;
483        if apply_proposals_values.self_removed && !is_external_commit {
484            return Err(CreateCommitError::CannotRemoveSelf);
485        }
486
487        let path_computation_result =
488            // If path is needed, compute path values
489            if apply_proposals_values.path_required
490                || contains_own_updates
491                || cur_stage.force_self_update
492                || !cur_stage.leaf_node_parameters.is_empty()
493            {
494                let commit_type = match &cur_stage.external_commit_info {
495                    Some(ExternalCommitInfo { credential , ..}) => {
496                        CommitType::External(credential.clone())
497                    }
498                    None => CommitType::Member,
499                };
500                // Process the path. This includes updating the provisional
501                // group context by updating the epoch and computing the new
502                // tree hash.
503                if let Some(new_signer) = new_signer {
504                    if let Some(credential_with_key) =
505                        cur_stage.leaf_node_parameters.credential_with_key()
506                    {
507                        if credential_with_key != &new_signer.credential_with_key {
508                            return Err(CreateCommitError::InvalidLeafNodeParameters);
509                        }
510                    }
511                    cur_stage.leaf_node_parameters.set_credential_with_key(
512                        new_signer.credential_with_key,
513                    );
514                    diff.compute_path(
515                        rand,
516                        crypto,
517                        own_leaf_index,
518                        apply_proposals_values.exclusion_list(),
519                        &commit_type,
520                        &cur_stage.leaf_node_parameters,
521                        new_signer.signer,
522                        apply_proposals_values.extensions.clone()
523                    )?
524                } else {
525                    diff.compute_path(
526                        rand,
527                        crypto,
528                        own_leaf_index,
529                        apply_proposals_values.exclusion_list(),
530                        &commit_type,
531                        &cur_stage.leaf_node_parameters,
532                        old_signer,
533                        apply_proposals_values.extensions.clone()
534                    )?
535                }
536            } else {
537                // If path is not needed, update the group context and return
538                // empty path processing results
539                diff.update_group_context(crypto, apply_proposals_values.extensions.clone())?;
540                PathComputationResult::default()
541            };
542
543        let update_path_leaf_node = path_computation_result
544            .encrypted_path
545            .as_ref()
546            .map(|path| path.leaf_node().clone());
547
548        // Create commit message
549        let commit = Commit {
550            proposals: proposal_reference_list,
551            path: path_computation_result.encrypted_path,
552        };
553
554        let framing_parameters =
555            if let Some(ExternalCommitInfo { aad, .. }) = &cur_stage.external_commit_info {
556                FramingParameters::new(aad, WireFormat::PublicMessage)
557            } else {
558                group.framing_parameters()
559            };
560
561        // Build AuthenticatedContent
562        let mut authenticated_content = AuthenticatedContent::commit(
563            framing_parameters,
564            sender,
565            commit,
566            group.public_group.group_context(),
567            old_signer,
568        )?;
569
570        // Update the confirmed transcript hash using the commit we just created.
571        diff.update_confirmed_transcript_hash(crypto, &authenticated_content)?;
572
573        let serialized_provisional_group_context = diff
574            .group_context()
575            .tls_serialize_detached()
576            .map_err(LibraryError::missing_bound_check)?;
577
578        let joiner_secret = JoinerSecret::new(
579            crypto,
580            ciphersuite,
581            path_computation_result.commit_secret,
582            group.group_epoch_secrets().init_secret(),
583            &serialized_provisional_group_context,
584        )
585        .map_err(LibraryError::unexpected_crypto_error)?;
586
587        // Prepare the PskSecret
588        let psk_secret = { PskSecret::new(crypto, ciphersuite, psks)? };
589
590        // Create key schedule
591        let mut key_schedule = KeySchedule::init(ciphersuite, crypto, &joiner_secret, psk_secret)?;
592
593        let serialized_provisional_group_context = diff
594            .group_context()
595            .tls_serialize_detached()
596            .map_err(LibraryError::missing_bound_check)?;
597
598        let welcome_secret = key_schedule
599            .welcome(crypto, ciphersuite)
600            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
601        key_schedule
602            .add_context(crypto, &serialized_provisional_group_context)
603            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
604        let provisional_epoch_secrets = key_schedule
605            .epoch_secrets(crypto, ciphersuite)
606            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
607
608        // Calculate the confirmation tag
609        let confirmation_tag = provisional_epoch_secrets
610            .confirmation_key()
611            .tag(
612                crypto,
613                ciphersuite,
614                diff.group_context().confirmed_transcript_hash(),
615            )
616            .map_err(LibraryError::unexpected_crypto_error)?;
617
618        // Set the confirmation tag
619        authenticated_content.set_confirmation_tag(confirmation_tag.clone());
620
621        diff.update_interim_transcript_hash(ciphersuite, crypto, confirmation_tag.clone())?;
622
623        // If there are invitations, we need to build a welcome
624        let needs_welcome = !apply_proposals_values.invitation_list.is_empty();
625
626        // We need a GroupInfo if we need to build a Welcome, or if
627        // `create_group_info` is set to `true`. If not overridden, `create_group_info`
628        // is set to the `use_ratchet_tree` flag in the group configuration.
629        let needs_group_info = needs_welcome || cur_stage.create_group_info;
630
631        let group_info = if !needs_group_info {
632            None
633        } else {
634            // Build ExternalPub extension
635            let external_pub = provisional_epoch_secrets
636                .external_secret()
637                .derive_external_keypair(crypto, ciphersuite)
638                .map_err(LibraryError::unexpected_crypto_error)?
639                .public;
640            let external_pub_extension =
641                Extension::ExternalPub(ExternalPubExtension::new(external_pub.into()));
642
643            // Create the ratchet tree extension if necessary
644            let extensions: Extensions = if group.configuration().use_ratchet_tree_extension {
645                Extensions::from_vec(vec![
646                    Extension::RatchetTree(RatchetTreeExtension::new(diff.export_ratchet_tree())),
647                    external_pub_extension,
648                ])?
649            } else {
650                Extensions::single(external_pub_extension)
651            };
652
653            // Create to-be-signed group info.
654            let group_info_tbs = {
655                GroupInfoTBS::new(
656                    diff.group_context().clone(),
657                    extensions,
658                    confirmation_tag,
659                    own_leaf_index,
660                )
661            };
662            // Sign to-be-signed group info.
663            Some(group_info_tbs.sign(old_signer)?)
664        };
665
666        let welcome_option = if !needs_welcome {
667            None
668        } else {
669            // Encrypt GroupInfo object
670            let (welcome_key, welcome_nonce) = welcome_secret
671                .derive_welcome_key_nonce(crypto, ciphersuite)
672                .map_err(LibraryError::unexpected_crypto_error)?;
673            let encrypted_group_info = welcome_key
674                .aead_seal(
675                    crypto,
676                    group_info
677                        .as_ref()
678                        .ok_or_else(|| LibraryError::custom("GroupInfo was not computed"))?
679                        .tls_serialize_detached()
680                        .map_err(LibraryError::missing_bound_check)?
681                        .as_slice(),
682                    &[],
683                    &welcome_nonce,
684                )
685                .map_err(LibraryError::unexpected_crypto_error)?;
686
687            // Create group secrets for later use, so we can afterwards consume the
688            // `joiner_secret`.
689            let encrypted_secrets = diff.encrypt_group_secrets(
690                &joiner_secret,
691                apply_proposals_values.invitation_list,
692                path_computation_result.plain_path.as_deref(),
693                &apply_proposals_values.presharedkeys,
694                &encrypted_group_info,
695                crypto,
696                own_leaf_index,
697            )?;
698
699            // Create welcome message
700            let welcome = Welcome::new(ciphersuite, encrypted_secrets, encrypted_group_info);
701            Some(welcome)
702        };
703
704        let (provisional_group_epoch_secrets, provisional_message_secrets) =
705            provisional_epoch_secrets.split_secrets(
706                serialized_provisional_group_context,
707                diff.tree_size(),
708                own_leaf_index,
709            );
710
711        let staged_commit_state = MemberStagedCommitState::new(
712            provisional_group_epoch_secrets,
713            provisional_message_secrets,
714            diff.into_staged_diff(crypto, ciphersuite)?,
715            path_computation_result.new_keypairs,
716            // The committer is not allowed to include their own update
717            // proposal, so there is no extra keypair to store here.
718            None,
719            update_path_leaf_node,
720        );
721        let staged_commit = StagedCommit::new(
722            proposal_queue,
723            StagedCommitState::GroupMember(Box::new(staged_commit_state)),
724        );
725
726        Ok(builder.into_stage(Complete {
727            result: CreateCommitResult {
728                commit: authenticated_content,
729                welcome_option,
730                staged_commit,
731                group_info: group_info.filter(|_| cur_stage.create_group_info),
732            },
733            original_wire_format_policy: cur_stage
734                .external_commit_info
735                .as_ref()
736                .map(|info| info.wire_format_policy),
737        }))
738    }
739}
740
741// Impls that apply only to regular commits.
742impl CommitBuilder<'_, Complete, &mut MlsGroup> {
743    #[cfg(test)]
744    pub(crate) fn commit_result(self) -> CreateCommitResult {
745        self.stage.result
746    }
747
748    /// Stages the commit and returns the protocol messages.
749    pub fn stage_commit<Provider: OpenMlsProvider>(
750        self,
751        provider: &Provider,
752    ) -> Result<CommitMessageBundle, CommitBuilderStageError<Provider::StorageError>> {
753        let Self {
754            group,
755            stage:
756                Complete {
757                    result: create_commit_result,
758                    original_wire_format_policy: _,
759                },
760            ..
761        } = self;
762
763        // Set the current group state to [`MlsGroupState::PendingCommit`],
764        // storing the current [`StagedCommit`] from the commit results
765        group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
766            create_commit_result.staged_commit,
767        )));
768
769        provider
770            .storage()
771            .write_group_state(group.group_id(), &group.group_state)
772            .map_err(CommitBuilderStageError::KeyStoreError)?;
773
774        group.reset_aad();
775
776        // Convert PublicMessage messages to MLSMessage and encrypt them if required by the
777        // configuration.
778        //
779        // Note that this performs writes to the storage, so we should do that here, rather than
780        // when working with the result.
781        let mls_message = group.content_to_mls_message(create_commit_result.commit, provider)?;
782
783        Ok(CommitMessageBundle {
784            version: group.version(),
785            commit: mls_message,
786            welcome: create_commit_result.welcome_option,
787            group_info: create_commit_result.group_info,
788        })
789    }
790}
791
792/// Contains the messages that are produced by committing. The messages can be accessed individually
793/// using getters or through the [`IntoIterator`] interface.
794#[derive(Debug, Clone)]
795pub struct CommitMessageBundle {
796    version: ProtocolVersion,
797    commit: MlsMessageOut,
798    welcome: Option<Welcome>,
799    group_info: Option<GroupInfo>,
800}
801
802#[cfg(test)]
803impl CommitMessageBundle {
804    pub fn new(
805        version: ProtocolVersion,
806        commit: MlsMessageOut,
807        welcome: Option<Welcome>,
808        group_info: Option<GroupInfo>,
809    ) -> Self {
810        Self {
811            version,
812            commit,
813            welcome,
814            group_info,
815        }
816    }
817}
818
819impl CommitMessageBundle {
820    // borrowed getters
821
822    /// Gets a the Commit messsage. For owned version, see [`Self::into_commit`].
823    pub fn commit(&self) -> &MlsMessageOut {
824        &self.commit
825    }
826
827    /// Gets a the Welcome messsage. Only [`Some`] if new clients have been added in the commit.
828    /// For owned version, see [`Self::into_welcome`].
829    pub fn welcome(&self) -> Option<&Welcome> {
830        self.welcome.as_ref()
831    }
832
833    /// Gets a the Welcome messsage. Only [`Some`] if new clients have been added in the commit.
834    /// Performs a copy of the Welcome. For owned version, see [`Self::into_welcome_msg`].
835    pub fn to_welcome_msg(&self) -> Option<MlsMessageOut> {
836        self.welcome
837            .as_ref()
838            .map(|welcome| MlsMessageOut::from_welcome(welcome.clone(), self.version))
839    }
840
841    /// Gets a the GroupInfo message. Only [`Some`] if new clients have been added or the group
842    /// configuration has `use_ratchet_tree_extension` set.
843    /// For owned version, see [`Self::into_group_info`].
844    pub fn group_info(&self) -> Option<&GroupInfo> {
845        self.group_info.as_ref()
846    }
847
848    /// Gets all three messages, some of which optional. For owned version, see
849    /// [`Self::into_contents`].
850    pub fn contents(&self) -> (&MlsMessageOut, Option<&Welcome>, Option<&GroupInfo>) {
851        (
852            &self.commit,
853            self.welcome.as_ref(),
854            self.group_info.as_ref(),
855        )
856    }
857
858    // owned getters
859    /// Gets a the Commit messsage. This method consumes the [`CommitMessageBundle`]. For a borrowed
860    /// version see [`Self::commit`].
861    pub fn into_commit(self) -> MlsMessageOut {
862        self.commit
863    }
864
865    /// Gets a the Welcome messsage. Only [`Some`] if new clients have been added in the commit.
866    /// This method consumes the [`CommitMessageBundle`]. For a borrowed version see
867    /// [`Self::welcome`].
868    pub fn into_welcome(self) -> Option<Welcome> {
869        self.welcome
870    }
871
872    /// Gets a the Welcome messsage. Only [`Some`] if new clients have been added in the commit.
873    /// For a borrowed version, see [`Self::to_welcome_msg`].
874    pub fn into_welcome_msg(self) -> Option<MlsMessageOut> {
875        self.welcome
876            .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version))
877    }
878
879    /// Gets a the GroupInfo message. Only [`Some`] if new clients have been added or the group
880    /// configuration has `use_ratchet_tree_extension` set.
881    /// This method consumes the [`CommitMessageBundle`]. For a borrowed version see
882    /// [`Self::group_info`].
883    pub fn into_group_info(self) -> Option<GroupInfo> {
884        self.group_info
885    }
886
887    /// Gets a the GroupInfo messsage. Only [`Some`] if new clients have been added in the commit.
888    pub fn into_group_info_msg(self) -> Option<MlsMessageOut> {
889        self.group_info.map(|group_info| group_info.into())
890    }
891
892    /// Gets all three messages, some of which optional. This method consumes the
893    /// [`CommitMessageBundle`]. For a borrowed version see [`Self::contents`].
894    pub fn into_contents(self) -> (MlsMessageOut, Option<Welcome>, Option<GroupInfo>) {
895        (self.commit, self.welcome, self.group_info)
896    }
897
898    /// Gets all three messages, some of which optional, as [`MlsMessageOut`].
899    /// This method consumes the [`CommitMessageBundle`].
900    pub fn into_messages(self) -> (MlsMessageOut, Option<MlsMessageOut>, Option<MlsMessageOut>) {
901        (
902            self.commit,
903            self.welcome
904                .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version)),
905            self.group_info.map(|group_info| group_info.into()),
906        )
907    }
908}
909
910impl IntoIterator for CommitMessageBundle {
911    type Item = MlsMessageOut;
912
913    type IntoIter = core::iter::Chain<
914        core::iter::Chain<
915            core::option::IntoIter<MlsMessageOut>,
916            core::option::IntoIter<MlsMessageOut>,
917        >,
918        core::option::IntoIter<MlsMessageOut>,
919    >;
920
921    fn into_iter(self) -> Self::IntoIter {
922        let welcome = self.to_welcome_msg();
923        let group_info = self.group_info.map(|group_info| group_info.into());
924
925        Some(self.commit)
926            .into_iter()
927            .chain(welcome)
928            .chain(group_info)
929    }
930}