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