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