1use std::{borrow::BorrowMut, marker::PhantomData};
5
6use openmls_traits::{
7 crypto::OpenMlsCrypto, random::OpenMlsRand, signatures::Signer, storage::StorageProvider as _,
8};
9use tls_codec::Serialize as _;
10
11use crate::{
12 binary_tree::LeafNodeIndex,
13 ciphersuite::{signable::Signable as _, Secret},
14 extensions::Extensions,
15 framing::{FramingParameters, WireFormat},
16 group::{
17 diff::compute_path::{CommitType, PathComputationResult},
18 CommitBuilderStageError, CreateCommitError, Extension, ExternalPubExtension, GroupContext,
19 ProposalQueue, ProposalQueueError, QueuedProposal, RatchetTreeExtension, StagedCommit,
20 WireFormatPolicy,
21 },
22 key_packages::KeyPackage,
23 messages::{
24 group_info::{GroupInfo, GroupInfoTBS},
25 Commit, Welcome,
26 },
27 prelude::{
28 CredentialWithKey, InvalidExtensionError, LeafNodeParameters, LibraryError,
29 NewSignerBundle, PreSharedKeyProposal,
30 },
31 schedule::{
32 psk::{load_psks, PskSecret, ResumptionPsk, ResumptionPskUsage},
33 EpochSecretsResult, JoinerSecret, KeySchedule, PreSharedKeyId, Psk,
34 },
35 storage::{OpenMlsProvider, StorageProvider},
36 treesync::errors::LeafNodeValidationError,
37 versions::ProtocolVersion,
38};
39#[cfg(feature = "virtual-clients-draft")]
40use crate::{
41 components::vc_commit_data::VirtualClientCommitData,
42 components::vc_derivation_info::{
43 require_newest_vc_derivation_epoch, DerivationInfo, DerivationInfoTbe, EpochEncryptionKey,
44 EpochId, ExternalInitSecret, OperationSecret, VcDerivationEpochState,
45 VirtualClientOperationType, VirtualClientsError,
46 },
47 components::vc_operation_tree::OperationSecretTree,
48 extensions::AppDataDictionary,
49 group::GroupId,
50};
51#[cfg(feature = "extensions-draft")]
52use crate::{
53 messages::proposals::AppDataUpdateProposal,
54 prelude::processing::{AppDataDictionaryUpdater, AppDataUpdates},
55 schedule::application_export_tree::ApplicationExportTree,
56};
57
58#[cfg(feature = "virtual-clients-draft")]
68#[derive(Debug)]
69struct VcLoaded {
70 epoch_id: EpochId,
71 emulation_leaf_index: LeafNodeIndex,
72 epoch_encryption_key: EpochEncryptionKey,
73 emulation_ciphersuite: openmls_traits::types::Ciphersuite,
74 generation: u32,
75 operation_secret: OperationSecret,
76 resolved_dictionary: AppDataDictionary,
80}
81
82pub(crate) mod external_commits;
83
84pub use external_commits::{ExternalCommitBuilder, ExternalCommitBuilderError};
85
86#[cfg(doc)]
87use super::MlsGroupJoinConfig;
88
89use super::{
90 branch::BranchInfo,
91 mls_auth_content::AuthenticatedContent,
92 staged_commit::{MemberStagedCommitState, StagedCommitState},
93 AddProposal, CreateCommitResult, GroupContextExtensionProposal, MlsGroup, MlsGroupState,
94 MlsMessageOut, PendingCommitState, Proposal, RemoveProposal, Sender,
95};
96
97#[cfg(feature = "virtual-clients-draft")]
98use super::HandshakeConfirmationData;
99
100#[derive(Debug)]
101struct ExternalCommitInfo {
102 aad: Vec<u8>,
103 credential: CredentialWithKey,
107 wire_format_policy: WireFormatPolicy,
108}
109
110#[derive(Debug, Default)]
111struct GroupInfoConfig {
112 create_group_info: bool,
113 use_ratchet_tree_extension: bool,
114 other_extensions: Vec<Extension>,
115}
116
117#[derive(Debug)]
119pub struct Initial {
120 own_proposals: Vec<Proposal>,
121 force_self_update: bool,
122 leaf_node_parameters: LeafNodeParameters,
123 external_commit_info: Option<ExternalCommitInfo>,
124
125 consume_proposal_store: bool,
128}
129
130impl Default for Initial {
131 fn default() -> Self {
132 Initial {
133 consume_proposal_store: true,
134 force_self_update: false,
135 leaf_node_parameters: LeafNodeParameters::default(),
136 own_proposals: vec![],
137 external_commit_info: None,
138 }
139 }
140}
141
142pub struct LoadedPsks {
144 own_proposals: Vec<Proposal>,
145 force_self_update: bool,
146 leaf_node_parameters: LeafNodeParameters,
147 external_commit_info: Option<ExternalCommitInfo>,
148
149 consume_proposal_store: bool,
152 psks: Vec<(PreSharedKeyId, Secret)>,
153
154 group_info_config: GroupInfoConfig,
156
157 #[cfg(feature = "extensions-draft")]
158 app_data_dictionary_updates: Option<AppDataUpdates>,
159}
160
161#[derive(Debug)]
163pub struct Complete {
164 result: CreateCommitResult,
165 original_wire_format_policy: Option<WireFormatPolicy>,
167}
168
169#[derive(Debug)]
207pub struct CommitBuilder<'a, T, G: BorrowMut<MlsGroup> = &'a mut MlsGroup> {
208 group: G,
211
212 stage: T,
214
215 #[cfg(feature = "virtual-clients-draft")]
219 vc_loaded: Option<VcLoaded>,
220
221 #[cfg(feature = "virtual-clients-draft")]
224 vc_new_derivation_epoch: bool,
225
226 pd: PhantomData<&'a ()>,
227}
228
229impl<'a, T, G: BorrowMut<MlsGroup>> CommitBuilder<'a, T, G> {
230 pub(crate) fn replace_stage<NextStage>(
231 self,
232 next_stage: NextStage,
233 ) -> (T, CommitBuilder<'a, NextStage, G>) {
234 self.map_stage(|prev_stage| (prev_stage, next_stage))
235 }
236
237 pub(crate) fn into_stage<NextStage>(
238 self,
239 next_stage: NextStage,
240 ) -> CommitBuilder<'a, NextStage, G> {
241 self.replace_stage(next_stage).1
242 }
243
244 fn take_stage(self) -> (T, CommitBuilder<'a, (), G>) {
245 self.replace_stage(())
246 }
247
248 fn map_stage<NextStage, Aux, F: FnOnce(T) -> (Aux, NextStage)>(
249 self,
250 f: F,
251 ) -> (Aux, CommitBuilder<'a, NextStage, G>) {
252 let Self {
253 group,
254 stage,
255 #[cfg(feature = "virtual-clients-draft")]
256 vc_loaded,
257 #[cfg(feature = "virtual-clients-draft")]
258 vc_new_derivation_epoch,
259 pd: PhantomData,
260 } = self;
261
262 let (aux, stage) = f(stage);
263
264 (
265 aux,
266 CommitBuilder {
267 group,
268 stage,
269 #[cfg(feature = "virtual-clients-draft")]
270 vc_loaded,
271 #[cfg(feature = "virtual-clients-draft")]
272 vc_new_derivation_epoch,
273 pd: PhantomData,
274 },
275 )
276 }
277
278 #[cfg(feature = "fork-resolution")]
279 pub(crate) fn stage(&self) -> &T {
280 &self.stage
281 }
282
283 #[cfg(feature = "virtual-clients-draft")]
286 pub fn vc_epoch_id(&self) -> Option<&EpochId> {
287 self.vc_loaded.as_ref().map(|loaded| &loaded.epoch_id)
288 }
289}
290
291impl MlsGroup {
292 pub fn commit_builder(&mut self) -> CommitBuilder<'_, Initial> {
294 CommitBuilder::<'_, Initial, &mut MlsGroup>::new(self)
295 }
296}
297
298impl<'a> CommitBuilder<'a, Initial, &mut MlsGroup> {
300 pub fn consume_proposal_store(mut self, consume_proposal_store: bool) -> Self {
303 self.stage.consume_proposal_store = consume_proposal_store;
304 self
305 }
306
307 pub fn force_self_update(mut self, force_self_update: bool) -> Self {
309 self.stage.force_self_update = force_self_update;
310 self
311 }
312
313 pub fn propose_adds(mut self, key_packages: impl IntoIterator<Item = KeyPackage>) -> Self {
316 self.stage.own_proposals.extend(
317 key_packages
318 .into_iter()
319 .map(|key_package| Proposal::add(AddProposal { key_package })),
320 );
321 self
322 }
323
324 pub fn propose_removals(mut self, removed: impl IntoIterator<Item = LeafNodeIndex>) -> Self {
327 self.stage.own_proposals.extend(
328 removed
329 .into_iter()
330 .map(|removed| Proposal::remove(RemoveProposal { removed })),
331 );
332 self
333 }
334
335 pub fn propose_group_context_extensions(
338 mut self,
339 extensions: Extensions<GroupContext>,
340 ) -> Result<Self, CreateCommitError> {
341 let proposal = GroupContextExtensionProposal::new(extensions);
342 self.stage
343 .own_proposals
344 .push(Proposal::group_context_extensions(proposal));
345 Ok(self)
346 }
347 pub fn propose_psks(mut self, psk_ids: impl IntoIterator<Item = PreSharedKeyId>) -> Self {
354 self.stage.own_proposals.extend(
355 psk_ids
356 .into_iter()
357 .map(|psk_id| Proposal::psk(PreSharedKeyProposal::new(psk_id))),
358 );
359 self
360 }
361
362 pub(crate) fn branch(
381 mut self,
382 rand: &impl OpenMlsRand,
383 branch_info: &BranchInfo,
384 ) -> Result<Self, CreateCommitError> {
385 let psk_id = PreSharedKeyId::new(
387 branch_info.ciphersuite(),
388 rand,
389 Psk::Resumption(ResumptionPsk::new(
390 ResumptionPskUsage::Branch,
391 branch_info.group_id().clone(),
392 branch_info.epoch(),
393 )),
394 )
395 .map_err(LibraryError::unexpected_crypto_error)?;
396 self = self.propose_psks([psk_id]);
397
398 let secret = branch_info.resumption_psk_secret().clone();
402 self.group.borrow_mut().resumption_psk_store.clear();
403 self.group
404 .borrow_mut()
405 .resumption_psk_store
406 .add(0.into(), secret);
407 Ok(self)
408 }
409
410 pub fn add_proposal(mut self, proposal: Proposal) -> Self {
413 self.stage.own_proposals.push(proposal);
414 self
415 }
416
417 pub fn add_proposals(mut self, proposals: impl IntoIterator<Item = Proposal>) -> Self {
419 self.stage.own_proposals.extend(proposals);
420 self
421 }
422}
423
424impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, Initial, G> {
426 pub fn new(group: G) -> CommitBuilder<'a, Initial, G> {
428 let stage = Initial {
429 ..Default::default()
430 };
431 CommitBuilder {
432 group,
433 stage,
434 #[cfg(feature = "virtual-clients-draft")]
435 vc_loaded: None,
436 #[cfg(feature = "virtual-clients-draft")]
437 vc_new_derivation_epoch: false,
438 pd: PhantomData,
439 }
440 }
441
442 pub fn leaf_node_parameters(mut self, leaf_node_parameters: LeafNodeParameters) -> Self {
445 self.stage.leaf_node_parameters = leaf_node_parameters;
446 self
447 }
448
449 #[cfg(feature = "virtual-clients-draft")]
504 pub fn vc_emulation<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
505 self,
506 crypto: &Crypto,
507 storage: &Storage,
508 emulation_group_id: &GroupId,
509 ) -> Result<Self, CreateCommitError> {
510 let epoch_id = require_newest_vc_derivation_epoch(storage, emulation_group_id)?;
511 self.vc_emulation_internal(crypto, storage, epoch_id)
512 }
513
514 #[cfg(all(feature = "virtual-clients-draft", any(test, feature = "test-utils")))]
523 pub fn vc_emulation_at_epoch<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
524 self,
525 crypto: &Crypto,
526 storage: &Storage,
527 epoch_id: EpochId,
528 ) -> Result<Self, CreateCommitError> {
529 self.vc_emulation_internal(crypto, storage, epoch_id)
530 }
531
532 #[cfg(feature = "virtual-clients-draft")]
533 fn vc_emulation_internal<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
534 mut self,
535 crypto: &Crypto,
536 storage: &Storage,
537 epoch_id: EpochId,
538 ) -> Result<Self, CreateCommitError> {
539 let state: VcDerivationEpochState = storage
540 .vc_derivation_epoch_state(&epoch_id)
541 .map_err(|e| {
542 log::error!("vc: load derivation epoch state in vc_emulation failed: {e:?}");
543 CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
544 })?
545 .ok_or(VirtualClientsError::MissingDerivationEpochState)?;
546 let mut operation_tree: OperationSecretTree = storage
547 .vc_operation_tree(&epoch_id)
548 .map_err(|e| {
549 log::error!("vc: load operation tree in vc_emulation failed: {e:?}");
550 CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
551 })?
552 .ok_or(VirtualClientsError::MissingOperationTree)?;
553 let (emulation_leaf_index, epoch_encryption_key, emulation_ciphersuite) =
554 state.into_parts();
555
556 let own_leaf_index = self.group.borrow().own_leaf_index();
563 let is_external_commit = self.stage.external_commit_info.is_some();
564 let resolved_dictionary = check_vc_leaf_configuration(
565 &self.stage.leaf_node_parameters,
566 self.group.borrow(),
567 own_leaf_index,
568 is_external_commit,
569 )?;
570
571 let (generation, operation_secret) = operation_tree.next_operation_secret(
576 crypto,
577 emulation_ciphersuite,
578 &epoch_id,
579 emulation_leaf_index,
580 VirtualClientOperationType::LeafNode,
581 self.group.borrow().group_id().as_slice(),
582 )?;
583 storage
586 .write_vc_operation_tree(&epoch_id, &operation_tree)
587 .map_err(|e| {
588 log::error!("vc: persist advanced operation tree failed: {e:?}");
589 CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
590 })?;
591
592 self.vc_loaded = Some(VcLoaded {
593 epoch_id,
594 emulation_leaf_index,
595 epoch_encryption_key,
596 emulation_ciphersuite,
597 generation,
598 operation_secret,
599 resolved_dictionary,
600 });
601 Ok(self)
602 }
603
604 #[cfg(feature = "virtual-clients-draft")]
628 pub fn derivation_epoch(mut self, derivation_epoch: bool) -> Self {
629 self.vc_new_derivation_epoch = derivation_epoch;
630 self
631 }
632
633 pub fn load_psks<Storage: StorageProvider>(
635 self,
636 storage: &'a Storage,
637 ) -> Result<CommitBuilder<'a, LoadedPsks, G>, CreateCommitError> {
638 let psk_ids: Vec<_> = self
639 .stage
640 .own_proposals
641 .iter()
642 .chain(
643 self.group
644 .borrow()
645 .proposal_store()
646 .proposals()
647 .map(|queued_proposal| queued_proposal.proposal()),
648 )
649 .filter_map(|proposal| match proposal {
650 Proposal::PreSharedKey(psk_proposal) => Some(psk_proposal.clone().into_psk_id()),
651 _ => None,
652 })
653 .collect();
654
655 let psks = load_psks(storage, &self.group.borrow().resumption_psk_store, &psk_ids)?
657 .into_iter()
658 .map(|(psk_id_ref, key)| (psk_id_ref.clone(), key))
659 .collect();
660
661 let use_ratchet_tree_extension = self
663 .group
664 .borrow()
665 .configuration()
666 .use_ratchet_tree_extension;
667
668 let group_info_config = GroupInfoConfig {
669 use_ratchet_tree_extension,
670 create_group_info: use_ratchet_tree_extension,
671 other_extensions: vec![],
672 };
673
674 Ok(self
675 .map_stage(|stage| {
676 (
677 (),
678 LoadedPsks {
679 own_proposals: stage.own_proposals,
680 psks,
681 force_self_update: stage.force_self_update,
682 leaf_node_parameters: stage.leaf_node_parameters,
683 consume_proposal_store: stage.consume_proposal_store,
684 group_info_config,
685 external_commit_info: stage.external_commit_info,
686 #[cfg(feature = "extensions-draft")]
687 app_data_dictionary_updates: None,
688 },
689 )
690 })
691 .1)
692 }
693}
694
695impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, LoadedPsks, G> {
696 pub fn create_group_info(mut self, create_group_info: bool) -> Self {
699 self.stage.group_info_config.create_group_info = create_group_info;
700 self
701 }
702
703 pub fn use_ratchet_tree_extension(mut self, use_ratchet_tree_extension: bool) -> Self {
706 if use_ratchet_tree_extension {
707 self.stage.group_info_config.create_group_info = true;
708 }
709 self.stage.group_info_config.use_ratchet_tree_extension = use_ratchet_tree_extension;
710 self
711 }
712
713 pub fn create_group_info_with_extensions(
718 mut self,
719 extensions: impl IntoIterator<Item = Extension>,
720 ) -> Result<Self, InvalidExtensionError> {
721 self.stage.group_info_config.create_group_info = true;
722 self.stage.group_info_config.other_extensions = extensions
723 .into_iter()
724 .map(|extension| {
725 if extension.as_ratchet_tree_extension().is_ok()
726 || extension.as_external_pub_extension().is_ok()
727 {
728 Err(InvalidExtensionError::CannotAddDirectlyToGroupInfo)
729 } else {
730 Ok(extension)
731 }
732 })
733 .collect::<Result<Vec<_>, _>>()?;
734
735 Ok(self)
736 }
737
738 pub fn build<S: Signer>(
742 self,
743 rand: &impl OpenMlsRand,
744 crypto: &impl OpenMlsCrypto,
745 signer: &S,
746 f: impl FnMut(&QueuedProposal) -> bool,
747 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
748 self.build_internal(rand, crypto, signer, None::<NewSignerBundle<'_, S>>, f)
749 }
750
751 pub fn build_with_new_signer<S: Signer>(
771 self,
772 rand: &impl OpenMlsRand,
773 crypto: &impl OpenMlsCrypto,
774 old_signer: &impl Signer,
775 new_signer: NewSignerBundle<'_, S>,
776 f: impl FnMut(&QueuedProposal) -> bool,
777 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
778 if self.stage.external_commit_info.is_some() {
782 return Err(CreateCommitError::ExternalCommitWithNewSigner);
783 }
784 self.build_internal(rand, crypto, old_signer, Some(new_signer), f)
785 }
786
787 fn build_internal<S: Signer>(
788 self,
789 rand: &impl OpenMlsRand,
790 crypto: &impl OpenMlsCrypto,
791 old_signer: &impl Signer,
792 new_signer: Option<NewSignerBundle<'_, S>>,
793 f: impl FnMut(&QueuedProposal) -> bool,
794 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
795 #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
796 let (mut cur_stage, mut builder) = self.take_stage();
797
798 let GroupInfoConfig {
800 create_group_info,
801 use_ratchet_tree_extension,
802 other_extensions,
803 } = cur_stage.group_info_config;
804
805 #[cfg(feature = "virtual-clients-draft")]
810 if builder.vc_new_derivation_epoch {
811 stage_vc_new_derivation_epoch(builder.group.borrow_mut())?;
812 }
813
814 let group = builder.group.borrow();
815
816 #[cfg(feature = "virtual-clients-draft")]
820 let marks_new_vc_derivation_epoch = staged_vc_commit_data(group)?
821 .is_some_and(|commit_data| commit_data.creates_derivation_epoch());
822 let ciphersuite = group.ciphersuite();
823 let own_leaf_index = group.own_leaf_index();
824 let (sender, is_external_commit) = match cur_stage.external_commit_info {
825 None => (Sender::build_member(own_leaf_index), false),
826 Some(_) => (Sender::NewMemberCommit, true),
827 };
828 let psks = cur_stage.psks;
829
830 if let Some(ExternalCommitInfo { credential, .. }) = &cur_stage.external_commit_info {
836 if let Some(params_credential) = cur_stage.leaf_node_parameters.credential_with_key() {
837 if params_credential != credential {
838 return Err(CreateCommitError::ExternalCommitCredentialMismatch);
839 }
840 }
841 cur_stage
842 .leaf_node_parameters
843 .set_credential_with_key(credential.clone());
844 }
845
846 let new_signer = match new_signer {
851 Some(NewSignerBundle {
852 signer,
853 credential_with_key,
854 }) => {
855 if ciphersuite.signature_algorithm() != signer.signature_scheme() {
856 return Err(CreateCommitError::InvalidSignerCiphersuite);
857 }
858 if let Some(params_credential) =
859 cur_stage.leaf_node_parameters.credential_with_key()
860 {
861 if params_credential != &credential_with_key {
862 return Err(CreateCommitError::InvalidLeafNodeParameters);
863 }
864 }
865 cur_stage
866 .leaf_node_parameters
867 .set_credential_with_key(credential_with_key);
868 Some(signer)
869 }
870 None => None,
871 };
872
873 let own_proposals: Vec<_> = cur_stage
876 .own_proposals
877 .into_iter()
878 .map(|proposal| {
879 QueuedProposal::from_proposal_and_sender(ciphersuite, crypto, proposal, &sender)
880 })
881 .collect::<Result<_, _>>()?;
882
883 let group_proposal_store_queue = group
886 .pending_proposals()
887 .filter(|_| cur_stage.consume_proposal_store)
888 .cloned();
889
890 let proposal_queue = group_proposal_store_queue.chain(own_proposals).filter(f);
894
895 let (proposal_queue, contains_own_updates) =
896 ProposalQueue::filter_proposals(proposal_queue, group.own_leaf_index).map_err(|e| {
897 match e {
898 ProposalQueueError::LibraryError(e) => e.into(),
899 ProposalQueueError::ProposalNotFound => CreateCommitError::MissingProposal,
900 ProposalQueueError::UpdateFromExternalSender
901 | ProposalQueueError::SelfRemoveFromNonMember => {
902 CreateCommitError::WrongProposalSenderType
903 }
904 }
905 })?;
906
907 group
912 .public_group
913 .validate_proposal_type_support(&proposal_queue)?;
914 let path_leaf_signature_key = cur_stage
919 .leaf_node_parameters
920 .credential_with_key()
921 .map(|credential_with_key| &credential_with_key.signature_key);
922 group.public_group.validate_key_uniqueness(
923 &proposal_queue,
924 None,
925 &sender,
926 path_leaf_signature_key,
927 )?;
928 group.public_group.validate_add_proposals(&proposal_queue)?;
930 group.public_group.validate_capabilities(&proposal_queue)?;
933 group
936 .public_group
937 .validate_remove_proposals(&proposal_queue)?;
938 group
942 .public_group
943 .validate_pre_shared_key_proposals(&proposal_queue)?;
944 group
949 .public_group
950 .validate_update_proposals(&proposal_queue, own_leaf_index)?;
951
952 group
955 .public_group
956 .validate_group_context_extensions_proposal(&proposal_queue)?;
957
958 #[cfg(feature = "extensions-draft")]
959 group
960 .public_group
961 .validate_app_data_update_proposals_and_group_context(&proposal_queue)?;
962
963 if is_external_commit {
964 group
965 .public_group
966 .validate_external_commit(&proposal_queue)?;
967 }
968
969 let proposal_reference_list = proposal_queue.commit_list();
970
971 let mut diff = group.public_group.empty_diff();
973
974 #[cfg(feature = "extensions-draft")]
976 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
977 &proposal_queue,
978 own_leaf_index,
979 cur_stage.app_data_dictionary_updates,
980 )?;
981 #[cfg(not(feature = "extensions-draft"))]
982 let apply_proposals_values = diff.apply_proposals(&proposal_queue, own_leaf_index)?;
983 if apply_proposals_values.self_removed && !is_external_commit {
984 return Err(CreateCommitError::CannotRemoveSelf);
985 }
986
987 #[cfg(feature = "virtual-clients-draft")]
995 let vc_loaded = builder.vc_loaded.take();
996 #[cfg(feature = "virtual-clients-draft")]
997 let own_update_override = if let Some(loaded) = vc_loaded.as_ref() {
998 let external_init_secret =
1008 is_external_commit.then(|| group.group_epoch_secrets().init_secret());
1009 Some(apply_vc_emulation(
1010 loaded,
1011 &mut cur_stage.leaf_node_parameters,
1012 loaded.resolved_dictionary.clone(),
1013 crypto,
1014 ciphersuite,
1015 group.group_id(),
1016 external_init_secret,
1017 )?)
1018 } else {
1019 None
1020 };
1021 #[cfg(not(feature = "virtual-clients-draft"))]
1022 let own_update_override: Option<crate::treesync::diff::OwnUpdatePathOverride> = None;
1023
1024 let path_computation_result =
1027 if apply_proposals_values.path_required
1029 || contains_own_updates
1030 || cur_stage.force_self_update
1031 || !cur_stage.leaf_node_parameters.is_empty()
1032 || new_signer.is_some()
1033 {
1034 let commit_type = if is_external_commit {
1035 CommitType::External
1036 } else {
1037 CommitType::Member
1038 };
1039 match new_signer {
1043 Some(new_signer) => diff.compute_path(
1044 rand,
1045 crypto,
1046 own_leaf_index,
1047 apply_proposals_values.exclusion_list(),
1048 &commit_type,
1049 &cur_stage.leaf_node_parameters,
1050 new_signer,
1051 apply_proposals_values.extensions.clone(),
1052 own_update_override,
1053 )?,
1054 None => diff.compute_path(
1055 rand,
1056 crypto,
1057 own_leaf_index,
1058 apply_proposals_values.exclusion_list(),
1059 &commit_type,
1060 &cur_stage.leaf_node_parameters,
1061 old_signer,
1062 apply_proposals_values.extensions.clone(),
1063 own_update_override,
1064 )?,
1065 }
1066 } else {
1067 diff.update_group_context(crypto, apply_proposals_values.extensions.clone())?;
1070 PathComputationResult::default()
1071 };
1072
1073 let update_path_leaf_node = path_computation_result
1074 .encrypted_path
1075 .as_ref()
1076 .map(|path| path.leaf_node().clone());
1077
1078 if let Some(ref leaf_node) = update_path_leaf_node {
1080 if !diff
1086 .group_context()
1087 .extensions()
1088 .iter()
1089 .map(Extension::extension_type)
1090 .all(|ext_type| leaf_node.supports_extension(&ext_type))
1091 {
1092 return Err(CreateCommitError::LeafNodeValidation(
1093 LeafNodeValidationError::UnsupportedExtensions,
1094 ));
1095 }
1096
1097 if let Some(required_capabilities) =
1100 diff.group_context().extensions().required_capabilities()
1101 {
1102 leaf_node
1103 .capabilities()
1104 .supports_required_capabilities(required_capabilities)?
1105 }
1106 }
1107
1108 let commit = Commit {
1110 proposals: proposal_reference_list,
1111 path: path_computation_result.encrypted_path,
1112 };
1113
1114 let (outgoing_aad, wire_format): (Vec<u8>, WireFormat) =
1115 match &cur_stage.external_commit_info {
1116 None => (
1117 group.outgoing_authenticated_data()?,
1118 group.outgoing_wire_format(),
1119 ),
1120 Some(ExternalCommitInfo { aad, .. }) => {
1121 #[cfg(feature = "extensions-draft")]
1128 let aad_bytes = if group.context().safe_aad_required() {
1129 crate::framing::safe_aad::assemble_authenticated_data(&group.safe_aad, aad)
1130 .map_err(|_| LibraryError::custom("SafeAad serialization failed"))?
1131 } else {
1132 aad.clone()
1133 };
1134 #[cfg(not(feature = "extensions-draft"))]
1135 let aad_bytes = aad.clone();
1136 (aad_bytes, WireFormat::PublicMessage)
1137 }
1138 };
1139
1140 let framing_parameters = FramingParameters::new(&outgoing_aad, wire_format);
1141
1142 let mut authenticated_content = AuthenticatedContent::commit(
1144 framing_parameters,
1145 sender,
1146 commit,
1147 group.public_group.group_context(),
1148 old_signer,
1149 )?;
1150
1151 diff.update_confirmed_transcript_hash(crypto, &authenticated_content)?;
1153
1154 let serialized_provisional_group_context = diff
1155 .group_context()
1156 .tls_serialize_detached()
1157 .map_err(LibraryError::missing_bound_check)?;
1158
1159 let joiner_secret = JoinerSecret::new(
1160 crypto,
1161 ciphersuite,
1162 path_computation_result.commit_secret,
1163 group.group_epoch_secrets().init_secret(),
1164 &serialized_provisional_group_context,
1165 )
1166 .map_err(LibraryError::unexpected_crypto_error)?;
1167
1168 let psk_secret = PskSecret::new(crypto, ciphersuite, psks)?;
1170
1171 let mut key_schedule = KeySchedule::init(ciphersuite, crypto, &joiner_secret, psk_secret)?;
1173
1174 let serialized_provisional_group_context = diff
1175 .group_context()
1176 .tls_serialize_detached()
1177 .map_err(LibraryError::missing_bound_check)?;
1178
1179 let welcome_secret = key_schedule
1180 .welcome(crypto, ciphersuite)
1181 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
1182 key_schedule
1183 .add_context(crypto, &serialized_provisional_group_context)
1184 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
1185 let EpochSecretsResult {
1186 epoch_secrets: provisional_epoch_secrets,
1187 #[cfg(feature = "extensions-draft")]
1188 application_exporter,
1189 } = key_schedule
1190 .epoch_secrets(crypto, ciphersuite)
1191 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
1192
1193 let confirmation_tag = provisional_epoch_secrets
1195 .confirmation_key()
1196 .tag(
1197 crypto,
1198 ciphersuite,
1199 diff.group_context().confirmed_transcript_hash(),
1200 )
1201 .map_err(LibraryError::unexpected_crypto_error)?;
1202
1203 authenticated_content.set_confirmation_tag(confirmation_tag.clone());
1205
1206 diff.update_interim_transcript_hash(ciphersuite, crypto, confirmation_tag.clone())?;
1207
1208 let needs_welcome = !apply_proposals_values.invitation_list.is_empty();
1210
1211 let needs_group_info = needs_welcome || create_group_info;
1215
1216 let (welcome_option, group_info) = if !needs_group_info {
1217 (None, None)
1218 } else {
1219 let mut extensions_list = vec![];
1221 if use_ratchet_tree_extension {
1222 extensions_list.push(Extension::RatchetTree(RatchetTreeExtension::new(
1223 diff.export_ratchet_tree(),
1224 )));
1225 };
1226 extensions_list.extend(other_extensions);
1228
1229 let mut extensions = Extensions::from_vec(extensions_list)?;
1230
1231 let welcome_option = needs_welcome
1232 .then(|| -> Result<_, CreateCommitError> {
1233 let group_info_tbs = {
1234 GroupInfoTBS::new(
1235 diff.group_context().clone(),
1236 extensions.clone(),
1237 confirmation_tag.clone(),
1238 own_leaf_index,
1239 )?
1240 };
1241 let group_info = match new_signer {
1245 Some(new_signer) => group_info_tbs.sign(new_signer)?,
1246 None => group_info_tbs.sign(old_signer)?,
1247 };
1248
1249 let (welcome_key, welcome_nonce) = welcome_secret
1251 .derive_welcome_key_nonce(crypto, ciphersuite)
1252 .map_err(LibraryError::unexpected_crypto_error)?;
1253 let encrypted_group_info = welcome_key
1254 .aead_seal(
1255 crypto,
1256 group_info
1257 .tls_serialize_detached()
1258 .map_err(LibraryError::missing_bound_check)?
1259 .as_slice(),
1260 &[],
1261 &welcome_nonce,
1262 )
1263 .map_err(LibraryError::unexpected_crypto_error)?;
1264
1265 let encrypted_secrets = diff.encrypt_group_secrets(
1268 &joiner_secret,
1269 apply_proposals_values.invitation_list,
1270 path_computation_result.plain_path.as_deref(),
1271 &apply_proposals_values.presharedkeys,
1272 &encrypted_group_info,
1273 crypto,
1274 own_leaf_index,
1275 )?;
1276
1277 let welcome =
1279 Welcome::new(ciphersuite, encrypted_secrets, encrypted_group_info);
1280 Ok(welcome)
1281 })
1282 .transpose()?;
1283
1284 let exported_group_info = create_group_info
1287 .then(|| -> Result<_, CreateCommitError> {
1288 let external_pub = provisional_epoch_secrets
1289 .external_secret()
1290 .derive_external_keypair(crypto, ciphersuite)
1291 .map_err(LibraryError::unexpected_crypto_error)?
1292 .public;
1293
1294 let external_pub_extension =
1295 Extension::ExternalPub(ExternalPubExtension::new(external_pub.into()));
1296 extensions.add(external_pub_extension)?;
1297 let group_info_tbs = {
1298 GroupInfoTBS::new(
1299 diff.group_context().clone(),
1300 extensions,
1301 confirmation_tag.clone(),
1302 own_leaf_index,
1303 )?
1304 };
1305 match new_signer {
1310 Some(new_signer) => Ok(group_info_tbs.sign(new_signer)?),
1311 None => Ok(group_info_tbs.sign(old_signer)?),
1312 }
1313 })
1314 .transpose()?;
1315
1316 (welcome_option, exported_group_info)
1317 };
1318
1319 let (provisional_group_epoch_secrets, provisional_message_secrets) =
1320 provisional_epoch_secrets.split_secrets(
1321 serialized_provisional_group_context,
1322 diff.tree_size(),
1323 own_leaf_index,
1324 );
1325
1326 #[cfg(feature = "extensions-draft")]
1327 let application_export_tree = ApplicationExportTree::new(application_exporter);
1328 let staged_commit_state = MemberStagedCommitState::new(
1329 provisional_group_epoch_secrets,
1330 provisional_message_secrets,
1331 diff.into_staged_diff(crypto, ciphersuite)?,
1332 path_computation_result.new_keypairs,
1333 None,
1336 update_path_leaf_node,
1337 #[cfg(feature = "extensions-draft")]
1338 application_export_tree,
1339 #[cfg(feature = "virtual-clients-draft")]
1343 None,
1344 );
1345 #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
1346 let mut staged_commit = StagedCommit::new(
1347 proposal_queue,
1348 StagedCommitState::GroupMember(Box::new(staged_commit_state)),
1349 #[cfg(feature = "virtual-clients-draft")]
1350 vc_loaded.as_ref().map(|loaded| loaded.epoch_id.clone()),
1351 );
1352 #[cfg(feature = "virtual-clients-draft")]
1353 {
1354 staged_commit.marks_new_vc_derivation_epoch = marks_new_vc_derivation_epoch;
1355 }
1356
1357 Ok(builder.into_stage(Complete {
1358 result: CreateCommitResult {
1359 commit: authenticated_content,
1360 welcome_option,
1361 staged_commit,
1362 group_info: group_info.filter(|_| create_group_info),
1363 },
1364 original_wire_format_policy: cur_stage
1365 .external_commit_info
1366 .as_ref()
1367 .map(|info| info.wire_format_policy),
1368 }))
1369 }
1370
1371 #[cfg(feature = "extensions-draft")]
1376 pub fn app_data_dictionary_updater(&self) -> AppDataDictionaryUpdater<'_> {
1377 AppDataDictionaryUpdater::new(self.group.borrow().context().app_data_dict())
1378 }
1379
1380 #[cfg(feature = "extensions-draft")]
1382 pub fn with_app_data_dictionary_updates(
1383 &mut self,
1384 app_data_dictionary_updates: Option<AppDataUpdates>,
1385 ) {
1386 self.stage.app_data_dictionary_updates = app_data_dictionary_updates;
1387 }
1388
1389 #[cfg(feature = "extensions-draft")]
1391 pub fn app_data_update_proposals(&self) -> impl Iterator<Item = &AppDataUpdateProposal> {
1392 let proposal_store_proposals = self
1393 .group
1394 .borrow()
1395 .proposal_store()
1396 .proposals()
1397 .map(|queued_proposal| queued_proposal.proposal());
1398
1399 let all_proposals = proposal_store_proposals.chain(self.stage.own_proposals.iter());
1401
1402 let mut app_data_update_proposals: Vec<&AppDataUpdateProposal> = all_proposals
1404 .filter_map(|proposal| match proposal {
1405 Proposal::AppDataUpdate(proposal) => Some(proposal.as_ref()),
1406 _ => None,
1407 })
1408 .collect();
1409
1410 app_data_update_proposals.sort_by_key(|prop| prop.component_id());
1411 app_data_update_proposals.into_iter()
1412 }
1413}
1414
1415impl CommitBuilder<'_, Complete, &mut MlsGroup> {
1417 #[cfg(test)]
1418 pub(crate) fn commit_result(self) -> CreateCommitResult {
1419 self.stage.result
1420 }
1421
1422 pub fn stage_commit<Provider: OpenMlsProvider>(
1424 self,
1425 provider: &Provider,
1426 ) -> Result<CommitMessageBundle, CommitBuilderStageError<Provider::StorageError>> {
1427 let Self {
1428 group,
1429 stage:
1430 Complete {
1431 result: create_commit_result,
1432 original_wire_format_policy: _,
1433 },
1434 ..
1435 } = self;
1436
1437 group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
1440 create_commit_result.staged_commit,
1441 )));
1442
1443 provider
1444 .storage()
1445 .write_group_state(group.group_id(), &group.group_state)
1446 .map_err(CommitBuilderStageError::KeyStoreError)?;
1447
1448 group.reset_aad();
1449
1450 let framing = group.content_to_mls_message(create_commit_result.commit, provider)?;
1456
1457 Ok(CommitMessageBundle {
1458 version: group.version(),
1459 commit: framing.message,
1460 welcome: create_commit_result.welcome_option,
1461 group_info: create_commit_result.group_info,
1462 #[cfg(feature = "virtual-clients-draft")]
1463 confirmation: framing.confirmation,
1464 })
1465 }
1466}
1467
1468#[cfg(feature = "virtual-clients-draft")]
1474fn staged_vc_commit_data(
1475 group: &MlsGroup,
1476) -> Result<Option<VirtualClientCommitData>, CreateCommitError> {
1477 if !group.is_emulation_group() || !group.context().safe_aad_required() {
1478 return Ok(None);
1479 }
1480 Ok(VirtualClientCommitData::from_safe_aad(&group.safe_aad)?)
1481}
1482
1483#[cfg(feature = "virtual-clients-draft")]
1490fn stage_vc_new_derivation_epoch(group: &mut MlsGroup) -> Result<(), CreateCommitError> {
1491 if !group.is_emulation_group() {
1492 return Err(CreateCommitError::NewDerivationEpochOutsideEmulationGroup);
1493 }
1494 if !group.context().safe_aad_required() {
1495 return Err(CreateCommitError::NewDerivationEpochWithoutSafeAad);
1496 }
1497
1498 let mut commit_data = staged_vc_commit_data(group)?
1499 .map_or_else(|| VirtualClientCommitData::new(Vec::new()), Ok)?;
1500 commit_data.require_new_derivation_epoch();
1501 group.safe_aad.upsert(commit_data.to_safe_aad_item()?);
1502 Ok(())
1503}
1504
1505#[cfg(feature = "virtual-clients-draft")]
1517fn apply_vc_emulation(
1518 loaded: &VcLoaded,
1519 leaf_node_parameters: &mut LeafNodeParameters,
1520 resolved_dictionary: AppDataDictionary,
1521 crypto: &impl OpenMlsCrypto,
1522 group_ciphersuite: openmls_traits::types::Ciphersuite,
1523 group_id: &crate::prelude::GroupId,
1524 external_init_secret: Option<&crate::schedule::InitSecret>,
1525) -> Result<crate::treesync::diff::OwnUpdatePathOverride, CreateCommitError> {
1526 let target_operation_secret = loaded.operation_secret.derive_target_operation_secret(
1527 crypto,
1528 group_ciphersuite,
1529 group_id,
1530 )?;
1531 let path_secret = target_operation_secret
1532 .derive_path_generation_secret(crypto, group_ciphersuite)?
1533 .into();
1534 let leaf_encryption_keypair = target_operation_secret
1535 .derive_encryption_key_secret(crypto, group_ciphersuite)?
1536 .generate_encryption_key_pair(crypto, group_ciphersuite)?;
1537 drop(target_operation_secret);
1538
1539 let leaf_encryption_key = leaf_encryption_keypair
1542 .public_key()
1543 .tls_serialize_detached()
1544 .map_err(VirtualClientsError::from)?;
1545 let tbe = DerivationInfoTbe::LeafNode {
1549 leaf_index: loaded.emulation_leaf_index,
1550 generation: loaded.generation,
1551 external_init_secret: external_init_secret
1552 .map(|init_secret| ExternalInitSecret::from_slice(init_secret.as_slice())),
1553 };
1554 let derivation_info = DerivationInfo::encrypt(
1555 crypto,
1556 loaded.emulation_ciphersuite,
1557 &loaded.epoch_encryption_key,
1558 loaded.epoch_id.clone(),
1559 &leaf_encryption_key,
1560 &tbe,
1561 )?;
1562 let derivation_info_bytes = derivation_info
1563 .tls_serialize_detached()
1564 .map_err(VirtualClientsError::from)?;
1565
1566 inject_vc_derivation_info(
1567 leaf_node_parameters,
1568 resolved_dictionary,
1569 derivation_info_bytes,
1570 )?;
1571
1572 Ok(crate::treesync::diff::OwnUpdatePathOverride {
1573 path_secret,
1574 leaf_encryption_keypair,
1575 })
1576}
1577
1578#[cfg(feature = "virtual-clients-draft")]
1590fn check_vc_leaf_configuration(
1591 leaf_node_parameters: &LeafNodeParameters,
1592 group: &MlsGroup,
1593 own_leaf_index: LeafNodeIndex,
1594 is_external_commit: bool,
1595) -> Result<AppDataDictionary, CreateCommitError> {
1596 let current_leaf = if is_external_commit {
1597 None
1598 } else {
1599 Some(group.public_group().leaf(own_leaf_index).ok_or_else(|| {
1600 LibraryError::custom("Couldn't find own leaf for VC capability check")
1601 })?)
1602 };
1603
1604 crate::components::vc_derivation_info::resolve_vc_leaf_dictionary(
1605 leaf_node_parameters.capabilities(),
1606 leaf_node_parameters.extensions(),
1607 current_leaf,
1608 )
1609 .map_err(CreateCommitError::VirtualClientsError)
1610}
1611
1612#[cfg(feature = "virtual-clients-draft")]
1617fn inject_vc_derivation_info(
1618 leaf_node_parameters: &mut LeafNodeParameters,
1619 resolved_dictionary: AppDataDictionary,
1620 derivation_info_bytes: Vec<u8>,
1621) -> Result<(), CreateCommitError> {
1622 let extensions = crate::components::vc_derivation_info::merge_vc_derivation_info(
1623 leaf_node_parameters.extensions(),
1624 resolved_dictionary,
1625 derivation_info_bytes,
1626 )?;
1627 leaf_node_parameters.set_extensions(extensions);
1628 Ok(())
1629}
1630
1631#[derive(Debug, Clone)]
1634pub struct CommitMessageBundle {
1635 version: ProtocolVersion,
1636 commit: MlsMessageOut,
1637 welcome: Option<Welcome>,
1638 group_info: Option<GroupInfo>,
1639 #[cfg(feature = "virtual-clients-draft")]
1642 confirmation: Option<HandshakeConfirmationData>,
1643}
1644
1645pub struct WelcomeCommitMessages {
1650 pub commit: MlsMessageOut,
1652
1653 pub welcome: MlsMessageOut,
1655
1656 pub group_info: Option<MlsMessageOut>,
1658}
1659
1660impl TryFrom<CommitMessageBundle> for WelcomeCommitMessages {
1661 type Error = LibraryError;
1662
1663 fn try_from(value: CommitMessageBundle) -> Result<Self, Self::Error> {
1664 let (commit, welcome_opt, group_info) = value.into_messages();
1665 Ok(Self {
1666 commit,
1667 welcome: welcome_opt.ok_or_else(|| {
1668 LibraryError::custom(
1669 "WelcomeCommitMessages must only be used with commits that produce a welcome.",
1670 )
1671 })?,
1672 group_info,
1673 })
1674 }
1675}
1676
1677#[cfg(test)]
1678impl CommitMessageBundle {
1679 pub fn new(
1680 version: ProtocolVersion,
1681 commit: MlsMessageOut,
1682 welcome: Option<Welcome>,
1683 group_info: Option<GroupInfo>,
1684 ) -> Self {
1685 Self {
1686 version,
1687 commit,
1688 welcome,
1689 group_info,
1690 #[cfg(feature = "virtual-clients-draft")]
1691 confirmation: None,
1692 }
1693 }
1694}
1695
1696impl CommitMessageBundle {
1697 pub fn commit(&self) -> &MlsMessageOut {
1701 &self.commit
1702 }
1703
1704 pub fn welcome(&self) -> Option<&Welcome> {
1707 self.welcome.as_ref()
1708 }
1709
1710 pub fn to_welcome_msg(&self) -> Option<MlsMessageOut> {
1713 self.welcome
1714 .as_ref()
1715 .map(|welcome| MlsMessageOut::from_welcome(welcome.clone(), self.version))
1716 }
1717
1718 pub fn group_info(&self) -> Option<&GroupInfo> {
1722 self.group_info.as_ref()
1723 }
1724
1725 #[cfg(feature = "virtual-clients-draft")]
1733 pub fn confirmation(&self) -> Option<&HandshakeConfirmationData> {
1734 self.confirmation.as_ref()
1735 }
1736
1737 #[cfg(feature = "virtual-clients-draft")]
1743 pub fn take_confirmation(&mut self) -> Option<HandshakeConfirmationData> {
1744 self.confirmation.take()
1745 }
1746
1747 pub fn contents(&self) -> (&MlsMessageOut, Option<&Welcome>, Option<&GroupInfo>) {
1750 (
1751 &self.commit,
1752 self.welcome.as_ref(),
1753 self.group_info.as_ref(),
1754 )
1755 }
1756
1757 pub fn into_commit(self) -> MlsMessageOut {
1761 self.commit
1762 }
1763
1764 pub fn into_welcome(self) -> Option<Welcome> {
1768 self.welcome
1769 }
1770
1771 pub fn into_welcome_msg(self) -> Option<MlsMessageOut> {
1774 self.welcome
1775 .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version))
1776 }
1777
1778 pub fn into_group_info(self) -> Option<GroupInfo> {
1783 self.group_info
1784 }
1785
1786 pub fn into_group_info_msg(self) -> Option<MlsMessageOut> {
1788 self.group_info.map(|group_info| group_info.into())
1789 }
1790
1791 pub fn into_contents(self) -> (MlsMessageOut, Option<Welcome>, Option<GroupInfo>) {
1794 (self.commit, self.welcome, self.group_info)
1795 }
1796
1797 pub fn into_messages(self) -> (MlsMessageOut, Option<MlsMessageOut>, Option<MlsMessageOut>) {
1800 (
1801 self.commit,
1802 self.welcome
1803 .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version)),
1804 self.group_info.map(|group_info| group_info.into()),
1805 )
1806 }
1807}
1808
1809impl IntoIterator for CommitMessageBundle {
1810 type Item = MlsMessageOut;
1811
1812 type IntoIter = core::iter::Chain<
1813 core::iter::Chain<
1814 core::option::IntoIter<MlsMessageOut>,
1815 core::option::IntoIter<MlsMessageOut>,
1816 >,
1817 core::option::IntoIter<MlsMessageOut>,
1818 >;
1819
1820 fn into_iter(self) -> Self::IntoIter {
1821 let welcome = self.to_welcome_msg();
1822 let group_info = self.group_info.map(|group_info| group_info.into());
1823
1824 Some(self.commit)
1825 .into_iter()
1826 .chain(welcome)
1827 .chain(group_info)
1828 }
1829}
1830
1831#[cfg(test)]
1832mod branch_tests {
1833 use crate::{
1834 group::{
1835 mls_group::tests_and_kats::utils::{setup_alice_bob_group, setup_client},
1836 CreateCommitError, MlsGroup, ProposalValidationError,
1837 },
1838 schedule::errors::PskError,
1839 };
1840
1841 #[openmls_test::openmls_test]
1849 fn subgroup_branch_psk_rejected_outside_initial_commit() {
1850 let alice_provider = &Provider::default();
1851 let bob_provider = &Provider::default();
1852 let parent_provider = &Provider::default();
1853
1854 let (mut alice_group, alice_signer, _bob_group, _bob_signer, _alice_cwk, _bob_cwk) =
1857 setup_alice_bob_group(ciphersuite, alice_provider, bob_provider);
1858
1859 let (parent_cwk, _parent_kpb, parent_signer, _parent_pk) =
1863 setup_client("Parent", ciphersuite, parent_provider);
1864 let parent_group = MlsGroup::builder()
1865 .ciphersuite(ciphersuite)
1866 .build(parent_provider, &parent_signer, parent_cwk)
1867 .unwrap();
1868
1869 let result = alice_group
1870 .commit_builder()
1871 .branch(alice_provider.rand(), &parent_group.branch_info())
1872 .unwrap()
1873 .load_psks(alice_provider.storage())
1874 .unwrap()
1875 .build(
1876 alice_provider.rand(),
1877 alice_provider.crypto(),
1878 &alice_signer,
1879 |_| true,
1880 );
1881
1882 assert!(
1883 matches!(
1884 result,
1885 Err(CreateCommitError::ProposalValidationError(
1886 ProposalValidationError::Psk(PskError::NotAllowed)
1887 ))
1888 ),
1889 "expected a branch PSK outside the initial commit to be rejected, got {result:?}"
1890 );
1891 }
1892}