1use 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 pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> {
87 Ok(MlsDecode::mls_decode(&mut &*bytes).map(Self)?)
88 }
89
90 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]
98pub struct CommitOutput {
102 pub commit_message: MlsMessage,
104 pub welcome_messages: Vec<MlsMessage>,
111 pub ratchet_tree: Option<ExportedTree<'static>>,
115 pub external_commit_group_info: Option<MlsMessage>,
119 #[cfg(feature = "by_ref_proposal")]
121 pub unused_proposals: Vec<crate::mls_rules::ProposalInfo<Proposal>>,
122 pub contains_update_path: bool,
124}
125
126impl CommitOutput {
127 pub fn commit_message(&self) -> &MlsMessage {
129 &self.commit_message
130 }
131
132 pub fn welcome_messages(&self) -> &[MlsMessage] {
134 &self.welcome_messages
135 }
136
137 pub fn ratchet_tree(&self) -> Option<&ExportedTree<'static>> {
141 self.ratchet_tree.as_ref()
142 }
143
144 pub fn external_commit_group_info(&self) -> Option<&MlsMessage> {
148 self.external_commit_group_info.as_ref()
149 }
150
151 #[cfg(feature = "by_ref_proposal")]
153 pub fn unused_proposals(&self) -> &[crate::mls_rules::ProposalInfo<Proposal>] {
154 &self.unused_proposals
155 }
156}
157
158pub 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 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 pub fn set_group_info_ext(self, extensions: ExtensionList) -> Self {
202 Self {
203 group_info_extensions: extensions,
204 ..self
205 }
206 }
207
208 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 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 #[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 #[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 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 #[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 pub fn raw_proposal(mut self, proposal: Proposal) -> Self {
282 self.proposals.push(proposal);
283 self
284 }
285
286 pub fn raw_proposals(mut self, mut proposals: Vec<Proposal>) -> Self {
290 self.proposals.append(&mut proposals);
291 self
292 }
293
294 pub fn authenticated_data(self, authenticated_data: Vec<u8>) -> Self {
300 Self {
301 authenticated_data,
302 ..self
303 }
304 }
305
306 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 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 pub fn commit_time(self, commit_time: MlsTime) -> Self {
335 Self {
336 commit_time: Some(commit_time),
337 ..self
338 }
339 }
340
341 #[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 #[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 #[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 #[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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 #[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(), signer: self.current_member_leaf_index(),
924 signature: vec![],
925 };
926
927 group_info.grease(self.cipher_suite_provider())?;
928
929 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 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 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 #[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}