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,
29        NewSignerBundle, PreSharedKeyProposal,
30    },
31    schedule::{
32        psk::{load_psks, PskSecret, ResumptionPsk, ResumptionPskUsage},
33        EpochSecretsResult, JoinerSecret, KeySchedule, PreSharedKeyId, Psk,
34    },
35    storage::{OpenMlsProvider, StorageProvider},
36    treesync::errors::LeafNodeValidationError,
37    versions::ProtocolVersion,
38};
39#[cfg(feature = "virtual-clients-draft")]
40use crate::{
41    components::vc_commit_data::VirtualClientCommitData,
42    components::vc_derivation_info::{
43        require_newest_vc_derivation_epoch, DerivationInfo, DerivationInfoTbe, EpochEncryptionKey,
44        EpochId, ExternalInitSecret, OperationSecret, VcDerivationEpochState,
45        VirtualClientOperationType, VirtualClientsError,
46    },
47    components::vc_operation_tree::OperationSecretTree,
48    extensions::AppDataDictionary,
49    group::GroupId,
50};
51#[cfg(feature = "extensions-draft")]
52use crate::{
53    messages::proposals::AppDataUpdateProposal,
54    prelude::processing::{AppDataDictionaryUpdater, AppDataUpdates},
55    schedule::application_export_tree::ApplicationExportTree,
56};
57
58/// Per-commit virtual-clients state allocated by
59/// [`CommitBuilder::vc_emulation`] and consumed by `build`.
60///
61/// `vc_emulation` advances the own `LeafNode` operation ratchet by one
62/// generation and immediately persists the advanced tree, before the commit
63/// message exists. A builder that is discarded after the setter therefore
64/// burns a generation, as does a commit the DS rejects. That is harmless:
65/// sibling ratchets skip over a burned generation, retaining the skipped
66/// generation secrets inside their copy of the operation secret tree.
67#[cfg(feature = "virtual-clients-draft")]
68#[derive(Debug)]
69struct VcLoaded {
70    epoch_id: EpochId,
71    emulation_leaf_index: LeafNodeIndex,
72    epoch_encryption_key: EpochEncryptionKey,
73    emulation_ciphersuite: openmls_traits::types::Ciphersuite,
74    generation: u32,
75    operation_secret: OperationSecret,
76    /// The resolved `AppDataDictionary` produced by the leaf-configuration
77    /// pre-check in `vc_emulation`, carried to `build` so the VC
78    /// derivation-info injection preserves every other entry.
79    resolved_dictionary: AppDataDictionary,
80}
81
82pub(crate) mod external_commits;
83
84pub use external_commits::{ExternalCommitBuilder, ExternalCommitBuilderError};
85
86#[cfg(doc)]
87use super::MlsGroupJoinConfig;
88
89use super::{
90    branch::BranchInfo,
91    mls_auth_content::AuthenticatedContent,
92    staged_commit::{MemberStagedCommitState, StagedCommitState},
93    AddProposal, CreateCommitResult, GroupContextExtensionProposal, MlsGroup, MlsGroupState,
94    MlsMessageOut, PendingCommitState, Proposal, RemoveProposal, Sender,
95};
96
97#[cfg(feature = "virtual-clients-draft")]
98use super::HandshakeConfirmationData;
99
100#[derive(Debug)]
101struct ExternalCommitInfo {
102    aad: Vec<u8>,
103    /// The authoritative credential and signature key for the external
104    /// committer's leaf. `build_internal` folds it into the leaf node
105    /// parameters and rejects parameters that pin a different credential.
106    credential: CredentialWithKey,
107    wire_format_policy: WireFormatPolicy,
108}
109
110#[derive(Debug, Default)]
111struct GroupInfoConfig {
112    create_group_info: bool,
113    use_ratchet_tree_extension: bool,
114    other_extensions: Vec<Extension>,
115}
116
117/// This stage is for populating the builder.
118#[derive(Debug)]
119pub struct Initial {
120    own_proposals: Vec<Proposal>,
121    force_self_update: bool,
122    leaf_node_parameters: LeafNodeParameters,
123    external_commit_info: Option<ExternalCommitInfo>,
124
125    /// Whether or not to clear the proposal queue of the group when staging the commit. Needs to
126    /// be done when we include the commits that have already been queued.
127    consume_proposal_store: bool,
128}
129
130impl Default for Initial {
131    fn default() -> Self {
132        Initial {
133            consume_proposal_store: true,
134            force_self_update: false,
135            leaf_node_parameters: LeafNodeParameters::default(),
136            own_proposals: vec![],
137            external_commit_info: None,
138        }
139    }
140}
141
142/// This stage is after the PSKs were loaded, ready for validation
143pub struct LoadedPsks {
144    own_proposals: Vec<Proposal>,
145    force_self_update: bool,
146    leaf_node_parameters: LeafNodeParameters,
147    external_commit_info: Option<ExternalCommitInfo>,
148
149    /// Whether or not to clear the proposal queue of the group when staging the commit. Needs to
150    /// be done when we include the commits that have already been queued.
151    consume_proposal_store: bool,
152    psks: Vec<(PreSharedKeyId, Secret)>,
153
154    /// The GroupInfo creation config
155    group_info_config: GroupInfoConfig,
156
157    #[cfg(feature = "extensions-draft")]
158    app_data_dictionary_updates: Option<AppDataUpdates>,
159}
160
161/// This stage is after we validated the data, ready for staging and exporting the messages
162#[derive(Debug)]
163pub struct Complete {
164    result: CreateCommitResult,
165    // Only for external commits
166    original_wire_format_policy: Option<WireFormatPolicy>,
167}
168
169/// The [`CommitBuilder`] is used to easily and dynamically build commit messages.
170/// It operates in a series of stages:
171///
172/// The [`Initial`] stage is used to populate the builder with proposals and other data using
173/// method calls on the builder that let the builder stay in the same stage.
174///
175/// The next stage is [`LoadedPsks`], and it signifies the stage after the builder loaded the the
176/// pre-shared keys for the PreSharedKey proposals in this commit.
177///
178/// Then comes the [`Complete`] stage, which denotes that all data has been validated. From this
179/// stage, the commit can be staged in the group, and the outgoing messages returned.
180///
181/// For example, to create a commit to a new Add proposal with a KeyPackage `key_package_to_add`
182/// that does not commit to the proposals in the proposal store, one could build the commit as
183/// follows:
184///
185/// ```rust,ignore
186/// let message_bundle: CommitMessageBundle = mls_group
187///   .commit_builder()
188///   .consume_proposal_store(false)
189///   .add_proposal(key_package_to_add)
190///   .load_psks(provider.storage())?
191///   .build(provider.rand(), provider.crypto(), signer, app_policy_proposals)?
192///   .stage_commit(provider)?;
193///
194/// let commit = message_bundle.commit();
195/// let welcome = message_bundle.welcome().expect("expected a welcome since there was an add");
196/// let group_info = message_bundle.welcome().expect("expected a group info since there was an add");
197/// ```
198///
199/// In this example `signer` is a reference to a [`Signer`] and `app_policy_proposals` is the
200/// application-defined policy for which proposals to accept, implemented by an
201/// `FnMut(&QueuedProposal) -> bool`.
202///
203/// See the [book] for another example.
204///
205/// [book]: https://book.openmls.tech/user_manual/add_members.html
206#[derive(Debug)]
207pub struct CommitBuilder<'a, T, G: BorrowMut<MlsGroup> = &'a mut MlsGroup> {
208    /// A mutable reference to the MlsGroup. This means that we hold an exclusive lock on the group
209    /// for the lifetime of this builder.
210    group: G,
211
212    /// The current stage
213    stage: T,
214
215    /// Virtual-clients material allocated by [`Self::vc_emulation`] and
216    /// consumed by `build`. Lives on the builder rather than on a stage
217    /// struct so the stage transitions can carry it through unchanged.
218    #[cfg(feature = "virtual-clients-draft")]
219    vc_loaded: Option<VcLoaded>,
220
221    /// Set by [`Self::derivation_epoch`]. `build` stages the marker action
222    /// in the group's Safe AAD before it assembles the commit.
223    #[cfg(feature = "virtual-clients-draft")]
224    vc_new_derivation_epoch: bool,
225
226    pd: PhantomData<&'a ()>,
227}
228
229impl<'a, T, G: BorrowMut<MlsGroup>> CommitBuilder<'a, T, G> {
230    pub(crate) fn replace_stage<NextStage>(
231        self,
232        next_stage: NextStage,
233    ) -> (T, CommitBuilder<'a, NextStage, G>) {
234        self.map_stage(|prev_stage| (prev_stage, next_stage))
235    }
236
237    pub(crate) fn into_stage<NextStage>(
238        self,
239        next_stage: NextStage,
240    ) -> CommitBuilder<'a, NextStage, G> {
241        self.replace_stage(next_stage).1
242    }
243
244    fn take_stage(self) -> (T, CommitBuilder<'a, (), G>) {
245        self.replace_stage(())
246    }
247
248    fn map_stage<NextStage, Aux, F: FnOnce(T) -> (Aux, NextStage)>(
249        self,
250        f: F,
251    ) -> (Aux, CommitBuilder<'a, NextStage, G>) {
252        let Self {
253            group,
254            stage,
255            #[cfg(feature = "virtual-clients-draft")]
256            vc_loaded,
257            #[cfg(feature = "virtual-clients-draft")]
258            vc_new_derivation_epoch,
259            pd: PhantomData,
260        } = self;
261
262        let (aux, stage) = f(stage);
263
264        (
265            aux,
266            CommitBuilder {
267                group,
268                stage,
269                #[cfg(feature = "virtual-clients-draft")]
270                vc_loaded,
271                #[cfg(feature = "virtual-clients-draft")]
272                vc_new_derivation_epoch,
273                pd: PhantomData,
274            },
275        )
276    }
277
278    #[cfg(feature = "fork-resolution")]
279    pub(crate) fn stage(&self) -> &T {
280        &self.stage
281    }
282
283    /// Returns the [`EpochId`] of the derivation epoch this commit acts from,
284    /// or `None` if no virtual-clients material was loaded.
285    #[cfg(feature = "virtual-clients-draft")]
286    pub fn vc_epoch_id(&self) -> Option<&EpochId> {
287        self.vc_loaded.as_ref().map(|loaded| &loaded.epoch_id)
288    }
289}
290
291impl MlsGroup {
292    /// Returns a builder for commits.
293    pub fn commit_builder(&mut self) -> CommitBuilder<'_, Initial> {
294        CommitBuilder::<'_, Initial, &mut MlsGroup>::new(self)
295    }
296}
297
298// Impls that only apply to non-external commits.
299impl<'a> CommitBuilder<'a, Initial, &mut MlsGroup> {
300    /// Sets whether or not the proposals in the proposal store of the group should be included in
301    /// the commit. Defaults to `true`.
302    pub fn consume_proposal_store(mut self, consume_proposal_store: bool) -> Self {
303        self.stage.consume_proposal_store = consume_proposal_store;
304        self
305    }
306
307    /// Sets whether or not the commit should force a self-update. Defaults to `false`.
308    pub fn force_self_update(mut self, force_self_update: bool) -> Self {
309        self.stage.force_self_update = force_self_update;
310        self
311    }
312
313    /// Adds an Add proposal to the provided [`KeyPackage`] to the list of proposals to be
314    /// committed.
315    pub fn propose_adds(mut self, key_packages: impl IntoIterator<Item = KeyPackage>) -> Self {
316        self.stage.own_proposals.extend(
317            key_packages
318                .into_iter()
319                .map(|key_package| Proposal::add(AddProposal { key_package })),
320        );
321        self
322    }
323
324    /// Adds a Remove proposal for the provided [`LeafNodeIndex`]es to the list of proposals to be
325    /// committed.
326    pub fn propose_removals(mut self, removed: impl IntoIterator<Item = LeafNodeIndex>) -> Self {
327        self.stage.own_proposals.extend(
328            removed
329                .into_iter()
330                .map(|removed| Proposal::remove(RemoveProposal { removed })),
331        );
332        self
333    }
334
335    /// Adds a GroupContextExtensions proposal for the provided [`Extensions`] to the list of
336    /// proposals to be committed.
337    pub fn propose_group_context_extensions(
338        mut self,
339        extensions: Extensions<GroupContext>,
340    ) -> Result<Self, CreateCommitError> {
341        let proposal = GroupContextExtensionProposal::new(extensions);
342        self.stage
343            .own_proposals
344            .push(Proposal::group_context_extensions(proposal));
345        Ok(self)
346    }
347    /// Adds a PreSharedKey proposal for the provided [`PreSharedKeyId`]s to the
348    /// list of proposals to be committed.
349    ///
350    /// Note that this should not be used for sub-group branching, as those PSKs
351    /// are not allowed in regular proposals. Please use
352    /// [`MlsGroupBuilder::branch`](crate::group::MlsGroupBuilder::branch) instead.
353    pub fn propose_psks(mut self, psk_ids: impl IntoIterator<Item = PreSharedKeyId>) -> Self {
354        self.stage.own_proposals.extend(
355            psk_ids
356                .into_iter()
357                .map(|psk_id| Proposal::psk(PreSharedKeyProposal::new(psk_id))),
358        );
359        self
360    }
361
362    /// Branches from a parent group into this (freshly created) group to form a
363    /// subgroup, as described in [RFC 9420 §11.3].
364    ///
365    /// This is the internal engine driven by
366    /// [`MlsGroupBuilder::branch`](crate::group::MlsGroupBuilder::branch), which
367    /// is the public entry point for sub-group branching and guarantees this is
368    /// called on a fresh (epoch-0) group with the parent's parameters.
369    ///
370    /// The parent group's parameters are provided via `branch_info`, which the
371    /// parent exports with
372    /// [`MlsGroup::branch_info`](crate::group::MlsGroup::branch_info).
373    ///
374    /// This adds a resumption [`PreSharedKeyId`] of usage `Branch` to the initial
375    /// commit, with a freshly sampled `psk_nonce` of length KDF.Nh, and injects
376    /// the parent group's resumption PSK secret so it is mixed into this
377    /// subgroup's key schedule.
378    ///
379    /// [RFC 9420 §11.3]: https://www.rfc-editor.org/rfc/rfc9420.html#name-subgroup-branching
380    pub(crate) fn branch(
381        mut self,
382        rand: &impl OpenMlsRand,
383        branch_info: &BranchInfo,
384    ) -> Result<Self, CreateCommitError> {
385        // Sample a fresh random nonce of length KDF.Nh, as required by the RFC.
386        let psk_id = PreSharedKeyId::new(
387            branch_info.ciphersuite(),
388            rand,
389            Psk::Resumption(ResumptionPsk::new(
390                ResumptionPskUsage::Branch,
391                branch_info.group_id().clone(),
392                branch_info.epoch(),
393            )),
394        )
395        .map_err(LibraryError::unexpected_crypto_error)?;
396        self = self.propose_psks([psk_id]);
397
398        // The branch PSK secret comes from a different group, so we clear this
399        // group's resumption PSK store and inject it at the sentinel epoch 0,
400        // where `load_psks` looks it up for branch usage.
401        let secret = branch_info.resumption_psk_secret().clone();
402        self.group.borrow_mut().resumption_psk_store.clear();
403        self.group
404            .borrow_mut()
405            .resumption_psk_store
406            .add(0.into(), secret);
407        Ok(self)
408    }
409
410    /// Adds a proposal to the proposals to be committed. To add multiple
411    /// proposals, use [`Self::add_proposals`].
412    pub fn add_proposal(mut self, proposal: Proposal) -> Self {
413        self.stage.own_proposals.push(proposal);
414        self
415    }
416
417    /// Adds the proposals in the iterator to the proposals to be committed.
418    pub fn add_proposals(mut self, proposals: impl IntoIterator<Item = Proposal>) -> Self {
419        self.stage.own_proposals.extend(proposals);
420        self
421    }
422}
423
424// Impls that apply to regular and external commits.
425impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, Initial, G> {
426    /// returns a new [`CommitBuilder`] for the given [`MlsGroup`].
427    pub fn new(group: G) -> CommitBuilder<'a, Initial, G> {
428        let stage = Initial {
429            ..Default::default()
430        };
431        CommitBuilder {
432            group,
433            stage,
434            #[cfg(feature = "virtual-clients-draft")]
435            vc_loaded: None,
436            #[cfg(feature = "virtual-clients-draft")]
437            vc_new_derivation_epoch: false,
438            pd: PhantomData,
439        }
440    }
441
442    /// Sets the leaf node parameters for the new leaf node in a self-update. Implies that a
443    /// self-update takes place.
444    pub fn leaf_node_parameters(mut self, leaf_node_parameters: LeafNodeParameters) -> Self {
445        self.stage.leaf_node_parameters = leaf_node_parameters;
446        self
447    }
448
449    /// Opt this commit into the virtual-clients-draft sender flow.
450    ///
451    /// The commit uses the newest derivation epoch of the emulation group named
452    /// by `emulation_group_id`, which is what the draft requires of every new
453    /// virtual-client operation. The epoch is resolved from the emulation
454    /// group's current state, so a commit that itself asks for a new derivation
455    /// epoch (see [`Self::derivation_epoch`]) still uses the epoch of its
456    /// input state: the requested one only exists once that commit is merged.
457    ///
458    /// This method loads the per-epoch operation secret tree and AEAD key from
459    /// the storage provider, validates the leaf configuration (see the
460    /// preconditions below), then advances the own `LeafNode` operation ratchet
461    /// by one generation and immediately persists the advanced tree. `build`
462    /// then:
463    ///
464    /// - derives the path secret and the new leaf's encryption keypair
465    ///   from the allocated `OperationSecret`, so a sibling virtual
466    ///   client can rederive them on the receiver side, and
467    /// - embeds an encrypted `DerivationInfo` blob under
468    ///   [`VC_COMPONENT_ID`](crate::components::vc_derivation_info::VC_COMPONENT_ID)
469    ///   in the new leaf's `app_data_dictionary` extension.
470    ///
471    /// Because the ratchet advance is persisted here, a builder that is
472    /// discarded after this call burns a generation. The same happens when
473    /// the DS rejects the commit. That is harmless because sibling ratchets
474    /// skip over a burned generation, retaining the skipped generation
475    /// secrets inside their copy of the operation secret tree.
476    ///
477    /// The leaf configuration is validated against the
478    /// `leaf_node_parameters` set on the builder so far, so call this after
479    /// configuring the self-update leaf. The application must ensure the new
480    /// leaf:
481    ///
482    /// - lists [`ExtensionType::AppDataDictionary`](crate::extensions::ExtensionType::AppDataDictionary)
483    ///   in its `Capabilities.extensions`, and
484    /// - signals support for
485    ///   [`VC_COMPONENT_ID`](crate::components::vc_derivation_info::VC_COMPONENT_ID).
486    ///
487    /// If those preconditions are not met this method fails with
488    /// `VirtualClientsError::AppDataDictionaryNotSupported` or
489    /// `VirtualClientsError::VcComponentNotListed` (wrapped in
490    /// [`CreateCommitError::VirtualClientsError`]) before allocating a
491    /// generation, so no operation secret is burned in that case.
492    ///
493    /// Fails with `VirtualClientsError::NoDerivationEpoch` if the emulation
494    /// group has no registered derivation epoch, and with
495    /// `VirtualClientsError::MissingDerivationEpochState` or
496    /// `VirtualClientsError::MissingOperationTree` if the resolved epoch's state
497    /// is gone. Neither the state nor the tree is instantiated on the fly, since
498    /// that could diverge from a sibling virtual client's already-advanced
499    /// ratchets.
500    ///
501    /// Implies that a self-update takes place: the commit will always have
502    /// a path even if no other proposals are queued.
503    #[cfg(feature = "virtual-clients-draft")]
504    pub fn vc_emulation<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
505        self,
506        crypto: &Crypto,
507        storage: &Storage,
508        emulation_group_id: &GroupId,
509    ) -> Result<Self, CreateCommitError> {
510        let epoch_id = require_newest_vc_derivation_epoch(storage, emulation_group_id)?;
511        self.vc_emulation_internal(crypto, storage, epoch_id)
512    }
513
514    /// Test-only variant of [`Self::vc_emulation`] that commits from the named
515    /// derivation epoch instead of the emulation group's newest one.
516    ///
517    /// Using an epoch other than the newest one violates the draft, which
518    /// requires every new virtual-client operation to use the newest derivation
519    /// epoch of the acting client's current emulation-group state. It exists to
520    /// construct scenarios that an application must not produce, such as a
521    /// sibling that acts on a stale emulation-group state.
522    #[cfg(all(feature = "virtual-clients-draft", any(test, feature = "test-utils")))]
523    pub fn vc_emulation_at_epoch<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
524        self,
525        crypto: &Crypto,
526        storage: &Storage,
527        epoch_id: EpochId,
528    ) -> Result<Self, CreateCommitError> {
529        self.vc_emulation_internal(crypto, storage, epoch_id)
530    }
531
532    #[cfg(feature = "virtual-clients-draft")]
533    fn vc_emulation_internal<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
534        mut self,
535        crypto: &Crypto,
536        storage: &Storage,
537        epoch_id: EpochId,
538    ) -> Result<Self, CreateCommitError> {
539        let state: VcDerivationEpochState = storage
540            .vc_derivation_epoch_state(&epoch_id)
541            .map_err(|e| {
542                log::error!("vc: load derivation epoch state in vc_emulation failed: {e:?}");
543                CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
544            })?
545            .ok_or(VirtualClientsError::MissingDerivationEpochState)?;
546        let mut operation_tree: OperationSecretTree = storage
547            .vc_operation_tree(&epoch_id)
548            .map_err(|e| {
549                log::error!("vc: load operation tree in vc_emulation failed: {e:?}");
550                CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
551            })?
552            .ok_or(VirtualClientsError::MissingOperationTree)?;
553        let (emulation_leaf_index, epoch_encryption_key, emulation_ciphersuite) =
554            state.into_parts();
555
556        // Validate the leaf configuration before allocating a generation, so
557        // a deterministic precondition failure (the new leaf not declaring
558        // `AppDataDictionary` or not listing `VC_COMPONENT_ID`) does not burn
559        // an operation secret. Returns the resolved `AppDataDictionary`, which
560        // `build` reuses so the injection preserves the AppComponents entry
561        // across commits.
562        let own_leaf_index = self.group.borrow().own_leaf_index();
563        let is_external_commit = self.stage.external_commit_info.is_some();
564        let resolved_dictionary = check_vc_leaf_configuration(
565            &self.stage.leaf_node_parameters,
566            self.group.borrow(),
567            own_leaf_index,
568            is_external_commit,
569        )?;
570
571        // Update-path leaf-node derivations are the only operation type
572        // wired up so far. KeyPackage / Application will get their own
573        // allocation entry points when emitted. The operation context for
574        // LeafNode operations is the higher-level group's id.
575        let (generation, operation_secret) = operation_tree.next_operation_secret(
576            crypto,
577            emulation_ciphersuite,
578            &epoch_id,
579            emulation_leaf_index,
580            VirtualClientOperationType::LeafNode,
581            self.group.borrow().group_id().as_slice(),
582        )?;
583        // Persist the advanced tree right away, so the allocation can never
584        // be observed on the wire before it is persisted.
585        storage
586            .write_vc_operation_tree(&epoch_id, &operation_tree)
587            .map_err(|e| {
588                log::error!("vc: persist advanced operation tree failed: {e:?}");
589                CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
590            })?;
591
592        self.vc_loaded = Some(VcLoaded {
593            epoch_id,
594            emulation_leaf_index,
595            epoch_encryption_key,
596            emulation_ciphersuite,
597            generation,
598            operation_secret,
599            resolved_dictionary,
600        });
601        Ok(self)
602    }
603
604    /// Ask the emulation group to start a new derivation epoch with this
605    /// commit.
606    ///
607    /// When set, `build` makes sure the commit's virtual-clients Safe AAD item
608    /// carries a `new_derivation_epoch` action, creating the item if the
609    /// application staged none. Every member of the emulation group then
610    /// registers the epoch this commit moves the group into as a derivation
611    /// epoch when the commit is merged, and subsequent virtual-client
612    /// operations resolve to it.
613    ///
614    /// This is the application's cadence knob for post-compromise security of
615    /// the virtual client's secrets. Commits that change membership create a
616    /// derivation epoch on their own, so they do not need this.
617    ///
618    /// Like all actions, the marker applies relative to the commit's input
619    /// state. Operations that reference a derivation epoch keep using the
620    /// newest derivation epoch of that input state, including operations
621    /// carried by this very commit.
622    ///
623    /// The group has to be configured as an emulation group and its
624    /// GroupContext has to require Safe AAD framing. Otherwise `build` fails
625    /// with [`CreateCommitError::NewDerivationEpochOutsideEmulationGroup`] or
626    /// [`CreateCommitError::NewDerivationEpochWithoutSafeAad`].
627    #[cfg(feature = "virtual-clients-draft")]
628    pub fn derivation_epoch(mut self, derivation_epoch: bool) -> Self {
629        self.vc_new_derivation_epoch = derivation_epoch;
630        self
631    }
632
633    /// Loads the PSKs for the PskProposals marked for inclusion and moves on to the next phase.
634    pub fn load_psks<Storage: StorageProvider>(
635        self,
636        storage: &'a Storage,
637    ) -> Result<CommitBuilder<'a, LoadedPsks, G>, CreateCommitError> {
638        let psk_ids: Vec<_> = self
639            .stage
640            .own_proposals
641            .iter()
642            .chain(
643                self.group
644                    .borrow()
645                    .proposal_store()
646                    .proposals()
647                    .map(|queued_proposal| queued_proposal.proposal()),
648            )
649            .filter_map(|proposal| match proposal {
650                Proposal::PreSharedKey(psk_proposal) => Some(psk_proposal.clone().into_psk_id()),
651                _ => None,
652            })
653            .collect();
654
655        // Load the PSKs and make the PskIds owned.
656        let psks = load_psks(storage, &self.group.borrow().resumption_psk_store, &psk_ids)?
657            .into_iter()
658            .map(|(psk_id_ref, key)| (psk_id_ref.clone(), key))
659            .collect();
660
661        // Initialize GroupInfoConfig
662        let use_ratchet_tree_extension = self
663            .group
664            .borrow()
665            .configuration()
666            .use_ratchet_tree_extension;
667
668        let group_info_config = GroupInfoConfig {
669            use_ratchet_tree_extension,
670            create_group_info: use_ratchet_tree_extension,
671            other_extensions: vec![],
672        };
673
674        Ok(self
675            .map_stage(|stage| {
676                (
677                    (),
678                    LoadedPsks {
679                        own_proposals: stage.own_proposals,
680                        psks,
681                        force_self_update: stage.force_self_update,
682                        leaf_node_parameters: stage.leaf_node_parameters,
683                        consume_proposal_store: stage.consume_proposal_store,
684                        group_info_config,
685                        external_commit_info: stage.external_commit_info,
686                        #[cfg(feature = "extensions-draft")]
687                        app_data_dictionary_updates: None,
688                    },
689                )
690            })
691            .1)
692    }
693}
694
695impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, LoadedPsks, G> {
696    /// Sets whether or not a [`GroupInfo`] should be created when the commit is staged. Defaults to
697    /// the value of the [`MlsGroup`]s [`MlsGroupJoinConfig`].
698    pub fn create_group_info(mut self, create_group_info: bool) -> Self {
699        self.stage.group_info_config.create_group_info = create_group_info;
700        self
701    }
702
703    /// Sets whether the [`GroupInfo`] should contain the ratchet tree extension. If set to `true`,
704    /// enables the [`GroupInfo`] to be created when the commit is staged.
705    pub fn use_ratchet_tree_extension(mut self, use_ratchet_tree_extension: bool) -> Self {
706        if use_ratchet_tree_extension {
707            self.stage.group_info_config.create_group_info = true;
708        }
709        self.stage.group_info_config.use_ratchet_tree_extension = use_ratchet_tree_extension;
710        self
711    }
712
713    /// Add the provided [`Extension`]s to the [`GroupInfo`].
714    ///
715    ///  Returns an error if a  [`RatchetTreeExtension`] or [`ExternalPubExtension`] is added
716    ///  directly here.
717    pub fn create_group_info_with_extensions(
718        mut self,
719        extensions: impl IntoIterator<Item = Extension>,
720    ) -> Result<Self, InvalidExtensionError> {
721        self.stage.group_info_config.create_group_info = true;
722        self.stage.group_info_config.other_extensions = extensions
723            .into_iter()
724            .map(|extension| {
725                if extension.as_ratchet_tree_extension().is_ok()
726                    || extension.as_external_pub_extension().is_ok()
727                {
728                    Err(InvalidExtensionError::CannotAddDirectlyToGroupInfo)
729                } else {
730                    Ok(extension)
731                }
732            })
733            .collect::<Result<Vec<_>, _>>()?;
734
735        Ok(self)
736    }
737
738    /// Validates the inputs and builds the commit. The last argument `f` is a function that lets
739    /// the caller filter the proposals that are considered for inclusion. This provides a way for
740    /// the application to enforce custom policies in the creation of commits.
741    pub fn build<S: Signer>(
742        self,
743        rand: &impl OpenMlsRand,
744        crypto: &impl OpenMlsCrypto,
745        signer: &S,
746        f: impl FnMut(&QueuedProposal) -> bool,
747    ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
748        self.build_internal(rand, crypto, signer, None::<NewSignerBundle<'_, S>>, f)
749    }
750
751    /// Just like `build`, this function validates the inputs and builds the
752    /// commit. The last argument `f` is a function that lets the caller filter
753    /// the proposals that are considered for inclusion. This provides a way for
754    /// the application to enforce custom policies in the creation of commits.
755    ///
756    /// In contrast to `build`, this function can be used to create commits that
757    /// rotate the own leaf node's signature key. Supplying a new signer implies
758    /// a self-update: the commit always contains an UpdatePath that installs
759    /// the new signature key in the committer's leaf, even if no proposal
760    /// requires a path.
761    ///
762    /// The Commit message itself is signed with `old_signer`, because
763    /// receivers verify it against the committer's pre-commit leaf. GroupInfo
764    /// objects created for this commit are signed with the new signer,
765    /// matching the post-commit leaf.
766    ///
767    /// Returns an error if the new signer's signature scheme does not match the
768    /// group's ciphersuite, or when used on an external commit. External commits
769    /// take their credential and signer from the external commit builder.
770    pub fn build_with_new_signer<S: Signer>(
771        self,
772        rand: &impl OpenMlsRand,
773        crypto: &impl OpenMlsCrypto,
774        old_signer: &impl Signer,
775        new_signer: NewSignerBundle<'_, S>,
776        f: impl FnMut(&QueuedProposal) -> bool,
777    ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
778        // On an external commit, the signer passed to the external commit
779        // builder signs the Commit, the UpdatePath leaf and the GroupInfo, so
780        // a new signer cannot be used.
781        if self.stage.external_commit_info.is_some() {
782            return Err(CreateCommitError::ExternalCommitWithNewSigner);
783        }
784        self.build_internal(rand, crypto, old_signer, Some(new_signer), f)
785    }
786
787    fn build_internal<S: Signer>(
788        self,
789        rand: &impl OpenMlsRand,
790        crypto: &impl OpenMlsCrypto,
791        old_signer: &impl Signer,
792        new_signer: Option<NewSignerBundle<'_, S>>,
793        f: impl FnMut(&QueuedProposal) -> bool,
794    ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
795        #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
796        let (mut cur_stage, mut builder) = self.take_stage();
797
798        // retrieve the config
799        let GroupInfoConfig {
800            create_group_info,
801            use_ratchet_tree_extension,
802            other_extensions,
803        } = cur_stage.group_info_config;
804
805        // Stage the marker before any proposal validation or path computation,
806        // so a misconfigured group is rejected before an operation generation is
807        // burned. The staged Safe AAD is what gets serialized into the commit's
808        // `authenticated_data` further down.
809        #[cfg(feature = "virtual-clients-draft")]
810        if builder.vc_new_derivation_epoch {
811            stage_vc_new_derivation_epoch(builder.group.borrow_mut())?;
812        }
813
814        let group = builder.group.borrow();
815
816        // The staged Safe AAD is authoritative for whether this commit creates a
817        // derivation epoch, so an application that staged the marker itself gets
818        // the same result as one that called `derivation_epoch`.
819        #[cfg(feature = "virtual-clients-draft")]
820        let marks_new_vc_derivation_epoch = staged_vc_commit_data(group)?
821            .is_some_and(|commit_data| commit_data.creates_derivation_epoch());
822        let ciphersuite = group.ciphersuite();
823        let own_leaf_index = group.own_leaf_index();
824        let (sender, is_external_commit) = match cur_stage.external_commit_info {
825            None => (Sender::build_member(own_leaf_index), false),
826            Some(_) => (Sender::NewMemberCommit, true),
827        };
828        let psks = cur_stage.psks;
829
830        // An external commit has exactly one authoritative credential and
831        // signature key: the ones passed to the external commit builder. Leaf
832        // node parameters that pin a different credential are rejected, and
833        // the authoritative credential is folded into the parameters here so
834        // the rest of the commit flow only sees a single value.
835        if let Some(ExternalCommitInfo { credential, .. }) = &cur_stage.external_commit_info {
836            if let Some(params_credential) = cur_stage.leaf_node_parameters.credential_with_key() {
837                if params_credential != credential {
838                    return Err(CreateCommitError::ExternalCommitCredentialMismatch);
839                }
840            }
841            cur_stage
842                .leaf_node_parameters
843                .set_credential_with_key(credential.clone());
844        }
845
846        // Fold the new signer's credential into the leaf node parameters. The
847        // new signature key is installed in the committer's leaf through the
848        // UpdatePath, so parameters that pin a different credential are
849        // rejected.
850        let new_signer = match new_signer {
851            Some(NewSignerBundle {
852                signer,
853                credential_with_key,
854            }) => {
855                if ciphersuite.signature_algorithm() != signer.signature_scheme() {
856                    return Err(CreateCommitError::InvalidSignerCiphersuite);
857                }
858                if let Some(params_credential) =
859                    cur_stage.leaf_node_parameters.credential_with_key()
860                {
861                    if params_credential != &credential_with_key {
862                        return Err(CreateCommitError::InvalidLeafNodeParameters);
863                    }
864                }
865                cur_stage
866                    .leaf_node_parameters
867                    .set_credential_with_key(credential_with_key);
868                Some(signer)
869            }
870            None => None,
871        };
872
873        // put the pending and uniform proposals into a uniform shape,
874        // i.e. produce queued proposals from the own proposals
875        let own_proposals: Vec<_> = cur_stage
876            .own_proposals
877            .into_iter()
878            .map(|proposal| {
879                QueuedProposal::from_proposal_and_sender(ciphersuite, crypto, proposal, &sender)
880            })
881            .collect::<Result<_, _>>()?;
882
883        // prepare an iterator for the proposals in the group's proposal store, but only if the
884        // flag is set.
885        let group_proposal_store_queue = group
886            .pending_proposals()
887            .filter(|_| cur_stage.consume_proposal_store)
888            .cloned();
889
890        // prepare the iterator for the proposal validation and selection function. That function
891        // assumes that "earlier in the list" means "older", so since our own proposals are
892        // newest, we have to put them last.
893        let proposal_queue = group_proposal_store_queue.chain(own_proposals).filter(f);
894
895        let (proposal_queue, contains_own_updates) =
896            ProposalQueue::filter_proposals(proposal_queue, group.own_leaf_index).map_err(|e| {
897                match e {
898                    ProposalQueueError::LibraryError(e) => e.into(),
899                    ProposalQueueError::ProposalNotFound => CreateCommitError::MissingProposal,
900                    ProposalQueueError::UpdateFromExternalSender
901                    | ProposalQueueError::SelfRemoveFromNonMember => {
902                        CreateCommitError::WrongProposalSenderType
903                    }
904                }
905            })?;
906
907        // Validate the proposals by doing the following checks:
908
909        // ValSem113: All Proposals: The proposal type must be supported by all
910        // members of the group
911        group
912            .public_group
913            .validate_proposal_type_support(&proposal_queue)?;
914        // ValSem101
915        // ValSem102
916        // ValSem103
917        // ValSem104
918        let path_leaf_signature_key = cur_stage
919            .leaf_node_parameters
920            .credential_with_key()
921            .map(|credential_with_key| &credential_with_key.signature_key);
922        group.public_group.validate_key_uniqueness(
923            &proposal_queue,
924            None,
925            &sender,
926            path_leaf_signature_key,
927        )?;
928        // ValSem105
929        group.public_group.validate_add_proposals(&proposal_queue)?;
930        // ValSem106
931        // ValSem109
932        group.public_group.validate_capabilities(&proposal_queue)?;
933        // ValSem107
934        // ValSem108
935        group
936            .public_group
937            .validate_remove_proposals(&proposal_queue)?;
938        // Also validates branch PSK proposals: a resumption PSK of usage `Branch`
939        // is only accepted at epoch 0 (i.e. in the initial commit of a subgroup),
940        // see `validate_pre_shared_key_proposals`.
941        group
942            .public_group
943            .validate_pre_shared_key_proposals(&proposal_queue)?;
944        // Validate update proposals for member commits
945        // ValSem110
946        // ValSem111
947        // ValSem112
948        group
949            .public_group
950            .validate_update_proposals(&proposal_queue, own_leaf_index)?;
951
952        // ValSem208
953        // ValSem209
954        group
955            .public_group
956            .validate_group_context_extensions_proposal(&proposal_queue)?;
957
958        #[cfg(feature = "extensions-draft")]
959        group
960            .public_group
961            .validate_app_data_update_proposals_and_group_context(&proposal_queue)?;
962
963        if is_external_commit {
964            group
965                .public_group
966                .validate_external_commit(&proposal_queue)?;
967        }
968
969        let proposal_reference_list = proposal_queue.commit_list();
970
971        // Make a copy of the public group to apply proposals safely
972        let mut diff = group.public_group.empty_diff();
973
974        // Apply proposals to tree
975        #[cfg(feature = "extensions-draft")]
976        let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
977            &proposal_queue,
978            own_leaf_index,
979            cur_stage.app_data_dictionary_updates,
980        )?;
981        #[cfg(not(feature = "extensions-draft"))]
982        let apply_proposals_values = diff.apply_proposals(&proposal_queue, own_leaf_index)?;
983        if apply_proposals_values.self_removed && !is_external_commit {
984            return Err(CreateCommitError::CannotRemoveSelf);
985        }
986
987        // Virtual-clients sender hook: when the caller opted into VC for
988        // this commit, validate that the effective leaf is configured to
989        // accept the derivation-info entry (capabilities + AppComponents),
990        // then derive the path-secret + leaf-keypair override from the
991        // operation secret allocated in `vc_emulation` and embed the
992        // `DerivationInfo` blob in the leaf's `app_data_dictionary`
993        // extension.
994        #[cfg(feature = "virtual-clients-draft")]
995        let vc_loaded = builder.vc_loaded.take();
996        #[cfg(feature = "virtual-clients-draft")]
997        let own_update_override = if let Some(loaded) = vc_loaded.as_ref() {
998            // The leaf-configuration pre-check already ran in `vc_emulation`,
999            // before the generation was allocated. Reuse the resolved
1000            // `AppDataDictionary` it produced so the inject step preserves
1001            // every other entry, including the AppComponents entry that
1002            // survives across multiple VC commits.
1003            // For an external commit, carry the external init secret in the
1004            // derivation info so a sibling emulator client can process the
1005            // commit without holding the previous epoch's `external_secret`.
1006            // Regular commits carry no external init secret.
1007            let external_init_secret =
1008                is_external_commit.then(|| group.group_epoch_secrets().init_secret());
1009            Some(apply_vc_emulation(
1010                loaded,
1011                &mut cur_stage.leaf_node_parameters,
1012                loaded.resolved_dictionary.clone(),
1013                crypto,
1014                ciphersuite,
1015                group.group_id(),
1016                external_init_secret,
1017            )?)
1018        } else {
1019            None
1020        };
1021        #[cfg(not(feature = "virtual-clients-draft"))]
1022        let own_update_override: Option<crate::treesync::diff::OwnUpdatePathOverride> = None;
1023
1024        // A new signer always requires a path: the new signature key only
1025        // becomes part of the group state through the UpdatePath leaf.
1026        let path_computation_result =
1027            // If path is needed, compute path values
1028            if apply_proposals_values.path_required
1029                || contains_own_updates
1030                || cur_stage.force_self_update
1031                || !cur_stage.leaf_node_parameters.is_empty()
1032                || new_signer.is_some()
1033            {
1034                let commit_type = if is_external_commit {
1035                    CommitType::External
1036                } else {
1037                    CommitType::Member
1038                };
1039                // Process the path. This includes updating the provisional
1040                // group context by updating the epoch and computing the new
1041                // tree hash.
1042                match new_signer {
1043                    Some(new_signer) => diff.compute_path(
1044                        rand,
1045                        crypto,
1046                        own_leaf_index,
1047                        apply_proposals_values.exclusion_list(),
1048                        &commit_type,
1049                        &cur_stage.leaf_node_parameters,
1050                        new_signer,
1051                        apply_proposals_values.extensions.clone(),
1052                        own_update_override,
1053                    )?,
1054                    None => diff.compute_path(
1055                        rand,
1056                        crypto,
1057                        own_leaf_index,
1058                        apply_proposals_values.exclusion_list(),
1059                        &commit_type,
1060                        &cur_stage.leaf_node_parameters,
1061                        old_signer,
1062                        apply_proposals_values.extensions.clone(),
1063                        own_update_override,
1064                    )?,
1065                }
1066            } else {
1067                // If path is not needed, update the group context and return
1068                // empty path processing results
1069                diff.update_group_context(crypto, apply_proposals_values.extensions.clone())?;
1070                PathComputationResult::default()
1071            };
1072
1073        let update_path_leaf_node = path_computation_result
1074            .encrypted_path
1075            .as_ref()
1076            .map(|path| path.leaf_node().clone());
1077
1078        // Validate that the update path leaf node's capabilities
1079        if let Some(ref leaf_node) = update_path_leaf_node {
1080            // Check that all extension types in the group context that are valid in leaf nodes
1081            // are supported by the leaf node
1082            //
1083            // This is currently not required by the RFC, likely by mistake:
1084            // https://mailarchive.ietf.org/arch/msg/mls/k18P4FP7dfS2cBmP0kL6Uh50-ok/
1085            if !diff
1086                .group_context()
1087                .extensions()
1088                .iter()
1089                .map(Extension::extension_type)
1090                .all(|ext_type| leaf_node.supports_extension(&ext_type))
1091            {
1092                return Err(CreateCommitError::LeafNodeValidation(
1093                    LeafNodeValidationError::UnsupportedExtensions,
1094                ));
1095            }
1096
1097            // Check that the leaf node supports everything listed in the required capabilities.
1098            // https://validation.openmls.tech/#valn0103
1099            if let Some(required_capabilities) =
1100                diff.group_context().extensions().required_capabilities()
1101            {
1102                leaf_node
1103                    .capabilities()
1104                    .supports_required_capabilities(required_capabilities)?
1105            }
1106        }
1107
1108        // Create commit message
1109        let commit = Commit {
1110            proposals: proposal_reference_list,
1111            path: path_computation_result.encrypted_path,
1112        };
1113
1114        let (outgoing_aad, wire_format): (Vec<u8>, WireFormat) =
1115            match &cur_stage.external_commit_info {
1116                None => (
1117                    group.outgoing_authenticated_data()?,
1118                    group.outgoing_wire_format(),
1119                ),
1120                Some(ExternalCommitInfo { aad, .. }) => {
1121                    // The spec requires the SafeAAD prefix even with zero items
1122                    // when the target GroupContext has `safe_aad` present, so a
1123                    // bare `aad` would be rejected by SafeAAD-aware receivers.
1124                    // The joining group carries no application-staged items, so
1125                    // the prefix is empty unless the builder staged the
1126                    // virtual-clients marker.
1127                    #[cfg(feature = "extensions-draft")]
1128                    let aad_bytes = if group.context().safe_aad_required() {
1129                        crate::framing::safe_aad::assemble_authenticated_data(&group.safe_aad, aad)
1130                            .map_err(|_| LibraryError::custom("SafeAad serialization failed"))?
1131                    } else {
1132                        aad.clone()
1133                    };
1134                    #[cfg(not(feature = "extensions-draft"))]
1135                    let aad_bytes = aad.clone();
1136                    (aad_bytes, WireFormat::PublicMessage)
1137                }
1138            };
1139
1140        let framing_parameters = FramingParameters::new(&outgoing_aad, wire_format);
1141
1142        // Build AuthenticatedContent
1143        let mut authenticated_content = AuthenticatedContent::commit(
1144            framing_parameters,
1145            sender,
1146            commit,
1147            group.public_group.group_context(),
1148            old_signer,
1149        )?;
1150
1151        // Update the confirmed transcript hash using the commit we just created.
1152        diff.update_confirmed_transcript_hash(crypto, &authenticated_content)?;
1153
1154        let serialized_provisional_group_context = diff
1155            .group_context()
1156            .tls_serialize_detached()
1157            .map_err(LibraryError::missing_bound_check)?;
1158
1159        let joiner_secret = JoinerSecret::new(
1160            crypto,
1161            ciphersuite,
1162            path_computation_result.commit_secret,
1163            group.group_epoch_secrets().init_secret(),
1164            &serialized_provisional_group_context,
1165        )
1166        .map_err(LibraryError::unexpected_crypto_error)?;
1167
1168        // Prepare the PskSecret
1169        let psk_secret = PskSecret::new(crypto, ciphersuite, psks)?;
1170
1171        // Create key schedule
1172        let mut key_schedule = KeySchedule::init(ciphersuite, crypto, &joiner_secret, psk_secret)?;
1173
1174        let serialized_provisional_group_context = diff
1175            .group_context()
1176            .tls_serialize_detached()
1177            .map_err(LibraryError::missing_bound_check)?;
1178
1179        let welcome_secret = key_schedule
1180            .welcome(crypto, ciphersuite)
1181            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
1182        key_schedule
1183            .add_context(crypto, &serialized_provisional_group_context)
1184            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
1185        let EpochSecretsResult {
1186            epoch_secrets: provisional_epoch_secrets,
1187            #[cfg(feature = "extensions-draft")]
1188            application_exporter,
1189        } = key_schedule
1190            .epoch_secrets(crypto, ciphersuite)
1191            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
1192
1193        // Calculate the confirmation tag
1194        let confirmation_tag = provisional_epoch_secrets
1195            .confirmation_key()
1196            .tag(
1197                crypto,
1198                ciphersuite,
1199                diff.group_context().confirmed_transcript_hash(),
1200            )
1201            .map_err(LibraryError::unexpected_crypto_error)?;
1202
1203        // Set the confirmation tag
1204        authenticated_content.set_confirmation_tag(confirmation_tag.clone());
1205
1206        diff.update_interim_transcript_hash(ciphersuite, crypto, confirmation_tag.clone())?;
1207
1208        // If there are invitations, we need to build a welcome
1209        let needs_welcome = !apply_proposals_values.invitation_list.is_empty();
1210
1211        // We need a GroupInfo if we need to build a Welcome, or if
1212        // `create_group_info` is set to `true`. If not overridden, `create_group_info`
1213        // is set to the `use_ratchet_tree` flag in the group configuration.
1214        let needs_group_info = needs_welcome || create_group_info;
1215
1216        let (welcome_option, group_info) = if !needs_group_info {
1217            (None, None)
1218        } else {
1219            // Create the ratchet tree extension if necessary
1220            let mut extensions_list = vec![];
1221            if use_ratchet_tree_extension {
1222                extensions_list.push(Extension::RatchetTree(RatchetTreeExtension::new(
1223                    diff.export_ratchet_tree(),
1224                )));
1225            };
1226            // Append rest of extensions
1227            extensions_list.extend(other_extensions);
1228
1229            let mut extensions = Extensions::from_vec(extensions_list)?;
1230
1231            let welcome_option = needs_welcome
1232                .then(|| -> Result<_, CreateCommitError> {
1233                    let group_info_tbs = {
1234                        GroupInfoTBS::new(
1235                            diff.group_context().clone(),
1236                            extensions.clone(),
1237                            confirmation_tag.clone(),
1238                            own_leaf_index,
1239                        )?
1240                    };
1241                    // Sign to-be-signed group info. Joiners verify this against
1242                    // the own leaf node in the post-commit ratchet tree, so a
1243                    // rotated signature key has to be used here as well.
1244                    let group_info = match new_signer {
1245                        Some(new_signer) => group_info_tbs.sign(new_signer)?,
1246                        None => group_info_tbs.sign(old_signer)?,
1247                    };
1248
1249                    // Encrypt GroupInfo object
1250                    let (welcome_key, welcome_nonce) = welcome_secret
1251                        .derive_welcome_key_nonce(crypto, ciphersuite)
1252                        .map_err(LibraryError::unexpected_crypto_error)?;
1253                    let encrypted_group_info = welcome_key
1254                        .aead_seal(
1255                            crypto,
1256                            group_info
1257                                .tls_serialize_detached()
1258                                .map_err(LibraryError::missing_bound_check)?
1259                                .as_slice(),
1260                            &[],
1261                            &welcome_nonce,
1262                        )
1263                        .map_err(LibraryError::unexpected_crypto_error)?;
1264
1265                    // Create group secrets for later use, so we can afterwards consume the
1266                    // `joiner_secret`.
1267                    let encrypted_secrets = diff.encrypt_group_secrets(
1268                        &joiner_secret,
1269                        apply_proposals_values.invitation_list,
1270                        path_computation_result.plain_path.as_deref(),
1271                        &apply_proposals_values.presharedkeys,
1272                        &encrypted_group_info,
1273                        crypto,
1274                        own_leaf_index,
1275                    )?;
1276
1277                    // Create welcome message
1278                    let welcome =
1279                        Welcome::new(ciphersuite, encrypted_secrets, encrypted_group_info);
1280                    Ok(welcome)
1281                })
1282                .transpose()?;
1283
1284            // Create the GroupInfo for export if needed. In contrast to the Welcome, this
1285            // group info contains the external public key extension.
1286            let exported_group_info = create_group_info
1287                .then(|| -> Result<_, CreateCommitError> {
1288                    let external_pub = provisional_epoch_secrets
1289                        .external_secret()
1290                        .derive_external_keypair(crypto, ciphersuite)
1291                        .map_err(LibraryError::unexpected_crypto_error)?
1292                        .public;
1293
1294                    let external_pub_extension =
1295                        Extension::ExternalPub(ExternalPubExtension::new(external_pub.into()));
1296                    extensions.add(external_pub_extension)?;
1297                    let group_info_tbs = {
1298                        GroupInfoTBS::new(
1299                            diff.group_context().clone(),
1300                            extensions,
1301                            confirmation_tag.clone(),
1302                            own_leaf_index,
1303                        )?
1304                    };
1305                    // Sign to-be-signed group info. Like the Welcome's
1306                    // GroupInfo, this is verified against the post-commit
1307                    // ratchet tree, so a rotated signature key has to be used
1308                    // here as well.
1309                    match new_signer {
1310                        Some(new_signer) => Ok(group_info_tbs.sign(new_signer)?),
1311                        None => Ok(group_info_tbs.sign(old_signer)?),
1312                    }
1313                })
1314                .transpose()?;
1315
1316            (welcome_option, exported_group_info)
1317        };
1318
1319        let (provisional_group_epoch_secrets, provisional_message_secrets) =
1320            provisional_epoch_secrets.split_secrets(
1321                serialized_provisional_group_context,
1322                diff.tree_size(),
1323                own_leaf_index,
1324            );
1325
1326        #[cfg(feature = "extensions-draft")]
1327        let application_export_tree = ApplicationExportTree::new(application_exporter);
1328        let staged_commit_state = MemberStagedCommitState::new(
1329            provisional_group_epoch_secrets,
1330            provisional_message_secrets,
1331            diff.into_staged_diff(crypto, ciphersuite)?,
1332            path_computation_result.new_keypairs,
1333            // The committer is not allowed to include their own update
1334            // proposal, so there is no extra keypair to store here.
1335            None,
1336            update_path_leaf_node,
1337            #[cfg(feature = "extensions-draft")]
1338            application_export_tree,
1339            // The committer's `own_leaf_index` is already set to the new
1340            // leaf (in `build_group` for external commits, or unchanged for
1341            // regular commits), so `merge_commit` has nothing to overwrite.
1342            #[cfg(feature = "virtual-clients-draft")]
1343            None,
1344        );
1345        #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
1346        let mut staged_commit = StagedCommit::new(
1347            proposal_queue,
1348            StagedCommitState::GroupMember(Box::new(staged_commit_state)),
1349            #[cfg(feature = "virtual-clients-draft")]
1350            vc_loaded.as_ref().map(|loaded| loaded.epoch_id.clone()),
1351        );
1352        #[cfg(feature = "virtual-clients-draft")]
1353        {
1354            staged_commit.marks_new_vc_derivation_epoch = marks_new_vc_derivation_epoch;
1355        }
1356
1357        Ok(builder.into_stage(Complete {
1358            result: CreateCommitResult {
1359                commit: authenticated_content,
1360                welcome_option,
1361                staged_commit,
1362                group_info: group_info.filter(|_| create_group_info),
1363            },
1364            original_wire_format_policy: cur_stage
1365                .external_commit_info
1366                .as_ref()
1367                .map(|info| info.wire_format_policy),
1368        }))
1369    }
1370
1371    /// Creates a new [`AppDataUpdates`] based on the current state of the
1372    /// [`AppDataDictionary`] of the group.
1373    ///
1374    /// [`AppDataDictionary`]: crate::extensions::AppDataDictionary
1375    #[cfg(feature = "extensions-draft")]
1376    pub fn app_data_dictionary_updater(&self) -> AppDataDictionaryUpdater<'_> {
1377        AppDataDictionaryUpdater::new(self.group.borrow().context().app_data_dict())
1378    }
1379
1380    /// Sets the [`AppDataUpdates`] that contain the changes made by the AppDataUpdate proposals
1381    #[cfg(feature = "extensions-draft")]
1382    pub fn with_app_data_dictionary_updates(
1383        &mut self,
1384        app_data_dictionary_updates: Option<AppDataUpdates>,
1385    ) {
1386        self.stage.app_data_dictionary_updates = app_data_dictionary_updates;
1387    }
1388
1389    /// Returns an iterator over all AppDataUpdate proposals in the proposal store of the group
1390    #[cfg(feature = "extensions-draft")]
1391    pub fn app_data_update_proposals(&self) -> impl Iterator<Item = &AppDataUpdateProposal> {
1392        let proposal_store_proposals = self
1393            .group
1394            .borrow()
1395            .proposal_store()
1396            .proposals()
1397            .map(|queued_proposal| queued_proposal.proposal());
1398
1399        // The proposals in the proposal store come earlier than the own_proposals.
1400        let all_proposals = proposal_store_proposals.chain(self.stage.own_proposals.iter());
1401
1402        // Filter for AppDataUpdate proposals
1403        let mut app_data_update_proposals: Vec<&AppDataUpdateProposal> = all_proposals
1404            .filter_map(|proposal| match proposal {
1405                Proposal::AppDataUpdate(proposal) => Some(proposal.as_ref()),
1406                _ => None,
1407            })
1408            .collect();
1409
1410        app_data_update_proposals.sort_by_key(|prop| prop.component_id());
1411        app_data_update_proposals.into_iter()
1412    }
1413}
1414
1415// Impls that apply only to regular commits.
1416impl CommitBuilder<'_, Complete, &mut MlsGroup> {
1417    #[cfg(test)]
1418    pub(crate) fn commit_result(self) -> CreateCommitResult {
1419        self.stage.result
1420    }
1421
1422    /// Stages the commit and returns the protocol messages.
1423    pub fn stage_commit<Provider: OpenMlsProvider>(
1424        self,
1425        provider: &Provider,
1426    ) -> Result<CommitMessageBundle, CommitBuilderStageError<Provider::StorageError>> {
1427        let Self {
1428            group,
1429            stage:
1430                Complete {
1431                    result: create_commit_result,
1432                    original_wire_format_policy: _,
1433                },
1434            ..
1435        } = self;
1436
1437        // Set the current group state to [`MlsGroupState::PendingCommit`],
1438        // storing the current [`StagedCommit`] from the commit results
1439        group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
1440            create_commit_result.staged_commit,
1441        )));
1442
1443        provider
1444            .storage()
1445            .write_group_state(group.group_id(), &group.group_state)
1446            .map_err(CommitBuilderStageError::KeyStoreError)?;
1447
1448        group.reset_aad();
1449
1450        // Convert PublicMessage messages to MLSMessage and encrypt them if required by the
1451        // configuration.
1452        //
1453        // Note that this performs writes to the storage, so we should do that here, rather than
1454        // when working with the result.
1455        let framing = group.content_to_mls_message(create_commit_result.commit, provider)?;
1456
1457        Ok(CommitMessageBundle {
1458            version: group.version(),
1459            commit: framing.message,
1460            welcome: create_commit_result.welcome_option,
1461            group_info: create_commit_result.group_info,
1462            #[cfg(feature = "virtual-clients-draft")]
1463            confirmation: framing.confirmation,
1464        })
1465    }
1466}
1467
1468/// The virtual-clients commit data staged for `group`'s next outgoing message.
1469///
1470/// `Ok(None)` when the group does not act on the item at all, that is when it is
1471/// not an emulation group or its GroupContext does not require Safe AAD framing,
1472/// and when no item is staged.
1473#[cfg(feature = "virtual-clients-draft")]
1474fn staged_vc_commit_data(
1475    group: &MlsGroup,
1476) -> Result<Option<VirtualClientCommitData>, CreateCommitError> {
1477    if !group.is_emulation_group() || !group.context().safe_aad_required() {
1478        return Ok(None);
1479    }
1480    Ok(VirtualClientCommitData::from_safe_aad(&group.safe_aad)?)
1481}
1482
1483/// Add a `new_derivation_epoch` action to the virtual-clients Safe AAD item
1484/// staged on `group`, creating the item if the application staged none.
1485///
1486/// Every other entry of an application-staged item is preserved. Fails if the
1487/// group cannot carry the marker, either because it is not an emulation group or
1488/// because its GroupContext does not require Safe AAD framing.
1489#[cfg(feature = "virtual-clients-draft")]
1490fn stage_vc_new_derivation_epoch(group: &mut MlsGroup) -> Result<(), CreateCommitError> {
1491    if !group.is_emulation_group() {
1492        return Err(CreateCommitError::NewDerivationEpochOutsideEmulationGroup);
1493    }
1494    if !group.context().safe_aad_required() {
1495        return Err(CreateCommitError::NewDerivationEpochWithoutSafeAad);
1496    }
1497
1498    let mut commit_data = staged_vc_commit_data(group)?
1499        .map_or_else(|| VirtualClientCommitData::new(Vec::new()), Ok)?;
1500    commit_data.require_new_derivation_epoch();
1501    group.safe_aad.upsert(commit_data.to_safe_aad_item()?);
1502    Ok(())
1503}
1504
1505/// Build the path-secret + leaf-keypair override from the
1506/// [`OperationSecret`] allocated in [`CommitBuilder::vc_emulation`] and
1507/// inject the corresponding `DerivationInfo` blob into
1508/// `leaf_node_parameters`'s `app_data_dictionary` extension.
1509///
1510/// The `DerivationInfoTbe` wrapping stays in the derivation epoch's
1511/// ciphersuite, while the operation secret is imported into the
1512/// higher-level group ciphersuite to produce MLS path material for this
1513/// group. The generation was consumed and the advanced tree persisted when
1514/// `vc_emulation` was called, so this helper neither allocates nor
1515/// persists anything.
1516#[cfg(feature = "virtual-clients-draft")]
1517fn apply_vc_emulation(
1518    loaded: &VcLoaded,
1519    leaf_node_parameters: &mut LeafNodeParameters,
1520    resolved_dictionary: AppDataDictionary,
1521    crypto: &impl OpenMlsCrypto,
1522    group_ciphersuite: openmls_traits::types::Ciphersuite,
1523    group_id: &crate::prelude::GroupId,
1524    external_init_secret: Option<&crate::schedule::InitSecret>,
1525) -> Result<crate::treesync::diff::OwnUpdatePathOverride, CreateCommitError> {
1526    let target_operation_secret = loaded.operation_secret.derive_target_operation_secret(
1527        crypto,
1528        group_ciphersuite,
1529        group_id,
1530    )?;
1531    let path_secret = target_operation_secret
1532        .derive_path_generation_secret(crypto, group_ciphersuite)?
1533        .into();
1534    let leaf_encryption_keypair = target_operation_secret
1535        .derive_encryption_key_secret(crypto, group_ciphersuite)?
1536        .generate_encryption_key_pair(crypto, group_ciphersuite)?;
1537    drop(target_operation_secret);
1538
1539    // Wrap the TBE under the per-epoch AEAD key, bound to the new leaf via
1540    // its serialized encryption key as derivation context.
1541    let leaf_encryption_key = leaf_encryption_keypair
1542        .public_key()
1543        .tls_serialize_detached()
1544        .map_err(VirtualClientsError::from)?;
1545    // leaf_node operations are not batched, so the TBE carries no
1546    // key_package_index. For an external commit the commit-case
1547    // `external_init_secret` is present; for a regular commit it is absent.
1548    let tbe = DerivationInfoTbe::LeafNode {
1549        leaf_index: loaded.emulation_leaf_index,
1550        generation: loaded.generation,
1551        external_init_secret: external_init_secret
1552            .map(|init_secret| ExternalInitSecret::from_slice(init_secret.as_slice())),
1553    };
1554    let derivation_info = DerivationInfo::encrypt(
1555        crypto,
1556        loaded.emulation_ciphersuite,
1557        &loaded.epoch_encryption_key,
1558        loaded.epoch_id.clone(),
1559        &leaf_encryption_key,
1560        &tbe,
1561    )?;
1562    let derivation_info_bytes = derivation_info
1563        .tls_serialize_detached()
1564        .map_err(VirtualClientsError::from)?;
1565
1566    inject_vc_derivation_info(
1567        leaf_node_parameters,
1568        resolved_dictionary,
1569        derivation_info_bytes,
1570    )?;
1571
1572    Ok(crate::treesync::diff::OwnUpdatePathOverride {
1573        path_secret,
1574        leaf_encryption_keypair,
1575    })
1576}
1577
1578/// Verify that the effective leaf for this commit (= the merged view of
1579/// `leaf_node_parameters` over the existing leaf, or `leaf_node_parameters`
1580/// alone for external commits) declares `AppDataDictionary` and lists
1581/// [`VC_COMPONENT_ID`] in its `AppComponents` entry. Without both, the
1582/// receiver cannot reliably surface the derivation-info entry to the
1583/// virtual-clients consumer, so we reject the commit at build time.
1584///
1585/// Returns the resolved `AppDataDictionary` (caller's override merged
1586/// over the existing leaf's, with the caller winning on duplicate keys)
1587/// so subsequent injection of the VC derivation-info preserves the
1588/// AppComponents entry across commits.
1589#[cfg(feature = "virtual-clients-draft")]
1590fn check_vc_leaf_configuration(
1591    leaf_node_parameters: &LeafNodeParameters,
1592    group: &MlsGroup,
1593    own_leaf_index: LeafNodeIndex,
1594    is_external_commit: bool,
1595) -> Result<AppDataDictionary, CreateCommitError> {
1596    let current_leaf = if is_external_commit {
1597        None
1598    } else {
1599        Some(group.public_group().leaf(own_leaf_index).ok_or_else(|| {
1600            LibraryError::custom("Couldn't find own leaf for VC capability check")
1601        })?)
1602    };
1603
1604    crate::components::vc_derivation_info::resolve_vc_leaf_dictionary(
1605        leaf_node_parameters.capabilities(),
1606        leaf_node_parameters.extensions(),
1607        current_leaf,
1608    )
1609    .map_err(CreateCommitError::VirtualClientsError)
1610}
1611
1612/// Merge a virtual-clients derivation info blob into
1613/// `leaf_node_parameters.app_data_dictionary[VC_COMPONENT_ID]`,
1614/// preserving every other component id from `resolved_dictionary` and
1615/// every non-`AppDataDictionary` leaf-node extension the caller put in.
1616#[cfg(feature = "virtual-clients-draft")]
1617fn inject_vc_derivation_info(
1618    leaf_node_parameters: &mut LeafNodeParameters,
1619    resolved_dictionary: AppDataDictionary,
1620    derivation_info_bytes: Vec<u8>,
1621) -> Result<(), CreateCommitError> {
1622    let extensions = crate::components::vc_derivation_info::merge_vc_derivation_info(
1623        leaf_node_parameters.extensions(),
1624        resolved_dictionary,
1625        derivation_info_bytes,
1626    )?;
1627    leaf_node_parameters.set_extensions(extensions);
1628    Ok(())
1629}
1630
1631/// Contains the messages that are produced by committing. The messages can be accessed individually
1632/// using getters or through the [`IntoIterator`] interface.
1633#[derive(Debug, Clone)]
1634pub struct CommitMessageBundle {
1635    version: ProtocolVersion,
1636    commit: MlsMessageOut,
1637    welcome: Option<Welcome>,
1638    group_info: Option<GroupInfo>,
1639    /// Confirmation data for a commit framed as a PrivateMessage, `None` for a
1640    /// plaintext-framed commit.
1641    #[cfg(feature = "virtual-clients-draft")]
1642    confirmation: Option<HandshakeConfirmationData>,
1643}
1644
1645/// The result of a commit with an add proposal. This includes
1646/// - The Commit as an [`MlsMessageOut`]
1647/// - The [`Welcome`] as an [`MlsMessageOut`]
1648/// - Optionally a [`GroupInfo`] as an [`MlsMessageOut`]
1649pub struct WelcomeCommitMessages {
1650    /// The Commit as an [`MlsMessageOut`].
1651    pub commit: MlsMessageOut,
1652
1653    /// The [`Welcome`] as an [`MlsMessageOut`].
1654    pub welcome: MlsMessageOut,
1655
1656    /// Optionally a [`GroupInfo`] as an [`MlsMessageOut`].
1657    pub group_info: Option<MlsMessageOut>,
1658}
1659
1660impl TryFrom<CommitMessageBundle> for WelcomeCommitMessages {
1661    type Error = LibraryError;
1662
1663    fn try_from(value: CommitMessageBundle) -> Result<Self, Self::Error> {
1664        let (commit, welcome_opt, group_info) = value.into_messages();
1665        Ok(Self {
1666            commit,
1667            welcome: welcome_opt.ok_or_else(|| {
1668                LibraryError::custom(
1669                    "WelcomeCommitMessages must only be used with commits that produce a welcome.",
1670                )
1671            })?,
1672            group_info,
1673        })
1674    }
1675}
1676
1677#[cfg(test)]
1678impl CommitMessageBundle {
1679    pub fn new(
1680        version: ProtocolVersion,
1681        commit: MlsMessageOut,
1682        welcome: Option<Welcome>,
1683        group_info: Option<GroupInfo>,
1684    ) -> Self {
1685        Self {
1686            version,
1687            commit,
1688            welcome,
1689            group_info,
1690            #[cfg(feature = "virtual-clients-draft")]
1691            confirmation: None,
1692        }
1693    }
1694}
1695
1696impl CommitMessageBundle {
1697    // borrowed getters
1698
1699    /// Gets the Commit messsage. For owned version, see [`Self::into_commit`].
1700    pub fn commit(&self) -> &MlsMessageOut {
1701        &self.commit
1702    }
1703
1704    /// Gets the Welcome messsage. Only [`Some`] if new clients have been added in the commit.
1705    /// For owned version, see [`Self::into_welcome`].
1706    pub fn welcome(&self) -> Option<&Welcome> {
1707        self.welcome.as_ref()
1708    }
1709
1710    /// Gets the Welcome messsage. Only [`Some`] if new clients have been added in the commit.
1711    /// Performs a copy of the Welcome. For owned version, see [`Self::into_welcome_msg`].
1712    pub fn to_welcome_msg(&self) -> Option<MlsMessageOut> {
1713        self.welcome
1714            .as_ref()
1715            .map(|welcome| MlsMessageOut::from_welcome(welcome.clone(), self.version))
1716    }
1717
1718    /// Gets the GroupInfo message. Only [`Some`] if new clients have been added or the group
1719    /// configuration has `use_ratchet_tree_extension` set.
1720    /// For owned version, see [`Self::into_group_info`].
1721    pub fn group_info(&self) -> Option<&GroupInfo> {
1722        self.group_info.as_ref()
1723    }
1724
1725    /// Gets the confirmation data for this commit. Present when the commit was
1726    /// framed as a PrivateMessage, `None` when it was framed as a plaintext
1727    /// PublicMessage. Pass its `epoch` and `generation` to
1728    /// [`MlsGroup::confirm_handshake_message`] once the DS has accepted the
1729    /// commit. For an owning version, see [`Self::take_confirmation`].
1730    ///
1731    /// [`MlsGroup::confirm_handshake_message`]: crate::group::MlsGroup::confirm_handshake_message
1732    #[cfg(feature = "virtual-clients-draft")]
1733    pub fn confirmation(&self) -> Option<&HandshakeConfirmationData> {
1734        self.confirmation.as_ref()
1735    }
1736
1737    /// Takes the confirmation data out of the bundle, leaving `None` in its
1738    /// place. Call this before handing the bundle to a consuming accessor such
1739    /// as [`Self::into_commit`], [`Self::into_contents`], or
1740    /// [`Self::into_messages`], which drop the confirmation data. For a
1741    /// borrowed version, see [`Self::confirmation`].
1742    #[cfg(feature = "virtual-clients-draft")]
1743    pub fn take_confirmation(&mut self) -> Option<HandshakeConfirmationData> {
1744        self.confirmation.take()
1745    }
1746
1747    /// Gets all three messages, some of which optional. For owned version, see
1748    /// [`Self::into_contents`].
1749    pub fn contents(&self) -> (&MlsMessageOut, Option<&Welcome>, Option<&GroupInfo>) {
1750        (
1751            &self.commit,
1752            self.welcome.as_ref(),
1753            self.group_info.as_ref(),
1754        )
1755    }
1756
1757    // owned getters
1758    /// Gets the Commit messsage. This method consumes the [`CommitMessageBundle`]. For a borrowed
1759    /// version see [`Self::commit`].
1760    pub fn into_commit(self) -> MlsMessageOut {
1761        self.commit
1762    }
1763
1764    /// Gets the Welcome messsage. Only [`Some`] if new clients have been added in the commit.
1765    /// This method consumes the [`CommitMessageBundle`]. For a borrowed version see
1766    /// [`Self::welcome`].
1767    pub fn into_welcome(self) -> Option<Welcome> {
1768        self.welcome
1769    }
1770
1771    /// Gets the Welcome messsage. Only [`Some`] if new clients have been added in the commit.
1772    /// For a borrowed version, see [`Self::to_welcome_msg`].
1773    pub fn into_welcome_msg(self) -> Option<MlsMessageOut> {
1774        self.welcome
1775            .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version))
1776    }
1777
1778    /// Gets the GroupInfo message. Only [`Some`] if new clients have been added or the group
1779    /// configuration has `use_ratchet_tree_extension` set.
1780    /// This method consumes the [`CommitMessageBundle`]. For a borrowed version see
1781    /// [`Self::group_info`].
1782    pub fn into_group_info(self) -> Option<GroupInfo> {
1783        self.group_info
1784    }
1785
1786    /// Gets the GroupInfo messsage. Only [`Some`] if new clients have been added in the commit.
1787    pub fn into_group_info_msg(self) -> Option<MlsMessageOut> {
1788        self.group_info.map(|group_info| group_info.into())
1789    }
1790
1791    /// Gets all three messages, some of which optional. This method consumes the
1792    /// [`CommitMessageBundle`]. For a borrowed version see [`Self::contents`].
1793    pub fn into_contents(self) -> (MlsMessageOut, Option<Welcome>, Option<GroupInfo>) {
1794        (self.commit, self.welcome, self.group_info)
1795    }
1796
1797    /// Gets all three messages, some of which optional, as [`MlsMessageOut`].
1798    /// This method consumes the [`CommitMessageBundle`].
1799    pub fn into_messages(self) -> (MlsMessageOut, Option<MlsMessageOut>, Option<MlsMessageOut>) {
1800        (
1801            self.commit,
1802            self.welcome
1803                .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version)),
1804            self.group_info.map(|group_info| group_info.into()),
1805        )
1806    }
1807}
1808
1809impl IntoIterator for CommitMessageBundle {
1810    type Item = MlsMessageOut;
1811
1812    type IntoIter = core::iter::Chain<
1813        core::iter::Chain<
1814            core::option::IntoIter<MlsMessageOut>,
1815            core::option::IntoIter<MlsMessageOut>,
1816        >,
1817        core::option::IntoIter<MlsMessageOut>,
1818    >;
1819
1820    fn into_iter(self) -> Self::IntoIter {
1821        let welcome = self.to_welcome_msg();
1822        let group_info = self.group_info.map(|group_info| group_info.into());
1823
1824        Some(self.commit)
1825            .into_iter()
1826            .chain(welcome)
1827            .chain(group_info)
1828    }
1829}
1830
1831#[cfg(test)]
1832mod branch_tests {
1833    use crate::{
1834        group::{
1835            mls_group::tests_and_kats::utils::{setup_alice_bob_group, setup_client},
1836            CreateCommitError, MlsGroup, ProposalValidationError,
1837        },
1838        schedule::errors::PskError,
1839    };
1840
1841    /// A resumption PSK of usage `Branch` must only appear in the initial commit
1842    /// of a subgroup (i.e. at epoch 0). Using it in any later commit must be
1843    /// rejected.
1844    ///
1845    /// This exercises the internal [`CommitBuilder::branch`] engine directly, on
1846    /// an already-established group, which the public `MlsGroupBuilder::branch`
1847    /// entry point does not allow.
1848    #[openmls_test::openmls_test]
1849    fn subgroup_branch_psk_rejected_outside_initial_commit() {
1850        let alice_provider = &Provider::default();
1851        let bob_provider = &Provider::default();
1852        let parent_provider = &Provider::default();
1853
1854        // `alice_group` is at epoch 1 after adding Bob, so a branch PSK in a
1855        // commit on it must be rejected.
1856        let (mut alice_group, alice_signer, _bob_group, _bob_signer, _alice_cwk, _bob_cwk) =
1857            setup_alice_bob_group(ciphersuite, alice_provider, bob_provider);
1858
1859        // Use a separate group as the (arbitrary) source of the branch PSK
1860        // secret, so that `load_psks` succeeds and we actually reach the
1861        // proposal validation.
1862        let (parent_cwk, _parent_kpb, parent_signer, _parent_pk) =
1863            setup_client("Parent", ciphersuite, parent_provider);
1864        let parent_group = MlsGroup::builder()
1865            .ciphersuite(ciphersuite)
1866            .build(parent_provider, &parent_signer, parent_cwk)
1867            .unwrap();
1868
1869        let result = alice_group
1870            .commit_builder()
1871            .branch(alice_provider.rand(), &parent_group.branch_info())
1872            .unwrap()
1873            .load_psks(alice_provider.storage())
1874            .unwrap()
1875            .build(
1876                alice_provider.rand(),
1877                alice_provider.crypto(),
1878                &alice_signer,
1879                |_| true,
1880            );
1881
1882        assert!(
1883            matches!(
1884                result,
1885                Err(CreateCommitError::ProposalValidationError(
1886                    ProposalValidationError::Psk(PskError::NotAllowed)
1887                ))
1888            ),
1889            "expected a branch PSK outside the initial commit to be rejected, got {result:?}"
1890        );
1891    }
1892}