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