Skip to main content

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