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