Skip to main content

mls_rs/group/
commit.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// Copyright by contributors to this project.
3// SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5use alloc::boxed::Box;
6use alloc::vec;
7use alloc::vec::Vec;
8use core::fmt::Debug;
9use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
10use mls_rs_core::{crypto::SignatureSecretKey, error::IntoAnyError};
11
12use crate::{
13    cipher_suite::CipherSuite,
14    client::MlsError,
15    client_config::ClientConfig,
16    extension::RatchetTreeExt,
17    group::proposal_filter::path_update_required,
18    identity::SigningIdentity,
19    protocol_version::ProtocolVersion,
20    signer::Signable,
21    time::MlsTime,
22    tree_kem::{kem::TreeKem, path_secret::PathSecret, TreeKemPrivate, UpdatePath},
23    ExtensionList, MlsRules,
24};
25
26#[cfg(all(not(mls_build_async), feature = "rayon"))]
27use {crate::iter::ParallelIteratorExt, rayon::prelude::*};
28
29use crate::tree_kem::leaf_node::LeafNode;
30
31#[cfg(not(feature = "private_message"))]
32use crate::WireFormat;
33
34#[cfg(feature = "psk")]
35use crate::{
36    group::{JustPreSharedKeyID, PskGroupId, ResumptionPSKUsage, ResumptionPsk},
37    psk::ExternalPskId,
38};
39
40use super::{
41    confirmation_tag::ConfirmationTag,
42    framing::{Content, MlsMessage, MlsMessagePayload, Sender},
43    key_schedule::{KeySchedule, WelcomeSecret},
44    message_hash::MessageHash,
45    message_processor::MessageProcessor,
46    message_signature::AuthenticatedContent,
47    mls_rules::CommitDirection,
48    proposal::{Proposal, ProposalOrRef},
49    CommitEffect, CommitMessageDescription, EncryptedGroupSecrets, EpochSecrets, ExportedTree,
50    Group, GroupContext, GroupInfo, GroupState, InterimTranscriptHash, NewEpoch,
51    PendingCommitSnapshot, Welcome,
52};
53
54#[cfg(not(feature = "by_ref_proposal"))]
55use super::proposal_cache::prepare_commit;
56
57#[cfg(feature = "custom_proposal")]
58use super::proposal::CustomProposal;
59
60#[derive(Clone, Debug, PartialEq, MlsSize, MlsEncode, MlsDecode)]
61#[cfg_attr(feature = "arbitrary", derive(mls_rs_core::arbitrary::Arbitrary))]
62#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
63pub(crate) struct Commit {
64    pub proposals: Vec<ProposalOrRef>,
65    pub path: Option<UpdatePath>,
66}
67
68#[derive(Clone, PartialEq, Debug, MlsEncode, MlsDecode, MlsSize)]
69pub(crate) struct PendingCommit {
70    pub(crate) state: GroupState,
71    pub(crate) epoch_secrets: EpochSecrets,
72    pub(crate) private_tree: TreeKemPrivate,
73    pub(crate) key_schedule: KeySchedule,
74    pub(crate) signer: SignatureSecretKey,
75
76    pub(crate) output: CommitMessageDescription,
77
78    pub(crate) commit_message_hash: MessageHash,
79}
80
81#[derive(Clone)]
82pub struct CommitSecrets(pub(crate) PendingCommitSnapshot);
83
84impl CommitSecrets {
85    /// Deserialize the commit secrets from bytes
86    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> {
87        Ok(MlsDecode::mls_decode(&mut &*bytes).map(Self)?)
88    }
89
90    /// Serialize the commit secrets to bytes
91    pub fn to_bytes(&self) -> Result<Vec<u8>, MlsError> {
92        Ok(self.0.mls_encode_to_vec()?)
93    }
94}
95
96#[derive(Clone, Debug)]
97#[non_exhaustive]
98/// Result of MLS commit operation using
99/// [`Group::commit`](crate::group::Group::commit) or
100/// [`CommitBuilder::build`](CommitBuilder::build).
101pub struct CommitOutput {
102    /// Commit message to send to other group members.
103    pub commit_message: MlsMessage,
104    /// Welcome messages to send to new group members. If the commit does not add members,
105    /// this list is empty. Otherwise, if [`MlsRules::commit_options`] returns `single_welcome_message`
106    /// set to true, then this list contains a single message sent to all members. Else, the list
107    /// contains one message for each added member. Recipients of each message can be identified using
108    /// [`MlsMessage::key_package_reference`] of their key packages and
109    /// [`MlsMessage::welcome_key_package_references`].
110    pub welcome_messages: Vec<MlsMessage>,
111    /// Ratchet tree that can be sent out of band if
112    /// `ratchet_tree_extension` is not used according to
113    /// [`MlsRules::commit_options`].
114    pub ratchet_tree: Option<ExportedTree<'static>>,
115    /// A group info that can be provided to new members in order to enable external commit
116    /// functionality. This value is set if [`MlsRules::commit_options`] returns
117    /// `allow_external_commit` set to true.
118    pub external_commit_group_info: Option<MlsMessage>,
119    /// Proposals that were received in the prior epoch but not included in the following commit.
120    #[cfg(feature = "by_ref_proposal")]
121    pub unused_proposals: Vec<crate::mls_rules::ProposalInfo<Proposal>>,
122    /// Indicator that the commit contains a path update
123    pub contains_update_path: bool,
124}
125
126impl CommitOutput {
127    /// Commit message to send to other group members.
128    pub fn commit_message(&self) -> &MlsMessage {
129        &self.commit_message
130    }
131
132    /// Welcome message to send to new group members.
133    pub fn welcome_messages(&self) -> &[MlsMessage] {
134        &self.welcome_messages
135    }
136
137    /// Ratchet tree that can be sent out of band if
138    /// `ratchet_tree_extension` is not used according to
139    /// [`MlsRules::commit_options`].
140    pub fn ratchet_tree(&self) -> Option<&ExportedTree<'static>> {
141        self.ratchet_tree.as_ref()
142    }
143
144    /// A group info that can be provided to new members in order to enable external commit
145    /// functionality. This value is set if [`MlsRules::commit_options`] returns
146    /// `allow_external_commit` set to true.
147    pub fn external_commit_group_info(&self) -> Option<&MlsMessage> {
148        self.external_commit_group_info.as_ref()
149    }
150
151    /// Proposals that were received in the prior epoch but not included in the following commit.
152    #[cfg(feature = "by_ref_proposal")]
153    pub fn unused_proposals(&self) -> &[crate::mls_rules::ProposalInfo<Proposal>] {
154        &self.unused_proposals
155    }
156}
157
158/// Build a commit with multiple proposals by-value.
159///
160/// Proposals within a commit can be by-value or by-reference.
161/// Proposals received during the current epoch will be added to the resulting
162/// commit by-reference automatically so long as they pass the rules defined
163/// in the current
164/// [proposal rules](crate::client_builder::ClientBuilder::mls_rules).
165pub struct CommitBuilder<'a, C>
166where
167    C: ClientConfig + Clone,
168{
169    group: &'a mut Group<C>,
170    pub(super) proposals: Vec<Proposal>,
171    authenticated_data: Vec<u8>,
172    group_info_extensions: ExtensionList,
173    new_signer: Option<SignatureSecretKey>,
174    new_signing_identity: Option<SigningIdentity>,
175    new_leaf_node_extensions: Option<ExtensionList>,
176    commit_time: Option<MlsTime>,
177}
178
179impl<'a, C> CommitBuilder<'a, C>
180where
181    C: ClientConfig + Clone,
182{
183    /// Insert an [`AddProposal`](crate::group::proposal::AddProposal) into
184    /// the current commit that is being built.
185    pub fn add_member(mut self, key_package: MlsMessage) -> Result<CommitBuilder<'a, C>, MlsError> {
186        let proposal = self.group.add_proposal(key_package)?;
187        self.proposals.push(proposal);
188        Ok(self)
189    }
190
191    /// Set group info extensions that will be inserted into the resulting
192    /// [welcome messages](CommitOutput::welcome_messages) for new members.
193    ///
194    /// Group info extensions that are transmitted as part of a welcome message
195    /// are encrypted along with other private values.
196    ///
197    /// These extensions can be retrieved as part of
198    /// [`NewMemberInfo`](crate::group::NewMemberInfo) that is returned
199    /// by joining the group via
200    /// [`Client::join_group`](crate::Client::join_group).
201    pub fn set_group_info_ext(self, extensions: ExtensionList) -> Self {
202        Self {
203            group_info_extensions: extensions,
204            ..self
205        }
206    }
207
208    /// Insert a [`RemoveProposal`](crate::group::proposal::RemoveProposal) into
209    /// the current commit that is being built.
210    pub fn remove_member(mut self, index: u32) -> Result<Self, MlsError> {
211        let proposal = self.group.remove_proposal(index)?;
212        self.proposals.push(proposal);
213        Ok(self)
214    }
215
216    /// Insert a
217    /// [`GroupContextExtensions`](crate::group::proposal::Proposal::GroupContextExtensions)
218    /// into the current commit that is being built.
219    pub fn set_group_context_ext(mut self, extensions: ExtensionList) -> Result<Self, MlsError> {
220        let proposal = self.group.group_context_extensions_proposal(extensions);
221        self.proposals.push(proposal);
222        Ok(self)
223    }
224
225    /// Insert a
226    /// [`PreSharedKeyProposal`](crate::group::proposal::PreSharedKeyProposal) with
227    /// an external PSK into the current commit that is being built.
228    #[cfg(feature = "psk")]
229    pub fn add_external_psk(mut self, psk_id: ExternalPskId) -> Result<Self, MlsError> {
230        let key_id = JustPreSharedKeyID::External(psk_id);
231        let proposal = self.group.psk_proposal(key_id)?;
232        self.proposals.push(proposal);
233        Ok(self)
234    }
235
236    /// Insert a
237    /// [`PreSharedKeyProposal`](crate::group::proposal::PreSharedKeyProposal) with
238    /// a resumption PSK into the current commit that is being built.
239    #[cfg(feature = "psk")]
240    pub fn add_resumption_psk(mut self, psk_epoch: u64) -> Result<Self, MlsError> {
241        let psk_id = ResumptionPsk {
242            psk_epoch,
243            usage: ResumptionPSKUsage::Application,
244            psk_group_id: PskGroupId(self.group.group_id().to_vec()),
245        };
246
247        let key_id = JustPreSharedKeyID::Resumption(psk_id);
248        let proposal = self.group.psk_proposal(key_id)?;
249        self.proposals.push(proposal);
250        Ok(self)
251    }
252
253    /// Insert a [`ReInitProposal`](crate::group::proposal::ReInitProposal) into
254    /// the current commit that is being built.
255    pub fn reinit(
256        mut self,
257        group_id: Option<Vec<u8>>,
258        version: ProtocolVersion,
259        cipher_suite: CipherSuite,
260        extensions: ExtensionList,
261    ) -> Result<Self, MlsError> {
262        let proposal = self
263            .group
264            .reinit_proposal(group_id, version, cipher_suite, extensions)?;
265
266        self.proposals.push(proposal);
267        Ok(self)
268    }
269
270    /// Insert a [`CustomProposal`](crate::group::proposal::CustomProposal) into
271    /// the current commit that is being built.
272    #[cfg(feature = "custom_proposal")]
273    pub fn custom_proposal(mut self, proposal: CustomProposal) -> Self {
274        self.proposals.push(Proposal::Custom(proposal));
275        self
276    }
277
278    /// Insert a proposal that was previously constructed such as when a
279    /// proposal is returned from
280    /// [`NewEpoch::unused_proposals`](super::NewEpoch::unused_proposals).
281    pub fn raw_proposal(mut self, proposal: Proposal) -> Self {
282        self.proposals.push(proposal);
283        self
284    }
285
286    /// Insert proposals that were previously constructed such as when a
287    /// proposal is returned from
288    /// [`NewEpoch::unused_proposals`](super::NewEpoch::unused_proposals).
289    pub fn raw_proposals(mut self, mut proposals: Vec<Proposal>) -> Self {
290        self.proposals.append(&mut proposals);
291        self
292    }
293
294    /// Add additional authenticated data to the commit.
295    ///
296    /// # Warning
297    ///
298    /// The data provided here is always sent unencrypted.
299    pub fn authenticated_data(self, authenticated_data: Vec<u8>) -> Self {
300        Self {
301            authenticated_data,
302            ..self
303        }
304    }
305
306    /// Change the committer's signing identity as part of making this commit.
307    /// This will only succeed if the [`IdentityProvider`](crate::IdentityProvider)
308    /// in use by the group considers the credential inside this signing_identity
309    /// [valid](crate::IdentityProvider::validate_member)
310    /// and results in the same
311    /// [identity](crate::IdentityProvider::identity)
312    /// being used.
313    pub fn set_new_signing_identity(
314        self,
315        signer: SignatureSecretKey,
316        signing_identity: SigningIdentity,
317    ) -> Self {
318        Self {
319            new_signer: Some(signer),
320            new_signing_identity: Some(signing_identity),
321            ..self
322        }
323    }
324
325    /// Change the committer's leaf node extensions as part of making this commit.
326    pub fn set_leaf_node_extensions(self, new_leaf_node_extensions: ExtensionList) -> Self {
327        Self {
328            new_leaf_node_extensions: Some(new_leaf_node_extensions),
329            ..self
330        }
331    }
332
333    /// Add a time to associate with the commit creation.
334    pub fn commit_time(self, commit_time: MlsTime) -> Self {
335        Self {
336            commit_time: Some(commit_time),
337            ..self
338        }
339    }
340
341    /// Finalize the commit to send.
342    ///
343    /// # Errors
344    ///
345    /// This function will return an error if any of the proposals provided
346    /// are not contextually valid according to the rules defined by the
347    /// MLS RFC, or if they do not pass the custom rules defined by the current
348    /// [proposal rules](crate::client_builder::ClientBuilder::mls_rules).
349    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
350    pub async fn build(self) -> Result<CommitOutput, MlsError> {
351        let (output, pending_commit) = self
352            .group
353            .commit_internal(
354                self.proposals,
355                None,
356                self.authenticated_data,
357                self.group_info_extensions,
358                self.new_signer,
359                self.new_signing_identity,
360                self.new_leaf_node_extensions,
361                self.commit_time,
362            )
363            .await?;
364
365        self.group.pending_commit = pending_commit.try_into()?;
366
367        Ok(output)
368    }
369
370    /// The same function as `GroupBuilder::build` except the secrets generated
371    /// for the commit are outputted instead of being cached internally.
372    ///
373    /// A detached commit can be applied using `Group::apply_detached_commit`.
374    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
375    pub async fn build_detached(self) -> Result<(CommitOutput, CommitSecrets), MlsError> {
376        let (output, pending_commit) = self
377            .group
378            .commit_internal(
379                self.proposals,
380                None,
381                self.authenticated_data,
382                self.group_info_extensions,
383                self.new_signer,
384                self.new_signing_identity,
385                self.new_leaf_node_extensions,
386                self.commit_time,
387            )
388            .await?;
389
390        Ok((
391            output,
392            CommitSecrets(PendingCommitSnapshot::PendingCommit(
393                pending_commit.mls_encode_to_vec()?,
394            )),
395        ))
396    }
397}
398
399impl<C> Group<C>
400where
401    C: ClientConfig + Clone,
402{
403    /// Perform a commit of received proposals.
404    ///
405    /// This function is the equivalent of [`Group::commit_builder`] immediately
406    /// followed by [`CommitBuilder::build`]. Any received proposals since the
407    /// last commit will be included in the resulting message by-reference.
408    ///
409    /// Data provided in the `authenticated_data` field will be placed into
410    /// the resulting commit message unencrypted.
411    ///
412    /// # Pending Commits
413    ///
414    /// When a commit is created, it is not applied immediately in order to
415    /// allow for the resolution of conflicts when multiple members of a group
416    /// attempt to make commits at the same time. For example, a central relay
417    /// can be used to decide which commit should be accepted by the group by
418    /// determining a consistent view of commit packet order for all clients.
419    ///
420    /// Pending commits are stored internally as part of the group's state
421    /// so they do not need to be tracked outside of this library. Any commit
422    /// message that is processed before calling [Group::apply_pending_commit]
423    /// will clear the currently pending commit.
424    ///
425    /// # Empty Commits
426    ///
427    /// Sending a commit that contains no proposals is a valid operation
428    /// within the MLS protocol. It is useful for providing stronger forward
429    /// secrecy and post-compromise security, especially for long running
430    /// groups when group membership does not change often.
431    ///
432    /// # Path Updates
433    ///
434    /// Path updates provide forward secrecy and post-compromise security
435    /// within the MLS protocol.
436    /// The `path_required` option returned by [`MlsRules::commit_options`](`crate::MlsRules::commit_options`)
437    /// controls the ability of a group to send a commit without a path update.
438    /// An update path will automatically be sent if there are no proposals
439    /// in the commit, or if any proposal other than
440    /// [`Add`](crate::group::proposal::Proposal::Add),
441    /// [`Psk`](crate::group::proposal::Proposal::Psk),
442    /// or [`ReInit`](crate::group::proposal::Proposal::ReInit) are part of the commit.
443    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
444    pub async fn commit(&mut self, authenticated_data: Vec<u8>) -> Result<CommitOutput, MlsError> {
445        self.commit_builder()
446            .authenticated_data(authenticated_data)
447            .build()
448            .await
449    }
450
451    /// The same function as `Group::commit` except the secrets generated
452    /// for the commit are outputted instead of being cached internally.
453    ///
454    /// A detached commit can be applied using `Group::apply_detached_commit`.
455    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
456    pub async fn commit_detached(
457        &mut self,
458        authenticated_data: Vec<u8>,
459    ) -> Result<(CommitOutput, CommitSecrets), MlsError> {
460        self.commit_builder()
461            .authenticated_data(authenticated_data)
462            .build_detached()
463            .await
464    }
465
466    /// Create a new commit builder that can include proposals
467    /// by-value.
468    pub fn commit_builder(&mut self) -> CommitBuilder<'_, C> {
469        CommitBuilder {
470            group: self,
471            proposals: Default::default(),
472            authenticated_data: Default::default(),
473            group_info_extensions: Default::default(),
474            new_signer: Default::default(),
475            new_signing_identity: Default::default(),
476            new_leaf_node_extensions: Default::default(),
477            commit_time: None,
478        }
479    }
480
481    /// Returns commit and optional [`MlsMessage`] containing a welcome message
482    /// for newly added members.
483    #[allow(clippy::too_many_arguments)]
484    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
485    pub(super) async fn commit_internal(
486        &mut self,
487        proposals: Vec<Proposal>,
488        external_leaf: Option<&LeafNode>,
489        authenticated_data: Vec<u8>,
490        mut welcome_group_info_extensions: ExtensionList,
491        new_signer: Option<SignatureSecretKey>,
492        new_signing_identity: Option<SigningIdentity>,
493        new_leaf_node_extensions: Option<ExtensionList>,
494        commit_time: Option<MlsTime>,
495    ) -> Result<(CommitOutput, PendingCommit), MlsError> {
496        if !self.pending_commit.is_none() {
497            return Err(MlsError::ExistingPendingCommit);
498        }
499
500        if self.state.pending_reinit.is_some() {
501            return Err(MlsError::GroupUsedAfterReInit);
502        }
503
504        let mls_rules = self.config.mls_rules();
505
506        let is_external = external_leaf.is_some();
507
508        // Construct an initial Commit object with the proposals field populated from Proposals
509        // received during the current epoch, and an empty path field. Add passed in proposals
510        // by value
511        let sender = if is_external {
512            Sender::NewMemberCommit
513        } else {
514            Sender::Member(*self.private_tree.self_index)
515        };
516
517        let new_signer = new_signer.unwrap_or_else(|| self.signer.clone());
518        let old_signer = &self.signer;
519
520        #[cfg(feature = "std")]
521        let time = Some(crate::time::MlsTime::now());
522
523        #[cfg(not(feature = "std"))]
524        let time = None;
525
526        let time = if commit_time.is_some() {
527            commit_time
528        } else {
529            time
530        };
531
532        #[cfg(feature = "by_ref_proposal")]
533        let proposals = self.state.proposals.prepare_commit(sender, proposals);
534
535        #[cfg(not(feature = "by_ref_proposal"))]
536        let proposals = prepare_commit(sender, proposals);
537
538        let mut provisional_state = self
539            .state
540            .apply_resolved(
541                sender,
542                proposals,
543                external_leaf,
544                &self.config.identity_provider(),
545                &self.cipher_suite_provider,
546                &self.config.secret_store(),
547                &mls_rules,
548                time,
549                CommitDirection::Send,
550            )
551            .await?;
552
553        let (mut provisional_private_tree, _) =
554            self.provisional_private_tree(&provisional_state)?;
555
556        if is_external {
557            provisional_private_tree.self_index = provisional_state
558                .external_init_index
559                .ok_or(MlsError::ExternalCommitMissingExternalInit)?;
560
561            self.private_tree.self_index = provisional_private_tree.self_index;
562        }
563
564        // Decide whether to populate the path field: If the path field is required based on the
565        // proposals that are in the commit (see above), then it MUST be populated. Otherwise, the
566        // sender MAY omit the path field at its discretion.
567        let commit_options = mls_rules
568            .commit_options(
569                &provisional_state.public_tree.roster(),
570                &provisional_state.group_context,
571                &provisional_state.applied_proposals,
572            )
573            .map_err(|e| MlsError::MlsRulesError(e.into_any_error()))?;
574
575        let perform_path_update = commit_options.path_required
576            || path_update_required(&provisional_state.applied_proposals, &mls_rules);
577
578        let (update_path, path_secrets, commit_secret) = if perform_path_update {
579            // If populating the path field: Create an UpdatePath using the new tree. Any new
580            // member (from an add proposal) MUST be excluded from the resolution during the
581            // computation of the UpdatePath. The GroupContext for this operation uses the
582            // group_id, epoch, tree_hash, and confirmed_transcript_hash values in the initial
583            // GroupContext object. The leaf_key_package for this UpdatePath must have a
584            // parent_hash extension.
585
586            let new_leaf_node_extensions =
587                new_leaf_node_extensions.or(external_leaf.map(|ln| ln.ungreased_extensions()));
588
589            let new_leaf_node_extensions = match new_leaf_node_extensions {
590                Some(extensions) => extensions,
591                // If we are not setting new extensions and this is not an external leaf then the current node MUST exist.
592                None => self.current_user_leaf_node()?.ungreased_extensions(),
593            };
594
595            #[cfg(feature = "tree_index")]
596            let old_committer_leaf = provisional_state
597                .public_tree
598                .get_leaf_node(provisional_private_tree.self_index)?
599                .clone();
600
601            let encap_gen = TreeKem::new(
602                &mut provisional_state.public_tree,
603                &mut provisional_private_tree,
604            )
605            .encap(
606                &mut provisional_state.group_context,
607                &provisional_state.indexes_of_added_kpkgs,
608                &new_signer,
609                Some(self.config.leaf_properties(new_leaf_node_extensions)),
610                new_signing_identity,
611                &self.cipher_suite_provider,
612                #[cfg(test)]
613                &self.commit_modifiers,
614            )
615            .await?;
616
617            provisional_state
618                .public_tree
619                .update_committer_leaf(
620                    &self.config.identity_provider(),
621                    &provisional_state.group_context.extensions,
622                    provisional_private_tree.self_index,
623                    #[cfg(feature = "tree_index")]
624                    &old_committer_leaf,
625                    #[cfg(test)]
626                    !self.commit_modifiers.skip_committer_self_update_validation,
627                )
628                .await?;
629
630            (
631                Some(encap_gen.update_path),
632                Some(encap_gen.path_secrets),
633                encap_gen.commit_secret,
634            )
635        } else {
636            // Update the tree hash, since it was not updated by encap.
637            provisional_state
638                .public_tree
639                .update_hashes(
640                    &[provisional_private_tree.self_index],
641                    &self.cipher_suite_provider,
642                )
643                .await?;
644
645            provisional_state.group_context.tree_hash = provisional_state
646                .public_tree
647                .tree_hash(&self.cipher_suite_provider)
648                .await?;
649
650            (None, None, PathSecret::empty(&self.cipher_suite_provider))
651        };
652
653        #[cfg(feature = "psk")]
654        let (psk_secret, psks) = self
655            .get_psk(&provisional_state.applied_proposals.psks)
656            .await?;
657
658        #[cfg(not(feature = "psk"))]
659        let psk_secret = self.get_psk();
660
661        let added_key_pkgs: Vec<_> = provisional_state
662            .applied_proposals
663            .additions
664            .iter()
665            .map(|info| info.proposal.key_package.clone())
666            .collect();
667
668        let commit = Commit {
669            proposals: provisional_state.applied_proposals.proposals_or_refs(),
670            path: update_path,
671        };
672
673        let mut auth_content = AuthenticatedContent::new_signed(
674            &self.cipher_suite_provider,
675            self.context(),
676            sender,
677            Content::Commit(Box::new(commit)),
678            old_signer,
679            #[cfg(feature = "private_message")]
680            self.encryption_options()?.control_wire_format(sender),
681            #[cfg(not(feature = "private_message"))]
682            WireFormat::PublicMessage,
683            authenticated_data,
684        )
685        .await?;
686
687        // Use the signature, the commit_secret and the psk_secret to advance the key schedule and
688        // compute the confirmation_tag value in the MlsPlaintext.
689        let confirmed_transcript_hash = super::transcript_hash::create(
690            self.cipher_suite_provider(),
691            &self.state.interim_transcript_hash,
692            &auth_content,
693        )
694        .await?;
695
696        provisional_state.group_context.confirmed_transcript_hash = confirmed_transcript_hash;
697
698        let key_schedule_result = KeySchedule::from_key_schedule(
699            &self.key_schedule,
700            &commit_secret,
701            &provisional_state.group_context,
702            #[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
703            provisional_state.public_tree.total_leaf_count(),
704            &psk_secret,
705            &self.cipher_suite_provider,
706        )
707        .await?;
708
709        let confirmation_tag = ConfirmationTag::create(
710            &key_schedule_result.confirmation_key,
711            &provisional_state.group_context.confirmed_transcript_hash,
712            &self.cipher_suite_provider,
713        )
714        .await?;
715
716        let interim_transcript_hash = InterimTranscriptHash::create(
717            self.cipher_suite_provider(),
718            &provisional_state.group_context.confirmed_transcript_hash,
719            &confirmation_tag,
720        )
721        .await?;
722
723        auth_content.auth.confirmation_tag = Some(confirmation_tag.clone());
724
725        let ratchet_tree_ext = commit_options
726            .ratchet_tree_extension
727            .then(|| RatchetTreeExt {
728                tree_data: ExportedTree::new(provisional_state.public_tree.nodes.clone()),
729            });
730
731        // Generate external commit group info if required by commit_options
732        let external_commit_group_info = match commit_options.allow_external_commit {
733            true => {
734                let mut extensions = ExtensionList::new();
735
736                extensions.set_from({
737                    key_schedule_result
738                        .key_schedule
739                        .get_external_key_pair_ext(&self.cipher_suite_provider)
740                        .await?
741                })?;
742
743                if let Some(ref ratchet_tree_ext) = ratchet_tree_ext {
744                    if !commit_options.always_out_of_band_ratchet_tree {
745                        extensions.set_from(ratchet_tree_ext.clone())?;
746                    }
747                }
748
749                let info = self
750                    .make_group_info(
751                        &provisional_state.group_context,
752                        extensions,
753                        &confirmation_tag,
754                        &new_signer,
755                    )
756                    .await?;
757
758                let msg =
759                    MlsMessage::new(self.protocol_version(), MlsMessagePayload::GroupInfo(info));
760
761                Some(msg)
762            }
763            false => None,
764        };
765
766        // Build the group info that will be placed into the welcome messages.
767        // Add the ratchet tree extension if necessary
768        if let Some(ratchet_tree_ext) = ratchet_tree_ext {
769            welcome_group_info_extensions.set_from(ratchet_tree_ext)?;
770        }
771
772        let welcome_group_info = self
773            .make_group_info(
774                &provisional_state.group_context,
775                welcome_group_info_extensions,
776                &confirmation_tag,
777                &new_signer,
778            )
779            .await?;
780
781        // Encrypt the GroupInfo using the key and nonce derived from the joiner_secret for
782        // the new epoch
783        let welcome_secret = WelcomeSecret::from_joiner_secret(
784            &self.cipher_suite_provider,
785            &key_schedule_result.joiner_secret,
786            &psk_secret,
787        )
788        .await?;
789
790        let encrypted_group_info = welcome_secret
791            .encrypt(&welcome_group_info.mls_encode_to_vec()?)
792            .await?;
793
794        // Encrypt path secrets and joiner secret to new members
795        let path_secrets = path_secrets.as_ref();
796
797        #[cfg(not(any(mls_build_async, not(feature = "rayon"))))]
798        let encrypted_path_secrets: Vec<_> = added_key_pkgs
799            .into_par_iter()
800            .zip(&provisional_state.indexes_of_added_kpkgs)
801            .map(|(key_package, leaf_index)| {
802                self.encrypt_group_secrets(
803                    &key_package,
804                    *leaf_index,
805                    &key_schedule_result.joiner_secret,
806                    path_secrets,
807                    #[cfg(feature = "psk")]
808                    psks.clone(),
809                    &encrypted_group_info,
810                )
811            })
812            .try_collect()?;
813
814        #[cfg(any(mls_build_async, not(feature = "rayon")))]
815        let encrypted_path_secrets = {
816            let mut secrets = Vec::new();
817
818            for (key_package, leaf_index) in added_key_pkgs
819                .into_iter()
820                .zip(&provisional_state.indexes_of_added_kpkgs)
821            {
822                secrets.push(
823                    self.encrypt_group_secrets(
824                        &key_package,
825                        *leaf_index,
826                        &key_schedule_result.joiner_secret,
827                        path_secrets,
828                        #[cfg(feature = "psk")]
829                        psks.clone(),
830                        &encrypted_group_info,
831                    )
832                    .await?,
833                );
834            }
835
836            secrets
837        };
838
839        let welcome_messages =
840            if commit_options.single_welcome_message && !encrypted_path_secrets.is_empty() {
841                vec![self.make_welcome_message(encrypted_path_secrets, encrypted_group_info)]
842            } else {
843                encrypted_path_secrets
844                    .into_iter()
845                    .map(|s| self.make_welcome_message(vec![s], encrypted_group_info.clone()))
846                    .collect()
847            };
848
849        let commit_message = self.format_for_wire(auth_content.clone()).await?;
850
851        // TODO is it necessary to clone the tree here? or can we just output serialized bytes?
852        let ratchet_tree = (!commit_options.ratchet_tree_extension
853            || commit_options.always_out_of_band_ratchet_tree)
854            .then(|| ExportedTree::new(provisional_state.public_tree.nodes.clone()));
855
856        let pending_reinit = provisional_state
857            .applied_proposals
858            .reinitializations
859            .first();
860
861        let pending_commit = PendingCommit {
862            output: CommitMessageDescription {
863                is_external: matches!(auth_content.content.sender, Sender::NewMemberCommit),
864                authenticated_data: auth_content.content.authenticated_data,
865                committer: *provisional_private_tree.self_index,
866                effect: match pending_reinit {
867                    Some(r) => CommitEffect::ReInit(r.clone()),
868                    None => CommitEffect::NewEpoch(
869                        NewEpoch::new(self.state.clone(), &provisional_state).into(),
870                    ),
871                },
872            },
873
874            state: GroupState {
875                #[cfg(feature = "by_ref_proposal")]
876                proposals: crate::group::ProposalCache::new(
877                    self.protocol_version(),
878                    self.group_id().to_vec(),
879                ),
880                context: provisional_state.group_context,
881                public_tree: provisional_state.public_tree,
882                interim_transcript_hash,
883                pending_reinit: pending_reinit.map(|r| r.proposal.clone()),
884                confirmation_tag,
885            },
886
887            commit_message_hash: MessageHash::compute(&self.cipher_suite_provider, &commit_message)
888                .await?,
889            signer: new_signer,
890            epoch_secrets: key_schedule_result.epoch_secrets,
891            key_schedule: key_schedule_result.key_schedule,
892
893            private_tree: provisional_private_tree,
894        };
895
896        let output = CommitOutput {
897            commit_message,
898            welcome_messages,
899            ratchet_tree,
900            external_commit_group_info,
901            contains_update_path: perform_path_update,
902            #[cfg(feature = "by_ref_proposal")]
903            unused_proposals: provisional_state.unused_proposals,
904        };
905
906        Ok((output, pending_commit))
907    }
908
909    // Construct a GroupInfo reflecting the new state
910    // Group ID, epoch, tree, and confirmed transcript hash from the new state
911    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
912    async fn make_group_info(
913        &self,
914        group_context: &GroupContext,
915        extensions: ExtensionList,
916        confirmation_tag: &ConfirmationTag,
917        signer: &SignatureSecretKey,
918    ) -> Result<GroupInfo, MlsError> {
919        let mut group_info = GroupInfo {
920            group_context: group_context.clone(),
921            extensions,
922            confirmation_tag: confirmation_tag.clone(), // The confirmation_tag from the MlsPlaintext object
923            signer: self.current_member_leaf_index(),
924            signature: vec![],
925        };
926
927        group_info.grease(self.cipher_suite_provider())?;
928
929        // Sign the GroupInfo using the member's private signing key
930        group_info
931            .sign(&self.cipher_suite_provider, signer, &())
932            .await?;
933
934        Ok(group_info)
935    }
936
937    fn make_welcome_message(
938        &self,
939        secrets: Vec<EncryptedGroupSecrets>,
940        encrypted_group_info: Vec<u8>,
941    ) -> MlsMessage {
942        MlsMessage::new(
943            self.context().protocol_version,
944            MlsMessagePayload::Welcome(Welcome {
945                cipher_suite: self.context().cipher_suite,
946                secrets,
947                encrypted_group_info,
948            }),
949        )
950    }
951}
952
953#[cfg(test)]
954pub(crate) mod test_utils {
955    use alloc::vec::Vec;
956
957    use crate::{
958        crypto::SignatureSecretKey,
959        tree_kem::{leaf_node::LeafNode, TreeKemPublic, UpdatePathNode},
960    };
961
962    #[derive(Copy, Clone, Debug)]
963    pub struct CommitModifiers {
964        pub modify_leaf: fn(&mut LeafNode, &SignatureSecretKey) -> Option<SignatureSecretKey>,
965        pub modify_tree: fn(&mut TreeKemPublic),
966        pub modify_path: fn(Vec<UpdatePathNode>) -> Vec<UpdatePathNode>,
967        pub skip_committer_self_update_validation: bool,
968    }
969
970    impl Default for CommitModifiers {
971        fn default() -> Self {
972            Self {
973                modify_leaf: |_, _| None,
974                modify_tree: |_| (),
975                modify_path: |a| a,
976                skip_committer_self_update_validation: false,
977            }
978        }
979    }
980}
981
982#[cfg(test)]
983mod tests {
984    use mls_rs_core::{
985        error::IntoAnyError,
986        extension::ExtensionType,
987        identity::{CredentialType, IdentityProvider, MemberValidationContext},
988        time::MlsTime,
989    };
990
991    use crate::extension::RequiredCapabilitiesExt;
992    use crate::{
993        client::test_utils::{test_client_with_key_pkg, TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION},
994        client_builder::{
995            test_utils::TestClientConfig, BaseConfig, ClientBuilder, WithCryptoProvider,
996            WithIdentityProvider,
997        },
998        client_config::ClientConfig,
999        crypto::test_utils::TestCryptoProvider,
1000        extension::test_utils::{TestExtension, TEST_EXTENSION_TYPE},
1001        group::test_utils::{test_group, test_group_custom},
1002        group::{
1003            proposal::ProposalType,
1004            test_utils::{test_group_custom_config, test_n_member_group},
1005        },
1006        identity::test_utils::get_test_signing_identity,
1007        identity::{basic::BasicIdentityProvider, test_utils::get_test_basic_credential},
1008        key_package::test_utils::test_key_package_message,
1009        mls_rules::CommitOptions,
1010        Client,
1011    };
1012
1013    #[cfg(feature = "by_ref_proposal")]
1014    use crate::crypto::test_utils::test_cipher_suite_provider;
1015    #[cfg(feature = "by_ref_proposal")]
1016    use crate::extension::ExternalSendersExt;
1017    #[cfg(feature = "by_ref_proposal")]
1018    use crate::group::mls_rules::DefaultMlsRules;
1019
1020    #[cfg(feature = "psk")]
1021    use crate::{
1022        group::proposal::PreSharedKeyProposal,
1023        psk::{JustPreSharedKeyID, PreSharedKey, PreSharedKeyID},
1024    };
1025
1026    use super::*;
1027
1028    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1029    async fn test_commit_builder_group() -> Group<TestClientConfig> {
1030        test_group_custom_config(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, |b| {
1031            b.custom_proposal_type(ProposalType::from(42))
1032                .extension_type(TEST_EXTENSION_TYPE.into())
1033        })
1034        .await
1035        .group
1036    }
1037
1038    fn assert_commit_builder_output<C: ClientConfig>(
1039        group: Group<C>,
1040        mut commit_output: CommitOutput,
1041        expected: Vec<Proposal>,
1042        welcome_count: usize,
1043    ) {
1044        let plaintext = commit_output.commit_message.into_plaintext().unwrap();
1045
1046        let commit_data = match plaintext.content.content {
1047            Content::Commit(commit) => commit,
1048            #[cfg(any(feature = "private_message", feature = "by_ref_proposal"))]
1049            _ => panic!("Found non-commit data"),
1050        };
1051
1052        assert_eq!(commit_data.proposals.len(), expected.len());
1053
1054        commit_data.proposals.into_iter().for_each(|proposal| {
1055            let proposal = match proposal {
1056                ProposalOrRef::Proposal(p) => p,
1057                #[cfg(feature = "by_ref_proposal")]
1058                ProposalOrRef::Reference(_) => panic!("found proposal reference"),
1059            };
1060
1061            #[cfg(feature = "psk")]
1062            if let Some(psk_id) = match proposal.as_ref() {
1063                Proposal::Psk(PreSharedKeyProposal { psk: PreSharedKeyID { key_id: JustPreSharedKeyID::External(psk_id), .. },}) => Some(psk_id),
1064                _ => None,
1065            } {
1066                let found = expected.iter().any(|item| matches!(item, Proposal::Psk(PreSharedKeyProposal { psk: PreSharedKeyID { key_id: JustPreSharedKeyID::External(id), .. }}) if id == psk_id));
1067
1068                assert!(found)
1069            } else {
1070                assert!(expected.contains(&proposal));
1071            }
1072
1073            #[cfg(not(feature = "psk"))]
1074            assert!(expected.contains(&proposal));
1075        });
1076
1077        if welcome_count > 0 {
1078            let welcome_msg = commit_output.welcome_messages.pop().unwrap();
1079
1080            assert_eq!(welcome_msg.version, group.state.context.protocol_version);
1081
1082            let welcome_msg = welcome_msg.into_welcome().unwrap();
1083
1084            assert_eq!(welcome_msg.cipher_suite, group.state.context.cipher_suite);
1085            assert_eq!(welcome_msg.secrets.len(), welcome_count);
1086        } else {
1087            assert!(commit_output.welcome_messages.is_empty());
1088        }
1089    }
1090
1091    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1092    async fn test_commit_builder_add() {
1093        let mut group = test_commit_builder_group().await;
1094
1095        let test_key_package =
1096            test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "alice").await;
1097
1098        let commit_output = group
1099            .commit_builder()
1100            .add_member(test_key_package.clone())
1101            .unwrap()
1102            .build()
1103            .await
1104            .unwrap();
1105
1106        let expected_add = group.add_proposal(test_key_package).unwrap();
1107
1108        assert_commit_builder_output(group, commit_output, vec![expected_add], 1)
1109    }
1110
1111    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1112    async fn test_commit_builder_add_with_ext() {
1113        let mut group = test_commit_builder_group().await;
1114
1115        let (bob_client, bob_key_package) =
1116            test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
1117
1118        let ext = TestExtension { foo: 42 };
1119        let mut extension_list = ExtensionList::default();
1120        extension_list.set_from(ext.clone()).unwrap();
1121
1122        let welcome_message = group
1123            .commit_builder()
1124            .add_member(bob_key_package)
1125            .unwrap()
1126            .set_group_info_ext(extension_list)
1127            .build()
1128            .await
1129            .unwrap()
1130            .welcome_messages
1131            .remove(0);
1132
1133        let (_, context) = bob_client
1134            .join_group(None, &welcome_message, None)
1135            .await
1136            .unwrap();
1137
1138        assert_eq!(
1139            context
1140                .group_info_extensions
1141                .get_as::<TestExtension>()
1142                .unwrap()
1143                .unwrap(),
1144            ext
1145        );
1146    }
1147
1148    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1149    async fn test_commit_builder_remove() {
1150        let mut group = test_commit_builder_group().await;
1151        let test_key_package =
1152            test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "alice").await;
1153
1154        group
1155            .commit_builder()
1156            .add_member(test_key_package)
1157            .unwrap()
1158            .build()
1159            .await
1160            .unwrap();
1161
1162        group.apply_pending_commit().await.unwrap();
1163
1164        let commit_output = group
1165            .commit_builder()
1166            .remove_member(1)
1167            .unwrap()
1168            .build()
1169            .await
1170            .unwrap();
1171
1172        let expected_remove = group.remove_proposal(1).unwrap();
1173
1174        assert_commit_builder_output(group, commit_output, vec![expected_remove], 0);
1175    }
1176
1177    #[cfg(feature = "psk")]
1178    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1179    async fn test_commit_builder_psk() {
1180        let mut group = test_commit_builder_group().await;
1181        let test_psk = ExternalPskId::new(vec![1]);
1182
1183        group
1184            .config
1185            .secret_store()
1186            .insert(test_psk.clone(), PreSharedKey::from(vec![1]));
1187
1188        let commit_output = group
1189            .commit_builder()
1190            .add_external_psk(test_psk.clone())
1191            .unwrap()
1192            .build()
1193            .await
1194            .unwrap();
1195
1196        let key_id = JustPreSharedKeyID::External(test_psk);
1197        let expected_psk = group.psk_proposal(key_id).unwrap();
1198
1199        assert_commit_builder_output(group, commit_output, vec![expected_psk], 0)
1200    }
1201
1202    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1203    async fn test_commit_builder_group_context_ext() {
1204        let mut group = test_commit_builder_group().await;
1205        let mut test_ext = ExtensionList::default();
1206        test_ext
1207            .set_from(RequiredCapabilitiesExt::default())
1208            .unwrap();
1209
1210        let commit_output = group
1211            .commit_builder()
1212            .set_group_context_ext(test_ext.clone())
1213            .unwrap()
1214            .build()
1215            .await
1216            .unwrap();
1217
1218        let expected_ext = group.group_context_extensions_proposal(test_ext);
1219
1220        assert_commit_builder_output(group, commit_output, vec![expected_ext], 0);
1221    }
1222
1223    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1224    async fn test_commit_builder_reinit() {
1225        let mut group = test_commit_builder_group().await;
1226        let test_group_id = "foo".as_bytes().to_vec();
1227        let test_cipher_suite = TEST_CIPHER_SUITE;
1228        let test_protocol_version = TEST_PROTOCOL_VERSION;
1229        let mut test_ext = ExtensionList::default();
1230
1231        test_ext
1232            .set_from(RequiredCapabilitiesExt::default())
1233            .unwrap();
1234
1235        let commit_output = group
1236            .commit_builder()
1237            .reinit(
1238                Some(test_group_id.clone()),
1239                test_protocol_version,
1240                test_cipher_suite,
1241                test_ext.clone(),
1242            )
1243            .unwrap()
1244            .build()
1245            .await
1246            .unwrap();
1247
1248        let expected_reinit = group
1249            .reinit_proposal(
1250                Some(test_group_id),
1251                test_protocol_version,
1252                test_cipher_suite,
1253                test_ext,
1254            )
1255            .unwrap();
1256
1257        assert_commit_builder_output(group, commit_output, vec![expected_reinit], 0);
1258    }
1259
1260    #[cfg(feature = "custom_proposal")]
1261    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1262    async fn test_commit_builder_custom_proposal() {
1263        let mut group = test_commit_builder_group().await;
1264
1265        let proposal = CustomProposal::new(42.into(), vec![0, 1]);
1266
1267        let commit_output = group
1268            .commit_builder()
1269            .custom_proposal(proposal.clone())
1270            .build()
1271            .await
1272            .unwrap();
1273
1274        assert_commit_builder_output(group, commit_output, vec![Proposal::Custom(proposal)], 0);
1275    }
1276
1277    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1278    async fn test_commit_builder_chaining() {
1279        let mut group = test_commit_builder_group().await;
1280        let kp1 = test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "alice").await;
1281        let kp2 = test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
1282
1283        let expected_adds = vec![
1284            group.add_proposal(kp1.clone()).unwrap(),
1285            group.add_proposal(kp2.clone()).unwrap(),
1286        ];
1287
1288        let commit_output = group
1289            .commit_builder()
1290            .add_member(kp1)
1291            .unwrap()
1292            .add_member(kp2)
1293            .unwrap()
1294            .build()
1295            .await
1296            .unwrap();
1297
1298        assert_commit_builder_output(group, commit_output, expected_adds, 2);
1299    }
1300
1301    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1302    async fn test_commit_builder_empty_commit() {
1303        let mut group = test_commit_builder_group().await;
1304
1305        let commit_output = group.commit_builder().build().await.unwrap();
1306
1307        assert_commit_builder_output(group, commit_output, vec![], 0);
1308    }
1309
1310    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1311    async fn test_commit_builder_authenticated_data() {
1312        let mut group = test_commit_builder_group().await;
1313        let test_data = "test".as_bytes().to_vec();
1314
1315        let commit_output = group
1316            .commit_builder()
1317            .authenticated_data(test_data.clone())
1318            .build()
1319            .await
1320            .unwrap();
1321
1322        assert_eq!(
1323            commit_output
1324                .commit_message
1325                .into_plaintext()
1326                .unwrap()
1327                .content
1328                .authenticated_data,
1329            test_data
1330        );
1331    }
1332
1333    #[cfg(feature = "by_ref_proposal")]
1334    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1335    async fn test_commit_builder_multiple_welcome_messages() {
1336        let mut group = test_group_custom_config(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, |b| {
1337            let options = CommitOptions::new().with_single_welcome_message(false);
1338            b.mls_rules(DefaultMlsRules::new().with_commit_options(options))
1339        })
1340        .await;
1341
1342        let (alice, alice_kp) =
1343            test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "a").await;
1344
1345        let (bob, bob_kp) =
1346            test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "b").await;
1347
1348        group.propose_add(alice_kp.clone(), vec![]).await.unwrap();
1349
1350        group.propose_add(bob_kp.clone(), vec![]).await.unwrap();
1351
1352        let output = group.commit(Vec::new()).await.unwrap();
1353        let welcomes = output.welcome_messages;
1354
1355        let cs = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1356
1357        for (client, kp) in [(alice, alice_kp), (bob, bob_kp)] {
1358            let kp_ref = kp.key_package_reference(&cs).await.unwrap().unwrap();
1359
1360            let welcome = welcomes
1361                .iter()
1362                .find(|w| w.welcome_key_package_references().contains(&&kp_ref))
1363                .unwrap();
1364
1365            client.join_group(None, welcome, None).await.unwrap();
1366
1367            assert_eq!(welcome.clone().into_welcome().unwrap().secrets.len(), 1);
1368        }
1369    }
1370
1371    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1372    async fn commit_can_change_credential() {
1373        let cs = TEST_CIPHER_SUITE;
1374        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, cs, 3).await;
1375        let (identity, secret_key) = get_test_signing_identity(cs, b"member").await;
1376
1377        let commit_output = groups[0]
1378            .commit_builder()
1379            .set_new_signing_identity(secret_key, identity.clone())
1380            .build()
1381            .await
1382            .unwrap();
1383
1384        // Check that the credential was updated by in the committer's state.
1385        groups[0].process_pending_commit().await.unwrap();
1386        let new_member = groups[0].roster().member_with_index(0).unwrap();
1387
1388        assert_eq!(
1389            new_member.signing_identity.credential,
1390            get_test_basic_credential(b"member".to_vec())
1391        );
1392
1393        assert_eq!(
1394            new_member.signing_identity.signature_key,
1395            identity.signature_key
1396        );
1397
1398        // Check that the credential was updated in another member's state.
1399        groups[1]
1400            .process_message(commit_output.commit_message)
1401            .await
1402            .unwrap();
1403
1404        let new_member = groups[1].roster().member_with_index(0).unwrap();
1405
1406        assert_eq!(
1407            new_member.signing_identity.credential,
1408            get_test_basic_credential(b"member".to_vec())
1409        );
1410
1411        assert_eq!(
1412            new_member.signing_identity.signature_key,
1413            identity.signature_key
1414        );
1415    }
1416
1417    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1418    async fn commit_includes_tree_if_no_ratchet_tree_ext() {
1419        let mut group = test_group_custom(
1420            TEST_PROTOCOL_VERSION,
1421            TEST_CIPHER_SUITE,
1422            Default::default(),
1423            None,
1424            Some(CommitOptions::new().with_ratchet_tree_extension(false)),
1425        )
1426        .await;
1427
1428        let commit = group.commit(vec![]).await.unwrap();
1429
1430        group.apply_pending_commit().await.unwrap();
1431
1432        let new_tree = group.export_tree();
1433
1434        assert_eq!(new_tree, commit.ratchet_tree.unwrap())
1435    }
1436
1437    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1438    async fn commit_does_not_include_tree_if_ratchet_tree_ext() {
1439        let mut group = test_group_custom(
1440            TEST_PROTOCOL_VERSION,
1441            TEST_CIPHER_SUITE,
1442            Default::default(),
1443            None,
1444            Some(CommitOptions::new().with_ratchet_tree_extension(true)),
1445        )
1446        .await;
1447
1448        let commit = group.commit(vec![]).await.unwrap();
1449
1450        assert!(commit.ratchet_tree.is_none());
1451    }
1452
1453    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1454    async fn commit_includes_external_commit_group_info_if_requested() {
1455        let mut group = test_group_custom(
1456            TEST_PROTOCOL_VERSION,
1457            TEST_CIPHER_SUITE,
1458            Default::default(),
1459            None,
1460            Some(
1461                CommitOptions::new()
1462                    .with_allow_external_commit(true)
1463                    .with_ratchet_tree_extension(false),
1464            ),
1465        )
1466        .await;
1467
1468        let commit = group.commit(vec![]).await.unwrap();
1469
1470        let info = commit
1471            .external_commit_group_info
1472            .unwrap()
1473            .into_group_info()
1474            .unwrap();
1475
1476        assert!(!info.extensions.has_extension(ExtensionType::RATCHET_TREE));
1477        assert!(info.extensions.has_extension(ExtensionType::EXTERNAL_PUB));
1478    }
1479
1480    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1481    async fn commit_includes_external_commit_and_tree_if_requested() {
1482        let mut group = test_group_custom(
1483            TEST_PROTOCOL_VERSION,
1484            TEST_CIPHER_SUITE,
1485            Default::default(),
1486            None,
1487            Some(
1488                CommitOptions::new()
1489                    .with_allow_external_commit(true)
1490                    .with_ratchet_tree_extension(true),
1491            ),
1492        )
1493        .await;
1494
1495        let commit = group.commit(vec![]).await.unwrap();
1496
1497        let info = commit
1498            .external_commit_group_info
1499            .unwrap()
1500            .into_group_info()
1501            .unwrap();
1502
1503        assert!(info.extensions.has_extension(ExtensionType::RATCHET_TREE));
1504        assert!(info.extensions.has_extension(ExtensionType::EXTERNAL_PUB));
1505    }
1506
1507    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1508    async fn commit_does_not_include_external_commit_group_info_if_not_requested() {
1509        let mut group = test_group_custom(
1510            TEST_PROTOCOL_VERSION,
1511            TEST_CIPHER_SUITE,
1512            Default::default(),
1513            None,
1514            Some(CommitOptions::new().with_allow_external_commit(false)),
1515        )
1516        .await;
1517
1518        let commit = group.commit(vec![]).await.unwrap();
1519
1520        assert!(commit.external_commit_group_info.is_none());
1521    }
1522
1523    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1524    async fn commit_includes_tree_out_of_bounds_and_not_in_external_group_info_if_requested_tree_ext_off(
1525    ) {
1526        let mut group = test_group_custom(
1527            TEST_PROTOCOL_VERSION,
1528            TEST_CIPHER_SUITE,
1529            Default::default(),
1530            None,
1531            Some(
1532                CommitOptions::new()
1533                    .with_always_out_of_band_ratchet_tree(true)
1534                    .with_ratchet_tree_extension(false)
1535                    .with_allow_external_commit(true),
1536            ),
1537        )
1538        .await;
1539
1540        let commit = group.commit(vec![]).await.unwrap();
1541
1542        assert!(commit.ratchet_tree.is_some());
1543
1544        let info = commit
1545            .external_commit_group_info
1546            .unwrap()
1547            .into_group_info()
1548            .unwrap();
1549
1550        assert!(!info.extensions.has_extension(ExtensionType::RATCHET_TREE));
1551    }
1552
1553    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1554    async fn commit_includes_tree_out_of_bounds_and_not_in_external_group_info_if_requested_tree_ext_on(
1555    ) {
1556        let mut group = test_group_custom(
1557            TEST_PROTOCOL_VERSION,
1558            TEST_CIPHER_SUITE,
1559            Default::default(),
1560            None,
1561            Some(
1562                CommitOptions::new()
1563                    .with_always_out_of_band_ratchet_tree(true)
1564                    .with_ratchet_tree_extension(true)
1565                    .with_allow_external_commit(true),
1566            ),
1567        )
1568        .await;
1569
1570        let commit = group.commit(vec![]).await.unwrap();
1571
1572        assert!(commit.ratchet_tree.is_some());
1573
1574        let info = commit
1575            .external_commit_group_info
1576            .unwrap()
1577            .into_group_info()
1578            .unwrap();
1579
1580        assert!(!info.extensions.has_extension(ExtensionType::RATCHET_TREE));
1581    }
1582
1583    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1584    async fn member_identity_is_validated_against_new_extensions() {
1585        let alice = client_with_test_extension(b"alice").await;
1586        let mut alice = alice.group_builder().unwrap().build().await.unwrap();
1587
1588        let bob = client_with_test_extension(b"bob").await;
1589        let bob_kp = bob
1590            .generate_key_package_message(Default::default(), Default::default(), None)
1591            .await
1592            .unwrap();
1593
1594        let mut extension_list = ExtensionList::new();
1595        let extension = TestExtension { foo: b'a' };
1596        extension_list.set_from(extension).unwrap();
1597
1598        let res = alice
1599            .commit_builder()
1600            .add_member(bob_kp)
1601            .unwrap()
1602            .set_group_context_ext(extension_list.clone())
1603            .unwrap()
1604            .build()
1605            .await;
1606
1607        assert!(res.is_err());
1608
1609        let alex = client_with_test_extension(b"alex").await;
1610
1611        alice
1612            .commit_builder()
1613            .add_member(
1614                alex.generate_key_package_message(Default::default(), Default::default(), None)
1615                    .await
1616                    .unwrap(),
1617            )
1618            .unwrap()
1619            .set_group_context_ext(extension_list.clone())
1620            .unwrap()
1621            .build()
1622            .await
1623            .unwrap();
1624    }
1625
1626    #[cfg(feature = "by_ref_proposal")]
1627    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1628    async fn server_identity_is_validated_against_new_extensions() {
1629        let alice = client_with_test_extension(b"alice").await;
1630        let mut alice = alice.group_builder().unwrap().build().await.unwrap();
1631
1632        let mut extension_list = ExtensionList::new();
1633        let extension = TestExtension { foo: b'a' };
1634        extension_list.set_from(extension).unwrap();
1635
1636        let (alex_server, _) = get_test_signing_identity(TEST_CIPHER_SUITE, b"alex").await;
1637
1638        let mut alex_extensions = extension_list.clone();
1639
1640        alex_extensions
1641            .set_from(ExternalSendersExt {
1642                allowed_senders: vec![alex_server],
1643            })
1644            .unwrap();
1645
1646        let res = alice
1647            .commit_builder()
1648            .set_group_context_ext(alex_extensions)
1649            .unwrap()
1650            .build()
1651            .await;
1652
1653        assert!(res.is_err());
1654
1655        let (bob_server, _) = get_test_signing_identity(TEST_CIPHER_SUITE, b"bob").await;
1656
1657        let mut bob_extensions = extension_list;
1658
1659        bob_extensions
1660            .set_from(ExternalSendersExt {
1661                allowed_senders: vec![bob_server],
1662            })
1663            .unwrap();
1664
1665        alice
1666            .commit_builder()
1667            .set_group_context_ext(bob_extensions)
1668            .unwrap()
1669            .build()
1670            .await
1671            .unwrap();
1672    }
1673
1674    #[derive(Debug, Clone)]
1675    struct IdentityProviderWithExtension(BasicIdentityProvider);
1676
1677    #[derive(Clone, Debug)]
1678    #[cfg_attr(feature = "std", derive(thiserror::Error))]
1679    #[cfg_attr(feature = "std", error("test error"))]
1680    struct IdentityProviderWithExtensionError {}
1681
1682    impl IntoAnyError for IdentityProviderWithExtensionError {
1683        #[cfg(feature = "std")]
1684        fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
1685            Ok(self.into())
1686        }
1687    }
1688
1689    impl IdentityProviderWithExtension {
1690        // True if the identity starts with the character `foo` from `TestExtension` or if `TestExtension`
1691        // is not set.
1692        #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1693        async fn starts_with_foo(
1694            &self,
1695            identity: &SigningIdentity,
1696            _timestamp: Option<MlsTime>,
1697            extensions: Option<&ExtensionList>,
1698        ) -> bool {
1699            if let Some(extensions) = extensions {
1700                if let Some(ext) = extensions.get_as::<TestExtension>().unwrap() {
1701                    self.identity(identity, extensions).await.unwrap()[0] == ext.foo
1702                } else {
1703                    true
1704                }
1705            } else {
1706                true
1707            }
1708        }
1709    }
1710
1711    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1712    #[cfg_attr(mls_build_async, maybe_async::must_be_async)]
1713    impl IdentityProvider for IdentityProviderWithExtension {
1714        type Error = IdentityProviderWithExtensionError;
1715
1716        async fn validate_member(
1717            &self,
1718            identity: &SigningIdentity,
1719            timestamp: Option<MlsTime>,
1720            context: MemberValidationContext<'_>,
1721        ) -> Result<(), Self::Error> {
1722            self.starts_with_foo(identity, timestamp, context.new_extensions())
1723                .await
1724                .then_some(())
1725                .ok_or(IdentityProviderWithExtensionError {})
1726        }
1727
1728        async fn validate_external_sender(
1729            &self,
1730            identity: &SigningIdentity,
1731            timestamp: Option<MlsTime>,
1732            extensions: Option<&ExtensionList>,
1733        ) -> Result<(), Self::Error> {
1734            (!self.starts_with_foo(identity, timestamp, extensions).await)
1735                .then_some(())
1736                .ok_or(IdentityProviderWithExtensionError {})
1737        }
1738
1739        async fn identity(
1740            &self,
1741            signing_identity: &SigningIdentity,
1742            extensions: &ExtensionList,
1743        ) -> Result<Vec<u8>, Self::Error> {
1744            self.0
1745                .identity(signing_identity, extensions)
1746                .await
1747                .map_err(|_| IdentityProviderWithExtensionError {})
1748        }
1749
1750        async fn valid_successor(
1751            &self,
1752            _predecessor: &SigningIdentity,
1753            _successor: &SigningIdentity,
1754            _extensions: &ExtensionList,
1755        ) -> Result<bool, Self::Error> {
1756            Ok(true)
1757        }
1758
1759        fn supported_types(&self) -> Vec<CredentialType> {
1760            self.0.supported_types()
1761        }
1762    }
1763
1764    type ExtensionClientConfig = WithIdentityProvider<
1765        IdentityProviderWithExtension,
1766        WithCryptoProvider<TestCryptoProvider, BaseConfig>,
1767    >;
1768
1769    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1770    async fn client_with_test_extension(name: &[u8]) -> Client<ExtensionClientConfig> {
1771        let (identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, name).await;
1772
1773        ClientBuilder::new()
1774            .crypto_provider(TestCryptoProvider::new())
1775            .extension_types(vec![TEST_EXTENSION_TYPE.into()])
1776            .identity_provider(IdentityProviderWithExtension(BasicIdentityProvider::new()))
1777            .signing_identity(identity, secret_key, TEST_CIPHER_SUITE)
1778            .build()
1779    }
1780
1781    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1782    async fn detached_commit() {
1783        let mut group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1784
1785        let (_commit, secrets) = group.commit_builder().build_detached().await.unwrap();
1786        assert!(group.pending_commit.is_none());
1787        group.apply_detached_commit(secrets).await.unwrap();
1788        assert_eq!(group.context().epoch, 1);
1789    }
1790
1791    #[cfg(feature = "tree_index")]
1792    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1793    async fn tree_index_consistent_after_committer_self_update() {
1794        use crate::identity::basic::BasicIdentityProvider;
1795        use crate::tree_kem::TreeKemPublic;
1796
1797        let mut group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1798
1799        group.commit(vec![]).await.unwrap();
1800        group.process_pending_commit().await.unwrap();
1801
1802        let mut rebuilt = TreeKemPublic::import_node_data(
1803            group.state.public_tree.nodes.clone(),
1804            &BasicIdentityProvider,
1805            &Default::default(),
1806        )
1807        .await
1808        .unwrap();
1809
1810        let cs = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1811        rebuilt.tree_hash(&cs).await.unwrap();
1812
1813        assert!(group.state.public_tree.equal_internals(&rebuilt));
1814    }
1815}