Skip to main content

openmls/group/mls_group/
commit_builder.rs

1//! This module contains the commit builder types, which can be used to build regular (i.e.
2//! non-external) commits. See the documentation of [`CommitBuilder`] for more information.
3
4use std::{borrow::BorrowMut, marker::PhantomData};
5
6use openmls_traits::{
7    crypto::OpenMlsCrypto, random::OpenMlsRand, signatures::Signer, storage::StorageProvider as _,
8};
9use tls_codec::Serialize as _;
10
11use crate::{
12    binary_tree::LeafNodeIndex,
13    ciphersuite::{signable::Signable as _, Secret},
14    extensions::Extensions,
15    framing::{FramingParameters, WireFormat},
16    group::{
17        diff::compute_path::{CommitType, PathComputationResult},
18        CommitBuilderStageError, CreateCommitError, Extension, ExternalPubExtension, GroupContext,
19        ProposalQueue, ProposalQueueError, QueuedProposal, RatchetTreeExtension, StagedCommit,
20        WireFormatPolicy,
21    },
22    key_packages::KeyPackage,
23    messages::{
24        group_info::{GroupInfo, GroupInfoTBS},
25        Commit, Welcome,
26    },
27    prelude::{
28        CredentialWithKey, InvalidExtensionError, LeafNodeParameters, LibraryError, NewSignerBundle,
29    },
30    schedule::{
31        psk::{load_psks, PskSecret},
32        EpochSecretsResult, JoinerSecret, KeySchedule, PreSharedKeyId,
33    },
34    storage::{OpenMlsProvider, StorageProvider},
35    treesync::errors::LeafNodeValidationError,
36    versions::ProtocolVersion,
37};
38#[cfg(feature = "virtual-clients-draft")]
39use crate::{
40    components::vc_derivation_info::{
41        pprf_input, DerivationInfo, EmulationEpochState, EpochEncryptionKey, EpochId, EpochInfoTbe,
42        OperationSecret, VcEmulation, VcPprf, VirtualClientOperationType, VirtualClientsError,
43        VC_COMPONENT_ID,
44    },
45    extensions::{AppDataDictionary, AppDataDictionaryExtension},
46    treesync::node::leaf_node::LeafNode,
47};
48#[cfg(feature = "extensions-draft-08")]
49use crate::{
50    messages::proposals::AppDataUpdateProposal,
51    prelude::processing::{AppDataDictionaryUpdater, AppDataUpdates},
52    schedule::application_export_tree::ApplicationExportTree,
53};
54
55/// Per-commit virtual-clients state loaded from the storage provider in
56/// `load_psks`, kept around through `build` (which punctures the PPRF) and
57/// `stage_commit` (which writes the punctured state back).
58#[cfg(feature = "virtual-clients-draft")]
59#[derive(Debug)]
60struct VcLoaded {
61    epoch_id: EpochId,
62    emulation_leaf_index: LeafNodeIndex,
63    pprf: VcPprf,
64    epoch_encryption_key: EpochEncryptionKey,
65}
66
67pub(crate) mod external_commits;
68
69pub use external_commits::{ExternalCommitBuilder, ExternalCommitBuilderError};
70
71#[cfg(doc)]
72use super::MlsGroupJoinConfig;
73
74use super::{
75    mls_auth_content::AuthenticatedContent,
76    staged_commit::{MemberStagedCommitState, StagedCommitState},
77    AddProposal, CreateCommitResult, GroupContextExtensionProposal, MlsGroup, MlsGroupState,
78    MlsMessageOut, PendingCommitState, Proposal, RemoveProposal, Sender,
79};
80
81#[derive(Debug)]
82struct ExternalCommitInfo {
83    aad: Vec<u8>,
84    credential: CredentialWithKey,
85    wire_format_policy: WireFormatPolicy,
86}
87
88#[derive(Debug, Default)]
89struct GroupInfoConfig {
90    create_group_info: bool,
91    use_ratchet_tree_extension: bool,
92    other_extensions: Vec<Extension>,
93}
94
95/// This stage is for populating the builder.
96#[derive(Debug)]
97pub struct Initial {
98    own_proposals: Vec<Proposal>,
99    force_self_update: bool,
100    leaf_node_parameters: LeafNodeParameters,
101    external_commit_info: Option<ExternalCommitInfo>,
102
103    /// Whether or not to clear the proposal queue of the group when staging the commit. Needs to
104    /// be done when we include the commits that have already been queued.
105    consume_proposal_store: bool,
106
107    /// Per-commit virtual-clients material supplied by the application.
108    /// When `Some`, the commit's path secret and leaf encryption keypair
109    /// are derived from `operation_secret` (so a sibling virtual client
110    /// can rederive them), the `DerivationInfo` blob is embedded in the
111    /// new leaf's `app_data_dictionary` extension, and the per-commit
112    /// secrets are persisted at `stage_commit` time.
113    #[cfg(feature = "virtual-clients-draft")]
114    vc_emulation: Option<VcEmulation>,
115}
116
117impl Default for Initial {
118    fn default() -> Self {
119        Initial {
120            consume_proposal_store: true,
121            force_self_update: false,
122            leaf_node_parameters: LeafNodeParameters::default(),
123            own_proposals: vec![],
124            external_commit_info: None,
125            #[cfg(feature = "virtual-clients-draft")]
126            vc_emulation: None,
127        }
128    }
129}
130
131/// This stage is after the PSKs were loaded, ready for validation
132pub struct LoadedPsks {
133    own_proposals: Vec<Proposal>,
134    force_self_update: bool,
135    leaf_node_parameters: LeafNodeParameters,
136    external_commit_info: Option<ExternalCommitInfo>,
137
138    /// Whether or not to clear the proposal queue of the group when staging the commit. Needs to
139    /// be done when we include the commits that have already been queued.
140    consume_proposal_store: bool,
141    psks: Vec<(PreSharedKeyId, Secret)>,
142
143    /// The GroupInfo creation config
144    group_info_config: GroupInfoConfig,
145
146    #[cfg(feature = "extensions-draft-08")]
147    app_data_dictionary_updates: Option<AppDataUpdates>,
148
149    /// Per-emulation-epoch PPRF + AEAD key loaded from the storage
150    /// provider when the caller opted in via `vc_emulation`. The PPRF is
151    /// punctured during `build` and persisted by `stage_commit`.
152    #[cfg(feature = "virtual-clients-draft")]
153    vc_loaded: Option<VcLoaded>,
154}
155
156/// This stage is after we validated the data, ready for staging and exporting the messages
157#[derive(Debug)]
158pub struct Complete {
159    result: CreateCommitResult,
160    // Only for external commits
161    original_wire_format_policy: Option<WireFormatPolicy>,
162    /// Punctured PPRF state to persist back to storage on `stage_commit`,
163    /// keyed on `epoch_id`. `None` when virtual-clients was not opted into
164    /// for this commit.
165    #[cfg(feature = "virtual-clients-draft")]
166    vc_punctured: Option<VcPunctured>,
167}
168
169#[cfg(feature = "virtual-clients-draft")]
170#[derive(Debug)]
171struct VcPunctured {
172    epoch_id: EpochId,
173    pprf: VcPprf,
174}
175
176/// The [`CommitBuilder`] is used to easily and dynamically build commit messages.
177/// It operates in a series of stages:
178///
179/// The [`Initial`] stage is used to populate the builder with proposals and other data using
180/// method calls on the builder that let the builder stay in the same stage.
181///
182/// The next stage is [`LoadedPsks`], and it signifies the stage after the builder loaded the the
183/// pre-shared keys for the PreSharedKey proposals in this commit.
184///
185/// Then comes the [`Complete`] stage, which denotes that all data has been validated. From this
186/// stage, the commit can be staged in the group, and the outgoing messages returned.
187///
188/// For example, to create a commit to a new Add proposal with a KeyPackage `key_package_to_add`
189/// that does not commit to the proposals in the proposal store, one could build the commit as
190/// follows:
191///
192/// ```rust,ignore
193/// let message_bundle: CommitMessageBundle = mls_group
194///   .commit_builder()
195///   .consume_proposal_store(false)
196///   .add_proposal(key_package_to_add)
197///   .load_psks(provider.storage())?
198///   .build(provider.rand(), provider.crypto(), signer, app_policy_proposals)?
199///   .stage_commit(provider)?;
200///
201/// let commit = message_bundle.commit();
202/// let welcome = message_bundle.welcome().expect("expected a welcome since there was an add");
203/// let group_info = message_bundle.welcome().expect("expected a group info since there was an add");
204/// ```
205///
206/// In this example `signer` is a reference to a [`Signer`] and `app_policy_proposals` is the
207/// application-defined policy for which proposals to accept, implemented by an
208/// `FnMut(&QueuedProposal) -> bool`.
209///
210/// See the [book] for another example.
211///
212/// [book]: https://book.openmls.tech/user_manual/add_members.html
213#[derive(Debug)]
214pub struct CommitBuilder<'a, T, G: BorrowMut<MlsGroup> = &'a mut MlsGroup> {
215    /// A mutable reference to the MlsGroup. This means that we hold an exclusive lock on the group
216    /// for the lifetime of this builder.
217    group: G,
218
219    /// The current stage
220    stage: T,
221
222    pd: PhantomData<&'a ()>,
223}
224
225impl<'a, T, G: BorrowMut<MlsGroup>> CommitBuilder<'a, T, G> {
226    pub(crate) fn replace_stage<NextStage>(
227        self,
228        next_stage: NextStage,
229    ) -> (T, CommitBuilder<'a, NextStage, G>) {
230        self.map_stage(|prev_stage| (prev_stage, next_stage))
231    }
232
233    pub(crate) fn into_stage<NextStage>(
234        self,
235        next_stage: NextStage,
236    ) -> CommitBuilder<'a, NextStage, G> {
237        self.replace_stage(next_stage).1
238    }
239
240    fn take_stage(self) -> (T, CommitBuilder<'a, (), G>) {
241        self.replace_stage(())
242    }
243
244    fn map_stage<NextStage, Aux, F: FnOnce(T) -> (Aux, NextStage)>(
245        self,
246        f: F,
247    ) -> (Aux, CommitBuilder<'a, NextStage, G>) {
248        let Self {
249            group,
250            stage,
251            pd: PhantomData,
252        } = self;
253
254        let (aux, stage) = f(stage);
255
256        (
257            aux,
258            CommitBuilder {
259                group,
260                stage,
261                pd: PhantomData,
262            },
263        )
264    }
265
266    #[cfg(feature = "fork-resolution")]
267    pub(crate) fn stage(&self) -> &T {
268        &self.stage
269    }
270}
271
272impl MlsGroup {
273    /// Returns a builder for commits.
274    pub fn commit_builder(&mut self) -> CommitBuilder<'_, Initial> {
275        CommitBuilder::<'_, Initial, &mut MlsGroup>::new(self)
276    }
277}
278
279// Impls that only apply to non-external commits.
280impl<'a> CommitBuilder<'a, Initial, &mut MlsGroup> {
281    /// Sets whether or not the proposals in the proposal store of the group should be included in
282    /// the commit. Defaults to `true`.
283    pub fn consume_proposal_store(mut self, consume_proposal_store: bool) -> Self {
284        self.stage.consume_proposal_store = consume_proposal_store;
285        self
286    }
287
288    /// Sets whether or not the commit should force a self-update. Defaults to `false`.
289    pub fn force_self_update(mut self, force_self_update: bool) -> Self {
290        self.stage.force_self_update = force_self_update;
291        self
292    }
293
294    /// Adds an Add proposal to the provided [`KeyPackage`] to the list of proposals to be
295    /// committed.
296    pub fn propose_adds(mut self, key_packages: impl IntoIterator<Item = KeyPackage>) -> Self {
297        self.stage.own_proposals.extend(
298            key_packages
299                .into_iter()
300                .map(|key_package| Proposal::add(AddProposal { key_package })),
301        );
302        self
303    }
304
305    /// Adds a Remove proposal for the provided [`LeafNodeIndex`]es to the list of proposals to be
306    /// committed.
307    pub fn propose_removals(mut self, removed: impl IntoIterator<Item = LeafNodeIndex>) -> Self {
308        self.stage.own_proposals.extend(
309            removed
310                .into_iter()
311                .map(|removed| Proposal::remove(RemoveProposal { removed })),
312        );
313        self
314    }
315
316    /// Adds a GroupContextExtensions proposal for the provided [`Extensions`] to the list of
317    /// proposals to be committed.
318    pub fn propose_group_context_extensions(
319        mut self,
320        extensions: Extensions<GroupContext>,
321    ) -> Result<Self, CreateCommitError> {
322        let proposal = GroupContextExtensionProposal::new(extensions);
323        self.stage
324            .own_proposals
325            .push(Proposal::group_context_extensions(proposal));
326        Ok(self)
327    }
328
329    /// Adds a proposal to the proposals to be committed. To add multiple
330    /// proposals, use [`Self::add_proposals`].
331    pub fn add_proposal(mut self, proposal: Proposal) -> Self {
332        self.stage.own_proposals.push(proposal);
333        self
334    }
335
336    /// Adds the proposals in the iterator to the proposals to be committed.
337    pub fn add_proposals(mut self, proposals: impl IntoIterator<Item = Proposal>) -> Self {
338        self.stage.own_proposals.extend(proposals);
339        self
340    }
341}
342
343// Impls that apply to regular and external commits.
344impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, Initial, G> {
345    /// returns a new [`CommitBuilder`] for the given [`MlsGroup`].
346    pub fn new(group: G) -> CommitBuilder<'a, Initial, G> {
347        let stage = Initial {
348            ..Default::default()
349        };
350        CommitBuilder {
351            group,
352            stage,
353            pd: PhantomData,
354        }
355    }
356
357    /// Sets the leaf node parameters for the new leaf node in a self-update. Implies that a
358    /// self-update takes place.
359    pub fn leaf_node_parameters(mut self, leaf_node_parameters: LeafNodeParameters) -> Self {
360        self.stage.leaf_node_parameters = leaf_node_parameters;
361        self
362    }
363
364    /// Opt this commit into the virtual-clients-draft sender flow.
365    ///
366    /// The application supplies a [`VcEmulation`] referencing an
367    /// already-registered emulation epoch (see
368    /// [`MlsGroup::register_vc_emulation_epoch`]). When set, building this
369    /// commit will:
370    ///
371    /// - load the per-epoch [`VcPprf`](crate::components::vc_derivation_info)
372    ///   and AEAD key from the storage provider;
373    /// - draw fresh per-commit randomness, evaluate (and puncture) the
374    ///   PPRF on the resulting input to produce the per-commit
375    ///   `OperationSecret`, and persist the punctured state at
376    ///   `stage_commit` time;
377    /// - derive the path secret and the new leaf's encryption keypair
378    ///   from that `OperationSecret`, so a sibling virtual client can
379    ///   rederive them on the receiver side;
380    /// - embed an encrypted `DerivationInfo` blob under [`VC_COMPONENT_ID`]
381    ///   in the new leaf's `app_data_dictionary` extension.
382    ///
383    /// The application is responsible for ensuring the new leaf:
384    ///
385    /// - lists [`ExtensionType::AppDataDictionary`](crate::extensions::ExtensionType::AppDataDictionary)
386    ///   in its `Capabilities.extensions`, and
387    /// - signals support for [`VC_COMPONENT_ID`].
388    ///
389    /// If those preconditions are not met the build fails with
390    /// `VirtualClientsError::AppDataDictionaryNotSupported` or
391    /// `VirtualClientsError::VcComponentNotListed` (wrapped in
392    /// [`CreateCommitError::VirtualClientsError`]) before any randomness
393    /// is drawn.
394    ///
395    /// Implies that a self-update takes place: the commit will always have
396    /// a path even if no other proposals are queued.
397    ///
398    /// [`MlsGroup::register_vc_emulation_epoch`]: crate::group::MlsGroup::register_vc_emulation_epoch
399    #[cfg(feature = "virtual-clients-draft")]
400    pub fn vc_emulation(mut self, emulation: VcEmulation) -> Self {
401        self.stage.vc_emulation = Some(emulation);
402        self
403    }
404
405    /// Loads the PSKs for the PskProposals marked for inclusion and moves on to the next phase.
406    pub fn load_psks<Storage: StorageProvider>(
407        self,
408        storage: &'a Storage,
409    ) -> Result<CommitBuilder<'a, LoadedPsks, G>, CreateCommitError> {
410        let psk_ids: Vec<_> = self
411            .stage
412            .own_proposals
413            .iter()
414            .chain(
415                self.group
416                    .borrow()
417                    .proposal_store()
418                    .proposals()
419                    .map(|queued_proposal| queued_proposal.proposal()),
420            )
421            .filter_map(|proposal| match proposal {
422                Proposal::PreSharedKey(psk_proposal) => Some(psk_proposal.clone().into_psk_id()),
423                _ => None,
424            })
425            .collect();
426
427        // Load the PSKs and make the PskIds owned.
428        let psks = load_psks(storage, &self.group.borrow().resumption_psk_store, &psk_ids)?
429            .into_iter()
430            .map(|(psk_id_ref, key)| (psk_id_ref.clone(), key))
431            .collect();
432
433        // Initialize GroupInfoConfig
434        let use_ratchet_tree_extension = self
435            .group
436            .borrow()
437            .configuration()
438            .use_ratchet_tree_extension;
439
440        let group_info_config = GroupInfoConfig {
441            use_ratchet_tree_extension,
442            create_group_info: use_ratchet_tree_extension,
443            other_extensions: vec![],
444        };
445
446        // When the caller opted into virtual-clients for this commit, load
447        // the per-emulation-epoch PPRF + state (AEAD key + the registering
448        // client's emulation-group leaf index) from storage now — both
449        // must already be registered via
450        // `MlsGroup::register_vc_emulation_epoch`. Refusing to instantiate
451        // either on the fly avoids accidentally diverging from a sibling
452        // virtual client's already-advanced PPRF.
453        #[cfg(feature = "virtual-clients-draft")]
454        let vc_loaded = if let Some(emulation) = &self.stage.vc_emulation {
455            let pprf: VcPprf = storage
456                .vc_pprf(&emulation.epoch_id)
457                .map_err(|e| {
458                    log::error!("vc: load pprf in load_psks failed: {e:?}");
459                    CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
460                })?
461                .ok_or(VirtualClientsError::MissingPprf)?;
462            let state: EmulationEpochState = storage
463                .vc_emulation_epoch_state(&emulation.epoch_id)
464                .map_err(|e| {
465                    log::error!("vc: load emulation epoch state in load_psks failed: {e:?}");
466                    CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
467                })?
468                .ok_or(VirtualClientsError::MissingEpochEncryptionKey)?;
469            let (emulation_leaf_index, epoch_encryption_key) = state.into_parts();
470            Some(VcLoaded {
471                epoch_id: emulation.epoch_id.clone(),
472                emulation_leaf_index,
473                pprf,
474                epoch_encryption_key,
475            })
476        } else {
477            None
478        };
479
480        Ok(self
481            .map_stage(|stage| {
482                (
483                    (),
484                    LoadedPsks {
485                        own_proposals: stage.own_proposals,
486                        psks,
487                        force_self_update: stage.force_self_update,
488                        leaf_node_parameters: stage.leaf_node_parameters,
489                        consume_proposal_store: stage.consume_proposal_store,
490                        group_info_config,
491                        external_commit_info: stage.external_commit_info,
492                        #[cfg(feature = "extensions-draft-08")]
493                        app_data_dictionary_updates: None,
494                        #[cfg(feature = "virtual-clients-draft")]
495                        vc_loaded,
496                    },
497                )
498            })
499            .1)
500    }
501}
502
503impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, LoadedPsks, G> {
504    /// Sets whether or not a [`GroupInfo`] should be created when the commit is staged. Defaults to
505    /// the value of the [`MlsGroup`]s [`MlsGroupJoinConfig`].
506    pub fn create_group_info(mut self, create_group_info: bool) -> Self {
507        self.stage.group_info_config.create_group_info = create_group_info;
508        self
509    }
510
511    /// Sets whether the [`GroupInfo`] should contain the ratchet tree extension. If set to `true`,
512    /// enables the [`GroupInfo`] to be created when the commit is staged.
513    pub fn use_ratchet_tree_extension(mut self, use_ratchet_tree_extension: bool) -> Self {
514        if use_ratchet_tree_extension {
515            self.stage.group_info_config.create_group_info = true;
516        }
517        self.stage.group_info_config.use_ratchet_tree_extension = use_ratchet_tree_extension;
518        self
519    }
520
521    /// Add the provided [`Extension`]s to the [`GroupInfo`].
522    ///
523    ///  Returns an error if a  [`RatchetTreeExtension`] or [`ExternalPubExtension`] is added
524    ///  directly here.
525    pub fn create_group_info_with_extensions(
526        mut self,
527        extensions: impl IntoIterator<Item = Extension>,
528    ) -> Result<Self, InvalidExtensionError> {
529        self.stage.group_info_config.create_group_info = true;
530        self.stage.group_info_config.other_extensions = extensions
531            .into_iter()
532            .map(|extension| {
533                if extension.as_ratchet_tree_extension().is_ok()
534                    || extension.as_external_pub_extension().is_ok()
535                {
536                    Err(InvalidExtensionError::CannotAddDirectlyToGroupInfo)
537                } else {
538                    Ok(extension)
539                }
540            })
541            .collect::<Result<Vec<_>, _>>()?;
542
543        Ok(self)
544    }
545    /// Validates the inputs and builds the commit. The last argument `f` is a function that lets
546    /// the caller filter the proposals that are considered for inclusion. This provides a way for
547    /// the application to enforce custom policies in the creation of commits.
548    pub fn build<S: Signer>(
549        self,
550        rand: &impl OpenMlsRand,
551        crypto: &impl OpenMlsCrypto,
552        signer: &S,
553        f: impl FnMut(&QueuedProposal) -> bool,
554    ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
555        self.build_internal(rand, crypto, signer, None::<NewSignerBundle<'_, S>>, f)
556    }
557
558    /// Just like `build`, this function validates the inputs and builds the
559    /// commit. The last argument `f` is a function that lets the caller filter
560    /// the proposals that are considered for inclusion. This provides a way for
561    /// the application to enforce custom policies in the creation of commits.
562    ///
563    /// In contrast to `build`, this function can be used to create commits that
564    /// rotate the own leaf node's signature key.
565    pub fn build_with_new_signer<S: Signer>(
566        self,
567        rand: &impl OpenMlsRand,
568        crypto: &impl OpenMlsCrypto,
569        old_signer: &impl Signer,
570        new_signer: NewSignerBundle<'_, S>,
571        f: impl FnMut(&QueuedProposal) -> bool,
572    ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
573        self.build_internal(rand, crypto, old_signer, Some(new_signer), f)
574    }
575
576    fn build_internal<S: Signer>(
577        self,
578        rand: &impl OpenMlsRand,
579        crypto: &impl OpenMlsCrypto,
580        old_signer: &impl Signer,
581        new_signer: Option<NewSignerBundle<'_, S>>,
582        f: impl FnMut(&QueuedProposal) -> bool,
583    ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
584        let (mut cur_stage, builder) = self.take_stage();
585
586        // retrieve the config
587        let GroupInfoConfig {
588            create_group_info,
589            use_ratchet_tree_extension,
590            other_extensions,
591        } = cur_stage.group_info_config;
592
593        let group = builder.group.borrow();
594        let ciphersuite = group.ciphersuite();
595        let own_leaf_index = group.own_leaf_index();
596        let (sender, is_external_commit) = match cur_stage.external_commit_info {
597            None => (Sender::build_member(own_leaf_index), false),
598            Some(_) => (Sender::NewMemberCommit, true),
599        };
600        let psks = cur_stage.psks;
601
602        // put the pending and uniform proposals into a uniform shape,
603        // i.e. produce queued proposals from the own proposals
604        let own_proposals: Vec<_> = cur_stage
605            .own_proposals
606            .into_iter()
607            .map(|proposal| {
608                QueuedProposal::from_proposal_and_sender(ciphersuite, crypto, proposal, &sender)
609            })
610            .collect::<Result<_, _>>()?;
611
612        // prepare an iterator for the proposals in the group's proposal store, but only if the
613        // flag is set.
614        let group_proposal_store_queue = group
615            .pending_proposals()
616            .filter(|_| cur_stage.consume_proposal_store)
617            .cloned();
618
619        // prepare the iterator for the proposal validation and seletion function. That function
620        // assumes that "earlier in the list" means "older", so since our own proposals are
621        // newest, we have to put them last.
622        let proposal_queue = group_proposal_store_queue.chain(own_proposals).filter(f);
623
624        let (proposal_queue, contains_own_updates) =
625            ProposalQueue::filter_proposals(proposal_queue, group.own_leaf_index).map_err(|e| {
626                match e {
627                    ProposalQueueError::LibraryError(e) => e.into(),
628                    ProposalQueueError::ProposalNotFound => CreateCommitError::MissingProposal,
629                    ProposalQueueError::UpdateFromExternalSender
630                    | ProposalQueueError::SelfRemoveFromNonMember => {
631                        CreateCommitError::WrongProposalSenderType
632                    }
633                }
634            })?;
635
636        // Validate the proposals by doing the following checks:
637
638        // ValSem113: All Proposals: The proposal type must be supported by all
639        // members of the group
640        group
641            .public_group
642            .validate_proposal_type_support(&proposal_queue)?;
643        // ValSem101
644        // ValSem102
645        // ValSem103
646        // ValSem104
647        group
648            .public_group
649            .validate_key_uniqueness(&proposal_queue, None)?;
650        // ValSem105
651        group.public_group.validate_add_proposals(&proposal_queue)?;
652        // ValSem106
653        // ValSem109
654        group.public_group.validate_capabilities(&proposal_queue)?;
655        // ValSem107
656        // ValSem108
657        group
658            .public_group
659            .validate_remove_proposals(&proposal_queue)?;
660        group
661            .public_group
662            .validate_pre_shared_key_proposals(&proposal_queue)?;
663        // Validate update proposals for member commits
664        // ValSem110
665        // ValSem111
666        // ValSem112
667        group
668            .public_group
669            .validate_update_proposals(&proposal_queue, own_leaf_index)?;
670
671        // ValSem208
672        // ValSem209
673        group
674            .public_group
675            .validate_group_context_extensions_proposal(&proposal_queue)?;
676
677        #[cfg(feature = "extensions-draft-08")]
678        group
679            .public_group
680            .validate_app_data_update_proposals_and_group_context(&proposal_queue)?;
681
682        if is_external_commit {
683            group
684                .public_group
685                .validate_external_commit(&proposal_queue)?;
686        }
687
688        let proposal_reference_list = proposal_queue.commit_list();
689
690        // Make a copy of the public group to apply proposals safely
691        let mut diff = group.public_group.empty_diff();
692
693        // Apply proposals to tree
694        #[cfg(feature = "extensions-draft-08")]
695        let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
696            &proposal_queue,
697            own_leaf_index,
698            cur_stage.app_data_dictionary_updates,
699        )?;
700        #[cfg(not(feature = "extensions-draft-08"))]
701        let apply_proposals_values = diff.apply_proposals(&proposal_queue, own_leaf_index)?;
702        if apply_proposals_values.self_removed && !is_external_commit {
703            return Err(CreateCommitError::CannotRemoveSelf);
704        }
705
706        // Virtual-clients sender hook: when the caller opted into VC for
707        // this commit, validate that the effective leaf is configured to
708        // accept the derivation-info entry (capabilities + AppComponents),
709        // then evaluate the pre-loaded PPRF (mutating it in place — the
710        // punctured state is propagated to `Complete` for `stage_commit`
711        // to persist), derive the path-secret + leaf-keypair override,
712        // and embed the `DerivationInfo` blob in the leaf's
713        // `app_data_dictionary` extension.
714        #[cfg(feature = "virtual-clients-draft")]
715        let own_update_override = if let Some(loaded) = cur_stage.vc_loaded.as_mut() {
716            // Run the leaf-configuration pre-check. Returns the resolved
717            // `AppDataDictionary` so the inject step can preserve every other
718            // entry the application registered, including the AppComponents
719            // entry that survives across multiple VC commits.
720            let resolved_dictionary = check_vc_leaf_configuration(
721                &cur_stage.leaf_node_parameters,
722                group,
723                own_leaf_index,
724                is_external_commit,
725            )?;
726
727            Some(apply_vc_emulation(
728                loaded,
729                &mut cur_stage.leaf_node_parameters,
730                resolved_dictionary,
731                rand,
732                crypto,
733                ciphersuite,
734            )?)
735        } else {
736            None
737        };
738        #[cfg(not(feature = "virtual-clients-draft"))]
739        let own_update_override: Option<crate::treesync::diff::OwnUpdatePathOverride> = None;
740
741        let path_computation_result =
742            // If path is needed, compute path values
743            if apply_proposals_values.path_required
744                || contains_own_updates
745                || cur_stage.force_self_update
746                || !cur_stage.leaf_node_parameters.is_empty()
747            {
748                let commit_type = match &cur_stage.external_commit_info {
749                    Some(ExternalCommitInfo { credential , ..}) => {
750                        CommitType::External(credential.clone())
751                    }
752                    None => CommitType::Member,
753                };
754                // Process the path. This includes updating the provisional
755                // group context by updating the epoch and computing the new
756                // tree hash.
757                if let Some(new_signer) = new_signer {
758                    if let Some(credential_with_key) =
759                        cur_stage.leaf_node_parameters.credential_with_key()
760                    {
761                        if credential_with_key != &new_signer.credential_with_key {
762                            return Err(CreateCommitError::InvalidLeafNodeParameters);
763                        }
764                    }
765                    cur_stage.leaf_node_parameters.set_credential_with_key(
766                        new_signer.credential_with_key,
767                    );
768
769                    diff.compute_path(
770                        rand,
771                        crypto,
772                        own_leaf_index,
773                        apply_proposals_values.exclusion_list(),
774                        &commit_type,
775                        &cur_stage.leaf_node_parameters,
776                        new_signer.signer,
777                        apply_proposals_values.extensions.clone(),
778                        own_update_override,
779                    )?
780                } else {
781                    diff.compute_path(
782                        rand,
783                        crypto,
784                        own_leaf_index,
785                        apply_proposals_values.exclusion_list(),
786                        &commit_type,
787                        &cur_stage.leaf_node_parameters,
788                        old_signer,
789                        apply_proposals_values.extensions.clone(),
790                        own_update_override,
791                    )?
792                }
793            } else {
794                // If path is not needed, update the group context and return
795                // empty path processing results
796                diff.update_group_context(crypto, apply_proposals_values.extensions.clone())?;
797                PathComputationResult::default()
798            };
799
800        let update_path_leaf_node = path_computation_result
801            .encrypted_path
802            .as_ref()
803            .map(|path| path.leaf_node().clone());
804
805        // Validate that the update path leaf node's capabilities
806        if let Some(ref leaf_node) = update_path_leaf_node {
807            // Check that all extension types in the group context that are valid in leaf nodes
808            // are supported by the leaf node
809            //
810            // This is currently not required by the RFC, likely by mistake:
811            // https://mailarchive.ietf.org/arch/msg/mls/k18P4FP7dfS2cBmP0kL6Uh50-ok/
812            if !diff
813                .group_context()
814                .extensions()
815                .iter()
816                .map(Extension::extension_type)
817                .all(|ext_type| leaf_node.supports_extension(&ext_type))
818            {
819                return Err(CreateCommitError::LeafNodeValidation(
820                    LeafNodeValidationError::UnsupportedExtensions,
821                ));
822            }
823
824            // Check that the leaf node supports everything listed in the required capabilities.
825            // https://validation.openmls.tech/#valn0103
826            if let Some(required_capabilities) =
827                diff.group_context().extensions().required_capabilities()
828            {
829                leaf_node
830                    .capabilities()
831                    .supports_required_capabilities(required_capabilities)?
832            }
833        }
834
835        // Create commit message
836        let commit = Commit {
837            proposals: proposal_reference_list,
838            path: path_computation_result.encrypted_path,
839        };
840
841        let (outgoing_aad, wire_format): (Vec<u8>, WireFormat) =
842            match &cur_stage.external_commit_info {
843                None => (
844                    group.outgoing_authenticated_data()?,
845                    group.outgoing_wire_format(),
846                ),
847                Some(ExternalCommitInfo { aad, .. }) => {
848                    // The spec requires the SafeAAD prefix even with zero items
849                    // when the target GroupContext has `safe_aad` present, so a
850                    // bare `aad` would be rejected by SafeAAD-aware receivers.
851                    #[cfg(feature = "extensions-draft-08")]
852                    let aad_bytes = if group.context().safe_aad_required() {
853                        crate::framing::safe_aad::assemble_authenticated_data(
854                            &crate::framing::SafeAad::empty(),
855                            aad,
856                        )
857                        .map_err(|_| LibraryError::custom("SafeAad serialization failed"))?
858                    } else {
859                        aad.clone()
860                    };
861                    #[cfg(not(feature = "extensions-draft-08"))]
862                    let aad_bytes = aad.clone();
863                    (aad_bytes, WireFormat::PublicMessage)
864                }
865            };
866        let framing_parameters = FramingParameters::new(&outgoing_aad, wire_format);
867
868        // Build AuthenticatedContent
869        let mut authenticated_content = AuthenticatedContent::commit(
870            framing_parameters,
871            sender,
872            commit,
873            group.public_group.group_context(),
874            old_signer,
875        )?;
876
877        // Update the confirmed transcript hash using the commit we just created.
878        diff.update_confirmed_transcript_hash(crypto, &authenticated_content)?;
879
880        let serialized_provisional_group_context = diff
881            .group_context()
882            .tls_serialize_detached()
883            .map_err(LibraryError::missing_bound_check)?;
884
885        let joiner_secret = JoinerSecret::new(
886            crypto,
887            ciphersuite,
888            path_computation_result.commit_secret,
889            group.group_epoch_secrets().init_secret(),
890            &serialized_provisional_group_context,
891        )
892        .map_err(LibraryError::unexpected_crypto_error)?;
893
894        // Prepare the PskSecret
895        let psk_secret = { PskSecret::new(crypto, ciphersuite, psks)? };
896
897        // Create key schedule
898        let mut key_schedule = KeySchedule::init(ciphersuite, crypto, &joiner_secret, psk_secret)?;
899
900        let serialized_provisional_group_context = diff
901            .group_context()
902            .tls_serialize_detached()
903            .map_err(LibraryError::missing_bound_check)?;
904
905        let welcome_secret = key_schedule
906            .welcome(crypto, ciphersuite)
907            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
908        key_schedule
909            .add_context(crypto, &serialized_provisional_group_context)
910            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
911        let EpochSecretsResult {
912            epoch_secrets: provisional_epoch_secrets,
913            #[cfg(feature = "extensions-draft-08")]
914            application_exporter,
915        } = key_schedule
916            .epoch_secrets(crypto, ciphersuite)
917            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
918
919        // Calculate the confirmation tag
920        let confirmation_tag = provisional_epoch_secrets
921            .confirmation_key()
922            .tag(
923                crypto,
924                ciphersuite,
925                diff.group_context().confirmed_transcript_hash(),
926            )
927            .map_err(LibraryError::unexpected_crypto_error)?;
928
929        // Set the confirmation tag
930        authenticated_content.set_confirmation_tag(confirmation_tag.clone());
931
932        diff.update_interim_transcript_hash(ciphersuite, crypto, confirmation_tag.clone())?;
933
934        // If there are invitations, we need to build a welcome
935        let needs_welcome = !apply_proposals_values.invitation_list.is_empty();
936
937        // We need a GroupInfo if we need to build a Welcome, or if
938        // `create_group_info` is set to `true`. If not overridden, `create_group_info`
939        // is set to the `use_ratchet_tree` flag in the group configuration.
940        let needs_group_info = needs_welcome || create_group_info;
941
942        let (welcome_option, group_info) = if !needs_group_info {
943            (None, None)
944        } else {
945            // Create the ratchet tree extension if necessary
946            let mut extensions_list = vec![];
947            if use_ratchet_tree_extension {
948                extensions_list.push(Extension::RatchetTree(RatchetTreeExtension::new(
949                    diff.export_ratchet_tree(),
950                )));
951            };
952            // Append rest of extensions
953            extensions_list.extend(other_extensions);
954
955            let mut extensions = Extensions::from_vec(extensions_list)?;
956
957            let welcome_option = needs_welcome
958                .then(|| -> Result<_, CreateCommitError> {
959                    let group_info_tbs = {
960                        GroupInfoTBS::new(
961                            diff.group_context().clone(),
962                            extensions.clone(),
963                            confirmation_tag.clone(),
964                            own_leaf_index,
965                        )?
966                    };
967                    // Sign to-be-signed group info.
968                    let group_info = group_info_tbs.sign(old_signer)?;
969
970                    // Encrypt GroupInfo object
971                    let (welcome_key, welcome_nonce) = welcome_secret
972                        .derive_welcome_key_nonce(crypto, ciphersuite)
973                        .map_err(LibraryError::unexpected_crypto_error)?;
974                    let encrypted_group_info = welcome_key
975                        .aead_seal(
976                            crypto,
977                            group_info
978                                .tls_serialize_detached()
979                                .map_err(LibraryError::missing_bound_check)?
980                                .as_slice(),
981                            &[],
982                            &welcome_nonce,
983                        )
984                        .map_err(LibraryError::unexpected_crypto_error)?;
985
986                    // Create group secrets for later use, so we can afterwards consume the
987                    // `joiner_secret`.
988                    let encrypted_secrets = diff.encrypt_group_secrets(
989                        &joiner_secret,
990                        apply_proposals_values.invitation_list,
991                        path_computation_result.plain_path.as_deref(),
992                        &apply_proposals_values.presharedkeys,
993                        &encrypted_group_info,
994                        crypto,
995                        own_leaf_index,
996                    )?;
997
998                    // Create welcome message
999                    let welcome =
1000                        Welcome::new(ciphersuite, encrypted_secrets, encrypted_group_info);
1001                    Ok(welcome)
1002                })
1003                .transpose()?;
1004
1005            // Create the GroupInfo for export if needed. In contrast to the Welcome, this
1006            // group info contains the external public key extension.
1007            let exported_group_info = create_group_info
1008                .then(|| -> Result<_, CreateCommitError> {
1009                    let external_pub = provisional_epoch_secrets
1010                        .external_secret()
1011                        .derive_external_keypair(crypto, ciphersuite)
1012                        .map_err(LibraryError::unexpected_crypto_error)?
1013                        .public;
1014
1015                    let external_pub_extension =
1016                        Extension::ExternalPub(ExternalPubExtension::new(external_pub.into()));
1017                    extensions.add(external_pub_extension)?;
1018                    let group_info_tbs = {
1019                        GroupInfoTBS::new(
1020                            diff.group_context().clone(),
1021                            extensions,
1022                            confirmation_tag.clone(),
1023                            own_leaf_index,
1024                        )?
1025                    };
1026                    // Sign to-be-signed group info.
1027                    Ok(group_info_tbs.sign(old_signer)?)
1028                })
1029                .transpose()?;
1030
1031            (welcome_option, exported_group_info)
1032        };
1033
1034        let (provisional_group_epoch_secrets, provisional_message_secrets) =
1035            provisional_epoch_secrets.split_secrets(
1036                serialized_provisional_group_context,
1037                diff.tree_size(),
1038                own_leaf_index,
1039            );
1040
1041        #[cfg(feature = "extensions-draft-08")]
1042        let application_export_tree = ApplicationExportTree::new(application_exporter);
1043        let staged_commit_state = MemberStagedCommitState::new(
1044            provisional_group_epoch_secrets,
1045            provisional_message_secrets,
1046            diff.into_staged_diff(crypto, ciphersuite)?,
1047            path_computation_result.new_keypairs,
1048            // The committer is not allowed to include their own update
1049            // proposal, so there is no extra keypair to store here.
1050            None,
1051            update_path_leaf_node,
1052            #[cfg(feature = "extensions-draft-08")]
1053            application_export_tree,
1054        );
1055        let staged_commit = StagedCommit::new(
1056            proposal_queue,
1057            StagedCommitState::GroupMember(Box::new(staged_commit_state)),
1058        );
1059
1060        Ok(builder.into_stage(Complete {
1061            result: CreateCommitResult {
1062                commit: authenticated_content,
1063                welcome_option,
1064                staged_commit,
1065                group_info: group_info.filter(|_| create_group_info),
1066            },
1067            original_wire_format_policy: cur_stage
1068                .external_commit_info
1069                .as_ref()
1070                .map(|info| info.wire_format_policy),
1071            #[cfg(feature = "virtual-clients-draft")]
1072            vc_punctured: cur_stage.vc_loaded.map(|loaded| VcPunctured {
1073                epoch_id: loaded.epoch_id,
1074                pprf: loaded.pprf,
1075            }),
1076        }))
1077    }
1078
1079    /// Creates a new [`AppDataUpdates`] based on the current state of the
1080    /// [`AppDataDictionary`] of the group.
1081    ///
1082    /// [`AppDataDictionary`]: crate::extensions::AppDataDictionary
1083    #[cfg(feature = "extensions-draft-08")]
1084    pub fn app_data_dictionary_updater(&self) -> AppDataDictionaryUpdater<'_> {
1085        AppDataDictionaryUpdater::new(self.group.borrow().context().app_data_dict())
1086    }
1087
1088    /// Sets the [`AppDataUpdates`] that contain the changes made by the AppDataUpdate proposals
1089    #[cfg(feature = "extensions-draft-08")]
1090    pub fn with_app_data_dictionary_updates(
1091        &mut self,
1092        app_data_dictionary_updates: Option<AppDataUpdates>,
1093    ) {
1094        self.stage.app_data_dictionary_updates = app_data_dictionary_updates;
1095    }
1096
1097    /// Returns an iterator over all AppDataUpdate proposals in the proposal store of the group
1098    #[cfg(feature = "extensions-draft-08")]
1099    pub fn app_data_update_proposals(&self) -> impl Iterator<Item = &AppDataUpdateProposal> {
1100        let proposal_store_proposals = self
1101            .group
1102            .borrow()
1103            .proposal_store()
1104            .proposals()
1105            .map(|queued_proposal| queued_proposal.proposal());
1106
1107        // The proposals in the proposal store come earlier than the own_proposals.
1108        let all_proposals = proposal_store_proposals.chain(self.stage.own_proposals.iter());
1109
1110        // Filter for AppDataUpdate proposals
1111        let mut app_data_update_proposals: Vec<&AppDataUpdateProposal> = all_proposals
1112            .filter_map(|proposal| match proposal {
1113                Proposal::AppDataUpdate(proposal) => Some(proposal.as_ref()),
1114                _ => None,
1115            })
1116            .collect();
1117
1118        app_data_update_proposals.sort_by_key(|prop| prop.component_id());
1119        app_data_update_proposals.into_iter()
1120    }
1121}
1122
1123// Impls that apply only to regular commits.
1124impl CommitBuilder<'_, Complete, &mut MlsGroup> {
1125    #[cfg(test)]
1126    pub(crate) fn commit_result(self) -> CreateCommitResult {
1127        self.stage.result
1128    }
1129
1130    /// Stages the commit and returns the protocol messages.
1131    pub fn stage_commit<Provider: OpenMlsProvider>(
1132        self,
1133        provider: &Provider,
1134    ) -> Result<CommitMessageBundle, CommitBuilderStageError<Provider::StorageError>> {
1135        let Self {
1136            group,
1137            stage:
1138                Complete {
1139                    result: create_commit_result,
1140                    original_wire_format_policy: _,
1141                    #[cfg(feature = "virtual-clients-draft")]
1142                    vc_punctured,
1143                },
1144            ..
1145        } = self;
1146
1147        // Set the current group state to [`MlsGroupState::PendingCommit`],
1148        // storing the current [`StagedCommit`] from the commit results
1149        group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
1150            create_commit_result.staged_commit,
1151        )));
1152
1153        provider
1154            .storage()
1155            .write_group_state(group.group_id(), &group.group_state)
1156            .map_err(CommitBuilderStageError::KeyStoreError)?;
1157
1158        // Persist the punctured PPRF for forward secrecy.
1159        #[cfg(feature = "virtual-clients-draft")]
1160        if let Some(VcPunctured { epoch_id, pprf }) = vc_punctured {
1161            provider
1162                .storage()
1163                .write_vc_pprf(&epoch_id, &pprf)
1164                .map_err(CommitBuilderStageError::KeyStoreError)?;
1165        }
1166
1167        group.reset_aad();
1168
1169        // Convert PublicMessage messages to MLSMessage and encrypt them if required by the
1170        // configuration.
1171        //
1172        // Note that this performs writes to the storage, so we should do that here, rather than
1173        // when working with the result.
1174        let mls_message = group.content_to_mls_message(create_commit_result.commit, provider)?;
1175
1176        Ok(CommitMessageBundle {
1177            version: group.version(),
1178            commit: mls_message,
1179            welcome: create_commit_result.welcome_option,
1180            group_info: create_commit_result.group_info,
1181        })
1182    }
1183}
1184
1185/// Build the path-secret + leaf-keypair override from a [`VcEmulation`] and
1186/// inject the corresponding `DerivationInfo` blob into `leaf_node_parameters`'s
1187/// `app_data_dictionary` extension.
1188#[cfg(feature = "virtual-clients-draft")]
1189fn apply_vc_emulation(
1190    loaded: &mut VcLoaded,
1191    leaf_node_parameters: &mut LeafNodeParameters,
1192    resolved_dictionary: AppDataDictionary,
1193    rand: &impl OpenMlsRand,
1194    crypto: &impl OpenMlsCrypto,
1195    ciphersuite: openmls_traits::types::Ciphersuite,
1196) -> Result<crate::treesync::diff::OwnUpdatePathOverride, CreateCommitError> {
1197    // Build the encrypted EpochInfoTbe payload the receiver will decrypt
1198    // with `epoch_encryption_key`. The same struct is hashed to produce
1199    // the PPRF input so that sender and receiver derive the same
1200    // per-commit secret.
1201    const VC_RANDOMNESS_SIZE: usize = 32;
1202    let random = rand.random_vec(VC_RANDOMNESS_SIZE).map_err(|e| {
1203        log::error!("vc: per-commit randomness failed: {e:?}");
1204        VirtualClientsError::RandError
1205    })?;
1206    let epoch_info = EpochInfoTbe {
1207        // Update-path leaf-node derivations are the only operation type
1208        // wired up so far; KeyPackage / Application will use this same
1209        // helper with their own variants when emitted.
1210        operation_type: VirtualClientOperationType::LeafNode,
1211        leaf_index: loaded.emulation_leaf_index,
1212        random,
1213    };
1214
1215    let pprf_input_bytes = pprf_input(crypto, ciphersuite, &epoch_info)?;
1216    let operation_secret: OperationSecret = loaded
1217        .pprf
1218        .evaluate(crypto, ciphersuite, &pprf_input_bytes)
1219        .map_err(VirtualClientsError::from)?
1220        .into();
1221
1222    let encrypted_epoch_info = epoch_info.encrypt(
1223        crypto,
1224        rand,
1225        ciphersuite,
1226        &loaded.epoch_encryption_key,
1227        &loaded.epoch_id,
1228    )?;
1229    let derivation_info = DerivationInfo::new(loaded.epoch_id.clone(), encrypted_epoch_info);
1230    let derivation_info_bytes = derivation_info
1231        .tls_serialize_detached()
1232        .map_err(VirtualClientsError::from)?;
1233
1234    inject_vc_derivation_info(
1235        leaf_node_parameters,
1236        resolved_dictionary,
1237        derivation_info_bytes,
1238    )?;
1239
1240    let path_secret = operation_secret
1241        .derive_path_generation_secret(crypto, ciphersuite)?
1242        .into();
1243    let leaf_encryption_keypair = operation_secret
1244        .derive_encryption_key_secret(crypto, ciphersuite)?
1245        .generate_encryption_key_pair(crypto, ciphersuite)?;
1246    Ok(crate::treesync::diff::OwnUpdatePathOverride {
1247        path_secret,
1248        leaf_encryption_keypair,
1249    })
1250}
1251
1252/// Verify that the effective leaf for this commit (= the merged view of
1253/// `leaf_node_parameters` over the existing leaf, or `leaf_node_parameters`
1254/// alone for external commits) declares `AppDataDictionary` and lists
1255/// [`VC_COMPONENT_ID`] in its `AppComponents` entry. Without both, the
1256/// receiver cannot reliably surface the derivation-info entry to the
1257/// virtual-clients consumer, so we reject the commit at build time.
1258///
1259/// Returns the resolved `AppDataDictionary` (caller's override merged
1260/// over the existing leaf's, with the caller winning on duplicate keys)
1261/// so subsequent injection of the VC derivation-info preserves the
1262/// AppComponents entry across commits.
1263#[cfg(feature = "virtual-clients-draft")]
1264fn check_vc_leaf_configuration(
1265    leaf_node_parameters: &LeafNodeParameters,
1266    group: &MlsGroup,
1267    own_leaf_index: LeafNodeIndex,
1268    is_external_commit: bool,
1269) -> Result<AppDataDictionary, CreateCommitError> {
1270    use crate::{
1271        component::{ComponentId, ComponentType},
1272        extensions::ExtensionType,
1273    };
1274    use tls_codec::DeserializeBytes as _;
1275
1276    let current_leaf = if is_external_commit {
1277        None
1278    } else {
1279        Some(group.public_group().leaf(own_leaf_index).ok_or_else(|| {
1280            LibraryError::custom("Couldn't find own leaf for VC capability check")
1281        })?)
1282    };
1283
1284    let supports_app_data_dictionary = match leaf_node_parameters.capabilities() {
1285        Some(c) => c.extensions().contains(&ExtensionType::AppDataDictionary),
1286        None => current_leaf
1287            .map(|leaf| {
1288                leaf.capabilities()
1289                    .extensions()
1290                    .contains(&ExtensionType::AppDataDictionary)
1291            })
1292            .unwrap_or(false),
1293    };
1294    if !supports_app_data_dictionary {
1295        return Err(CreateCommitError::VirtualClientsError(
1296            VirtualClientsError::AppDataDictionaryNotSupported,
1297        ));
1298    }
1299
1300    // Merge the dictionary from the current leaf with anything the
1301    // caller passed in `leaf_node_parameters`, with the caller winning.
1302    // For external commits there's no current leaf to merge from.
1303    let mut resolved_dictionary = current_leaf
1304        .and_then(|leaf| leaf.extensions().app_data_dictionary())
1305        .map(|ext| ext.dictionary().clone())
1306        .unwrap_or_default();
1307    if let Some(caller_dict) = leaf_node_parameters
1308        .extensions()
1309        .and_then(|exts| exts.app_data_dictionary())
1310    {
1311        for entry in caller_dict.dictionary().entries() {
1312            resolved_dictionary.insert(entry.id(), entry.data().to_vec());
1313        }
1314    }
1315
1316    let app_components_bytes = resolved_dictionary
1317        .get(&ComponentId::from(ComponentType::AppComponents))
1318        .map(|bytes| bytes.to_vec());
1319    let Some(app_components_bytes) = app_components_bytes else {
1320        return Err(CreateCommitError::VirtualClientsError(
1321            VirtualClientsError::VcComponentNotListed,
1322        ));
1323    };
1324
1325    // The AppComponents body is `ComponentID supported_components<V>`,
1326    // i.e. a TLS-encoded variable-length vector of u16. `Vec<u16>`'s
1327    // `DeserializeBytes` impl handles the length prefix.
1328    let supported_components = Vec::<u16>::tls_deserialize_exact_bytes(&app_components_bytes)
1329        .map_err(|e| {
1330            log::error!("vc: AppComponents body failed to deserialize: {e:?}");
1331            CreateCommitError::VirtualClientsError(VirtualClientsError::VcComponentNotListed)
1332        })?;
1333    if !supported_components.contains(&VC_COMPONENT_ID) {
1334        return Err(CreateCommitError::VirtualClientsError(
1335            VirtualClientsError::VcComponentNotListed,
1336        ));
1337    }
1338
1339    Ok(resolved_dictionary)
1340}
1341
1342/// Merge a virtual-clients derivation info blob into
1343/// `leaf_node_parameters.app_data_dictionary[VC_COMPONENT_ID]`,
1344/// preserving every other component id from `resolved_dictionary` and
1345/// every non-`AppDataDictionary` leaf-node extension the caller put in.
1346#[cfg(feature = "virtual-clients-draft")]
1347fn inject_vc_derivation_info(
1348    leaf_node_parameters: &mut LeafNodeParameters,
1349    mut resolved_dictionary: AppDataDictionary,
1350    derivation_info_bytes: Vec<u8>,
1351) -> Result<(), CreateCommitError> {
1352    resolved_dictionary.insert(VC_COMPONENT_ID, derivation_info_bytes);
1353    let vc_extension =
1354        Extension::AppDataDictionary(AppDataDictionaryExtension::new(resolved_dictionary));
1355
1356    // Drop any pre-existing AppDataDictionary entry from the caller-
1357    // supplied extension list (we just rebuilt it) and append the merged
1358    // one.
1359    let other_extensions = leaf_node_parameters
1360        .extensions()
1361        .map(|exts| {
1362            exts.iter()
1363                .filter(|ext| !matches!(ext, Extension::AppDataDictionary(_)))
1364                .cloned()
1365                .collect::<Vec<_>>()
1366        })
1367        .unwrap_or_default();
1368    let new_extensions: Vec<Extension> = other_extensions
1369        .into_iter()
1370        .chain(std::iter::once(vc_extension))
1371        .collect();
1372    let extensions = crate::extensions::Extensions::<LeafNode>::from_vec(new_extensions)
1373        .map_err(|_| LibraryError::custom("Failed to build leaf-node extensions"))?;
1374
1375    leaf_node_parameters.set_extensions(extensions);
1376    Ok(())
1377}
1378
1379/// Contains the messages that are produced by committing. The messages can be accessed individually
1380/// using getters or through the [`IntoIterator`] interface.
1381#[derive(Debug, Clone)]
1382pub struct CommitMessageBundle {
1383    version: ProtocolVersion,
1384    commit: MlsMessageOut,
1385    welcome: Option<Welcome>,
1386    group_info: Option<GroupInfo>,
1387}
1388
1389/// The result of a commit with an add proposal. This includes
1390/// - The Commit as an [`MlsMessageOut`]
1391/// - The [`Welcome`] as an [`MlsMessageOut`]
1392/// - Optionally a [`GroupInfo`] as an [`MlsMessageOut`]
1393pub struct WelcomeCommitMessages {
1394    /// The Commit as an [`MlsMessageOut`].
1395    pub commit: MlsMessageOut,
1396
1397    /// The [`Welcome`] as an [`MlsMessageOut`].
1398    pub welcome: MlsMessageOut,
1399
1400    /// Optionally a [`GroupInfo`] as an [`MlsMessageOut`].
1401    pub group_info: Option<MlsMessageOut>,
1402}
1403
1404impl TryFrom<CommitMessageBundle> for WelcomeCommitMessages {
1405    type Error = LibraryError;
1406
1407    fn try_from(value: CommitMessageBundle) -> Result<Self, Self::Error> {
1408        let (commit, welcome_opt, group_info) = value.into_messages();
1409        Ok(Self {
1410            commit,
1411            welcome: welcome_opt.ok_or(LibraryError::custom(
1412                "WelcomeCommitMessages must only be used with commits that produce a welcome.",
1413            ))?,
1414            group_info,
1415        })
1416    }
1417}
1418
1419#[cfg(test)]
1420impl CommitMessageBundle {
1421    pub fn new(
1422        version: ProtocolVersion,
1423        commit: MlsMessageOut,
1424        welcome: Option<Welcome>,
1425        group_info: Option<GroupInfo>,
1426    ) -> Self {
1427        Self {
1428            version,
1429            commit,
1430            welcome,
1431            group_info,
1432        }
1433    }
1434}
1435
1436impl CommitMessageBundle {
1437    // borrowed getters
1438
1439    /// Gets the Commit messsage. For owned version, see [`Self::into_commit`].
1440    pub fn commit(&self) -> &MlsMessageOut {
1441        &self.commit
1442    }
1443
1444    /// Gets the Welcome messsage. Only [`Some`] if new clients have been added in the commit.
1445    /// For owned version, see [`Self::into_welcome`].
1446    pub fn welcome(&self) -> Option<&Welcome> {
1447        self.welcome.as_ref()
1448    }
1449
1450    /// Gets the Welcome messsage. Only [`Some`] if new clients have been added in the commit.
1451    /// Performs a copy of the Welcome. For owned version, see [`Self::into_welcome_msg`].
1452    pub fn to_welcome_msg(&self) -> Option<MlsMessageOut> {
1453        self.welcome
1454            .as_ref()
1455            .map(|welcome| MlsMessageOut::from_welcome(welcome.clone(), self.version))
1456    }
1457
1458    /// Gets the GroupInfo message. Only [`Some`] if new clients have been added or the group
1459    /// configuration has `use_ratchet_tree_extension` set.
1460    /// For owned version, see [`Self::into_group_info`].
1461    pub fn group_info(&self) -> Option<&GroupInfo> {
1462        self.group_info.as_ref()
1463    }
1464
1465    /// Gets all three messages, some of which optional. For owned version, see
1466    /// [`Self::into_contents`].
1467    pub fn contents(&self) -> (&MlsMessageOut, Option<&Welcome>, Option<&GroupInfo>) {
1468        (
1469            &self.commit,
1470            self.welcome.as_ref(),
1471            self.group_info.as_ref(),
1472        )
1473    }
1474
1475    // owned getters
1476    /// Gets the Commit messsage. This method consumes the [`CommitMessageBundle`]. For a borrowed
1477    /// version see [`Self::commit`].
1478    pub fn into_commit(self) -> MlsMessageOut {
1479        self.commit
1480    }
1481
1482    /// Gets the Welcome messsage. Only [`Some`] if new clients have been added in the commit.
1483    /// This method consumes the [`CommitMessageBundle`]. For a borrowed version see
1484    /// [`Self::welcome`].
1485    pub fn into_welcome(self) -> Option<Welcome> {
1486        self.welcome
1487    }
1488
1489    /// Gets the Welcome messsage. Only [`Some`] if new clients have been added in the commit.
1490    /// For a borrowed version, see [`Self::to_welcome_msg`].
1491    pub fn into_welcome_msg(self) -> Option<MlsMessageOut> {
1492        self.welcome
1493            .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version))
1494    }
1495
1496    /// Gets the GroupInfo message. Only [`Some`] if new clients have been added or the group
1497    /// configuration has `use_ratchet_tree_extension` set.
1498    /// This method consumes the [`CommitMessageBundle`]. For a borrowed version see
1499    /// [`Self::group_info`].
1500    pub fn into_group_info(self) -> Option<GroupInfo> {
1501        self.group_info
1502    }
1503
1504    /// Gets the GroupInfo messsage. Only [`Some`] if new clients have been added in the commit.
1505    pub fn into_group_info_msg(self) -> Option<MlsMessageOut> {
1506        self.group_info.map(|group_info| group_info.into())
1507    }
1508
1509    /// Gets all three messages, some of which optional. This method consumes the
1510    /// [`CommitMessageBundle`]. For a borrowed version see [`Self::contents`].
1511    pub fn into_contents(self) -> (MlsMessageOut, Option<Welcome>, Option<GroupInfo>) {
1512        (self.commit, self.welcome, self.group_info)
1513    }
1514
1515    /// Gets all three messages, some of which optional, as [`MlsMessageOut`].
1516    /// This method consumes the [`CommitMessageBundle`].
1517    pub fn into_messages(self) -> (MlsMessageOut, Option<MlsMessageOut>, Option<MlsMessageOut>) {
1518        (
1519            self.commit,
1520            self.welcome
1521                .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version)),
1522            self.group_info.map(|group_info| group_info.into()),
1523        )
1524    }
1525}
1526
1527impl IntoIterator for CommitMessageBundle {
1528    type Item = MlsMessageOut;
1529
1530    type IntoIter = core::iter::Chain<
1531        core::iter::Chain<
1532            core::option::IntoIter<MlsMessageOut>,
1533            core::option::IntoIter<MlsMessageOut>,
1534        >,
1535        core::option::IntoIter<MlsMessageOut>,
1536    >;
1537
1538    fn into_iter(self) -> Self::IntoIter {
1539        let welcome = self.to_welcome_msg();
1540        let group_info = self.group_info.map(|group_info| group_info.into());
1541
1542        Some(self.commit)
1543            .into_iter()
1544            .chain(welcome)
1545            .chain(group_info)
1546    }
1547}